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, *, include_diagnostics: bool = True) -> 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
|
include_diagnostics
|
bool
|
For |
True
|
Returns:
| Type | Description |
|---|---|
tuple[ndarray, dict[str, float]]
|
Tuple of (normalized_signal, normalization_params) |
Source code in src/leech/features.py
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 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 | |
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
- 'level_skew': population skewness ( |
dict[str, ndarray]
|
All six are always computed and returned -- which of them a chunk |
dict[str, ndarray]
|
actually gets is decided later, by the run's recorded |
dict[str, ndarray]
|
|
Source code in src/leech/features.py
816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 | |
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 |