Models Module¶
Neural network architectures for nanopore signal classification.
Overview¶
The models module contains 29 PyTorch model architectures across 5 families. All models accept a signal_in_channels parameter for multi-channel signal input (default: 1).
Architectures come from two places, both reachable via get_model() and MODEL_REGISTRY:
- TOML configs in
leech/models/configs/— the ConvLSTM and TCN families (22 of the 29 names). Each config is a fully declarativekind = "graph"architecture built from the layer registry inleech.models.nn, with one[[variants]]entry per registry name. Adding a normalization/pooling variant is a[[variants]]entry, not a new class. - Hand-written classes for architectures not yet converted (Remora-compatible ConvLSTM, Transformer, ConvOnly, ResNet, SignalCNN).
Discovery is torch-free, so listing model names (e.g. for CLI --model choices)
does not pay the torch import cost.
Model Registry¶
get_model
¶
Get model by name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model_name
|
str
|
Name of model architecture |
required |
**kwargs
|
Any
|
Model-specific parameters (passed to model constructor) |
{}
|
Returns:
| Type | Description |
|---|---|
Module
|
Instantiated model |
Raises:
| Type | Description |
|---|---|
ValueError
|
If model_name not in registry |
Source code in src/leech/models/__init__.py
Config-Driven Architectures (ConvLSTM and TCN)¶
The ConvLSTM and TCN families are declared in TOML configs, not Python
classes. build_model_class() turns each declaration into a real class on
first access, so get_model("ConvLSTMDwell"), isinstance() checks, and
checkpoint loading all behave as if the class were hand-written.
| Registry names | Config |
|---|---|
ConvLSTMBase, ConvLSTMBaseBN, ConvLSTMDwell, ConvLSTMDwellBN |
configs/conv_lstm.toml |
ConvLSTMBaseAttn, ConvLSTMBaseBNAttn, ConvLSTMDwellAttn, ConvLSTMDwellBNAttn, ConvLSTMDwellGNAttn, ConvLSTMDwellLNAttn |
configs/conv_lstm_attn.toml |
TCNDwell, TCNDwellGN, TCNDwellLN |
configs/tcn_dwell.toml |
TCNDwellResidual, TCNDwellResidualGN, TCNDwellResidualLN |
configs/tcn_dwell_residual.toml |
TCNDwellResidualMotor, TCNDwellResidualLNMotor |
configs/tcn_dwell_residual_motor.toml |
TCNDwellResidualDwellAttn, TCNDwellResidualLNDwellAttn |
configs/tcn_dwell_residual_dwell_attn.toml |
TCNDwellSplitResidual, TCNDwellSplitResidualLN |
configs/tcn_dwell_split_residual.toml |
ConvLSTMDwell (multi-branch Conv-LSTM with dwell time features) is the
recommended default. It has three branches — signal (Conv1d on raw signal),
sequence (Conv1d on one-hot k-mers), and features (Conv1d on dwell + level
statistics) — merged into a BiLSTM followed by a fully connected head.
ConvLSTMBase is the same architecture without the feature branch; compare
the two to measure the impact of dwell features.
The TCN family replaces the BiLSTM with stacks of dilated causal convolutions.
Residual variants take a 2-channel signal input (raw + k-mer model residual),
SplitResidual keeps separate branches for the raw signal and the residual,
Motor adds motor-region pooling, and DwellAttn adds dwell-only
cross-attention.
Config Loader¶
config_loader
¶
Load model architectures declared in TOML config files.
A config declares one fully-declarative architecture — a list of nodes wired
into a :class:leech.models.nn.Graph — plus a [[variants]] block per
registry name it serves. Variants pin parameters that are not user-facing
(has_features, norm_type, ...), which is what removes the
normalisation/pooling subclass explosion: adding a variant is a config entry,
not a class.
:func:build_model_class turns a declaration into a real type, so
MODEL_REGISTRY[name] still yields a class: isinstance() works and
inspect.signature() returns the declared parameters (needed by
leech.model_loading._instantiate_model).
This module is deliberately torch-free at import time. Discovering and
parsing configs (used to render CLI --model choices) touches only
tomllib and leech.constants; torch is imported lazily inside
:func:build_model_class.
Layer Registry¶
nn
¶
Config-driven neural-network layer registry for leech.
Modeled on Oxford Nanopore's bonito (bonito/nn.py): every layer type is
registered in a name -> class table, layers know how to serialize themselves
via to_dict(), and from_dict() rebuilds an architecture from a plain
nested dict (which is what a TOML config parses to).
Composition primitives¶
Serial
Strictly sequential container (bonito's Serial).
Stack
Serial built by repeating one layer depth times.
Parallel
Fans a single input out to a set of named branches and concatenates
their outputs. This is the self-contained multi-branch primitive.
Graph
Flat named dataflow container. Unlike Parallel it accepts several
distinct model inputs (signal/sequence/features), routes each
to the branches that need it, and — crucially — installs every node as a
direct attribute of the root module. That keeps state_dict() keys
identical to the hand-written classes it replaces (signal_branch.
conv_layers.0.weight, lstm.weight_ih_l0, fc.1.weight, ...), so
existing checkpoints load unchanged.
Every leaf layer here either subclasses the torch.nn module it wraps or
subclasses the corresponding leech.models.components branch, so wrapping
never introduces an extra level of nesting in the state dict.
Serial
¶
Bases: Sequential
Strictly sequential container (bonito-compatible).
Stack
¶
Parallel
¶
Bases: Module
Fan one input out to named branches and merge their outputs.
This is the self-contained multi-branch primitive: it nests, so it can
appear anywhere a layer can. Note that nesting means branch parameters
live under <parallel_attr>.<branch_name>...; use :class:Graph when
flat (checkpoint-compatible) parameter names are required.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sublayers
|
dict[str, Module]
|
Mapping of branch name -> layer. |
required |
mode
|
str
|
|
'cat'
|
dim
|
int
|
Concatenation dimension when |
1
|
Graph
¶
Graph(nodes: list[dict], inputs: list[str] | None = None, output: str = 'logits', attrs: dict[str, Any] | None = None, build_order: list[str] | None = None)
Bases: BaseModel
Flat named dataflow container for multi-input, multi-branch models.
Each node is {"name", "layer", "inputs", "out"}:
name— the attribute the layer is installed under (this is what shows up instate_dict()keys, so it is chosen to match the hand-written classes being replaced).inputs— names of values in the environment to feed the layer.out— name to bind the result to (defaults toname). Rebinding an existing name (seq_feat -> seq_feat) makes optional nodes such asseq_pooldrop out cleanly when theirwhencondition is false.
Nodes execute in declaration order, so declaration order must respect dependencies. It is otherwise free, which lets configs reproduce the module-construction order of the original classes (and hence identical random initialisation for a given seed).
When dataflow order and module-construction order genuinely disagree — a
subclass that appended layers after its parent's head, say — pass
build_order: a permutation of the node names giving the order in which
layers are installed on the root module (and therefore the state_dict()
key order). Declaration order still drives execution.
register
¶
Register a layer class under its lower-cased class name.
to_dict
¶
Serialize a registered layer to a plain dict.
from_dict
¶
Rebuild a layer (or a whole architecture) from a plain dict.
Remora-compatible Architectures¶
ConvLSTMRemora
¶
ConvLSTMRemora(signal_len: int = 400, kmer_len: int = 11, num_features: int = DEFAULT_NUM_FEATURES, size: int = DEFAULT_REMORA_SIZE, num_out: int = 2, dropout: float = DEFAULT_REMORA_DROPOUT, seq_encoding: str = 'signal_kmer', signal_kmer_context: tuple[int, int] = (4, 4), signal_in_channels: int = 1)
Bases: BaseModel
Remora architecture + leech's dwell/signal feature branch.
Same as ConvLSTMRemoraBase but with a third branch for engineered features (dwell times + signal level statistics).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
signal_len
|
int
|
Length of input signal |
400
|
kmer_len
|
int
|
Length of k-mer sequence (unused, kept for API compat) |
11
|
num_features
|
int
|
Number of feature channels (dwell + signal levels) |
DEFAULT_NUM_FEATURES
|
size
|
int
|
Channel width throughout the network (default: 64) |
DEFAULT_REMORA_SIZE
|
num_out
|
int
|
Number of output classes (default: 2 for CrossEntropyLoss) |
2
|
dropout
|
float
|
Dropout probability (default: 0.3) |
DEFAULT_REMORA_DROPOUT
|
seq_encoding
|
str
|
Sequence encoding type ("signal_kmer" expected) |
'signal_kmer'
|
signal_kmer_context
|
tuple[int, int]
|
Kmer context for signal_kmer encoding |
(4, 4)
|
forward
¶
Forward pass.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
signal
|
Tensor
|
(B, signal_len) |
required |
sequence
|
Tensor
|
(B, C, signal_len) for signal_kmer or (B, 4, kmer_len) for base_onehot |
required |
features
|
Tensor
|
(B, num_features, kmer_len) |
required |
Returns:
| Type | Description |
|---|---|
Tensor
|
(B, num_out) logits |
predict_proba
¶
Predict probability of positive class.
ConvLSTMRemoraBase
¶
ConvLSTMRemoraBase(signal_len: int = 400, kmer_len: int = 11, size: int = DEFAULT_REMORA_SIZE, num_out: int = 2, dropout: float = DEFAULT_REMORA_DROPOUT, seq_encoding: str = 'signal_kmer', signal_kmer_context: tuple[int, int] = (4, 4), signal_in_channels: int = 1, **kwargs)
Bases: BaseModel
Pure Remora ConvLSTM reproduction (signal + sequence only).
Architecture matches Remora's default ConvLSTM: - BatchNorm + SiLU activations - Strided convolutions for downsampling - Merge convolution to fuse branches - Manual bidirectional LSTM (flip trick) - Single linear output layer
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
signal_len
|
int
|
Length of input signal |
400
|
kmer_len
|
int
|
Length of k-mer sequence (unused, kept for API compat) |
11
|
size
|
int
|
Channel width throughout the network (default: 64) |
DEFAULT_REMORA_SIZE
|
num_out
|
int
|
Number of output classes (default: 2 for CrossEntropyLoss) |
2
|
dropout
|
float
|
Dropout probability (default: 0.3) |
DEFAULT_REMORA_DROPOUT
|
seq_encoding
|
str
|
Sequence encoding type ("signal_kmer" expected) |
'signal_kmer'
|
signal_kmer_context
|
tuple[int, int]
|
Kmer context for signal_kmer encoding |
(4, 4)
|
forward
¶
Forward pass.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
signal
|
Tensor
|
(B, signal_len) |
required |
sequence
|
Tensor
|
(B, C, signal_len) for signal_kmer or (B, 4, kmer_len) for base_onehot |
required |
Returns:
| Type | Description |
|---|---|
Tensor
|
(B, num_out) logits |
predict_proba
¶
Predict probability of positive class.
Transformer Architectures¶
TransformerDwell¶
Transformer with multi-head self-attention and dwell features.
TransformerDwell
¶
TransformerDwell(signal_len: int = DEFAULT_SIGNAL_LEN, kmer_len: int = DEFAULT_KMER_LEN, num_features: int = DEFAULT_NUM_FEATURES, dwell_margin: int = DEFAULT_DWELL_MARGIN, d_model: int = 256, nhead: int = 8, num_layers: int = 4, dim_feedforward: int = 1024, dropout: float = DEFAULT_DROPOUT, seq_encoding: str = 'base_onehot', signal_kmer_context: tuple[int, int] = DEFAULT_SIGNAL_KMER_CONTEXT, signal_in_channels: int = 1, **kwargs)
Bases: BaseModel
Transformer-based model with cross-attention for learning motor-sensor offset.
Signal and sequence branches each get their own transformer encoder. Their outputs are concatenated and serve as Q for cross-attention against full-width dwell features (K/V), allowing each position to attend to dwell features at any offset.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
signal_len
|
int
|
Length of input signal |
DEFAULT_SIGNAL_LEN
|
kmer_len
|
int
|
Length of k-mer sequence (e.g., 2*context+1) |
DEFAULT_KMER_LEN
|
num_features
|
int
|
Number of feature channels (dwell + signal levels) |
DEFAULT_NUM_FEATURES
|
dwell_margin
|
int
|
Extra bases on each side of dwell window (default: 15) |
DEFAULT_DWELL_MARGIN
|
d_model
|
int
|
Dimension of transformer model (default: 256) |
256
|
nhead
|
int
|
Number of attention heads (default: 8) |
8
|
num_layers
|
int
|
Number of transformer encoder layers (default: 4) |
4
|
dim_feedforward
|
int
|
Dimension of feedforward network (default: 1024) |
1024
|
dropout
|
float
|
Dropout probability (default: 0.1) |
DEFAULT_DROPOUT
|
forward
¶
Forward pass.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
signal
|
Tensor
|
Raw signal (batch, signal_len) |
required |
sequence
|
Tensor
|
Encoded sequence (batch, 4, kmer_len) or (batch, 36, signal_len) |
required |
features
|
Tensor
|
Dwell + signal level features with full margin (batch, num_features, kmer_len + margin_left + margin_right) |
required |
Returns:
| Type | Description |
|---|---|
Tensor
|
Logits for binary classification (batch, 1) |
TransformerDwellResidual¶
Transformer with 2-channel signal input (raw + kmer residual).
TransformerDwellResidual
¶
Bases: TransformerDwell
TransformerDwell with 2-channel signal input (raw + kmer residual).
Identical to TransformerDwell except signal_in_channels defaults to 2, giving the model direct access to per-sample deviations from expected kmer levels.
Convolutional Architectures¶
ConvOnly¶
Pure convolutional network with multi-scale convolutions.
ConvOnly
¶
ConvOnly(signal_len: int = DEFAULT_SIGNAL_LEN, kmer_len: int = DEFAULT_KMER_LEN, num_features: int = DEFAULT_NUM_FEATURES, dwell_margin: int = DEFAULT_DWELL_MARGIN, base_channels: int = 16, num_blocks: int = 3, num_attn_heads: int = 4, dropout: float = DEFAULT_DROPOUT, seq_encoding: str = 'base_onehot', signal_kmer_context: tuple[int, int] = DEFAULT_SIGNAL_KMER_CONTEXT, signal_in_channels: int = 1, **kwargs)
Bases: BaseModel
Pure CNN model with cross-attention for learning motor-sensor offset.
Signal and sequence branches use Inception-style multi-scale convolutions. Their outputs are merged and serve as Q for cross-attention against full-width dwell features (K/V), allowing each position to attend to dwell features at any offset.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
signal_len
|
int
|
Length of input signal |
DEFAULT_SIGNAL_LEN
|
kmer_len
|
int
|
Length of k-mer sequence (e.g., 2*context+1) |
DEFAULT_KMER_LEN
|
num_features
|
int
|
Number of feature channels (dwell + signal levels) |
DEFAULT_NUM_FEATURES
|
dwell_margin
|
int
|
Extra bases on each side of dwell window (default: 15) |
DEFAULT_DWELL_MARGIN
|
base_channels
|
int
|
Base number of channels for inception blocks (default: 16) |
16
|
num_blocks
|
int
|
Number of inception blocks per branch (default: 3) |
3
|
num_attn_heads
|
int
|
Number of attention heads for cross-attention (default: 4) |
4
|
dropout
|
float
|
Dropout probability (default: 0.1) |
DEFAULT_DROPOUT
|
forward
¶
Forward pass.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
signal
|
Tensor
|
Raw signal (batch, signal_len) |
required |
sequence
|
Tensor
|
Encoded sequence (batch, 4, kmer_len) or (batch, 36, signal_len) |
required |
features
|
Tensor
|
Dwell + signal level features with full margin (batch, num_features, kmer_len + margin_left + margin_right) |
required |
Returns:
| Type | Description |
|---|---|
Tensor
|
Logits for binary classification (batch, 1) |
ResNetDwell¶
Deep residual network with skip connections.
ResNetDwell
¶
ResNetDwell(signal_len: int = DEFAULT_SIGNAL_LEN, kmer_len: int = DEFAULT_KMER_LEN, num_features: int = DEFAULT_NUM_FEATURES, dwell_margin: int = DEFAULT_DWELL_MARGIN, base_channels: int = 64, num_attn_heads: int = 4, dropout: float = DEFAULT_DROPOUT, seq_encoding: str = 'base_onehot', signal_kmer_context: tuple[int, int] = DEFAULT_SIGNAL_KMER_CONTEXT, signal_in_channels: int = 1, **kwargs)
Bases: BaseModel
ResNet model with cross-attention for learning motor-sensor offset.
Signal and sequence branches use ResNet1D. Their outputs are pooled to kmer_len positions and serve as Q for cross-attention against full-width dwell features (K/V), allowing each position to attend to dwell features at any offset.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
signal_len
|
int
|
Length of input signal |
DEFAULT_SIGNAL_LEN
|
kmer_len
|
int
|
Length of k-mer sequence (e.g., 2*context+1) |
DEFAULT_KMER_LEN
|
num_features
|
int
|
Number of feature channels (dwell + signal levels) |
DEFAULT_NUM_FEATURES
|
dwell_margin
|
int
|
Extra bases on each side of dwell window (default: 15) |
DEFAULT_DWELL_MARGIN
|
base_channels
|
int
|
Base number of channels (default: 64) |
64
|
num_attn_heads
|
int
|
Number of attention heads for cross-attention (default: 4) |
4
|
dropout
|
float
|
Dropout probability (default: 0.1) |
DEFAULT_DROPOUT
|
forward
¶
Forward pass.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
signal
|
Tensor
|
Raw signal (batch, signal_len) |
required |
sequence
|
Tensor
|
Encoded sequence (batch, 4, kmer_len) or (batch, 36, signal_len) |
required |
features
|
Tensor
|
Dwell + signal level features with full margin (batch, num_features, kmer_len + margin_left + margin_right) |
required |
Returns:
| Type | Description |
|---|---|
Tensor
|
Logits for binary classification (batch, 1) |
SignalCNN¶
Signal-only 1D-CNN classifier (ignores sequence and dwell inputs).
SignalCNN
¶
SignalCNN(num_classes: int = 2, signal_len: int = 256, channels: int = 32, signal_in_channels: int = 1, kernel_size: int = 7, seq_encoding: str = 'base_onehot', **_: object)
Bases: Module
Inference Wrappers¶
ModelInferenceWrapper
¶
Wrapper that provides unified forward pass interface for all model types.
This eliminates the need for conditional if/else blocks when calling models that have different input signatures (signal+sequence vs signal+sequence+features).
Example
Instead of:¶
if "features" in batch: logits = model(signal, sequence, features) else: logits = model(signal, sequence)
Use:¶
wrapper = ModelInferenceWrapper(model, model_type) logits = wrapper.forward_batch(batch, device)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
Module
|
PyTorch model to wrap |
required |
model_type
|
str
|
Model architecture name (e.g., "ConvLSTMDwell") |
required |
enable_repr_capture
¶
Register a hook to capture the penultimate representation.
The hook intercepts the input to the classifier/fc head so that
self.captured_repr holds the representation after each forward
pass. This is used by the adversarial and CL-regression heads.
Returns:
| Type | Description |
|---|---|
int
|
Dimension of the captured representation vector. |
forward_batch
¶
Forward pass from batch dictionary.
Automatically moves tensors to device and calls model with correct arguments.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
batch
|
dict
|
Batch dict with "signal", "sequence", and optionally "features" |
required |
device
|
str
|
Device to move tensors to |
required |
Returns:
| Type | Description |
|---|---|
Tensor
|
Model logits |
to
¶
Move model to device.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
device
|
str
|
Device to move to |
required |
Returns:
| Type | Description |
|---|---|
ModelInferenceWrapper
|
Self for chaining |