Skip to content

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 declarative kind = "graph" architecture built from the layer registry in leech.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(model_name: str, **kwargs: Any) -> nn.Module

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
def get_model(model_name: str, **kwargs: Any) -> nn.Module:
    """
    Get model by name.

    Args:
        model_name: Name of model architecture
        **kwargs: Model-specific parameters (passed to model constructor)

    Returns:
        Instantiated model

    Raises:
        ValueError: If model_name not in registry
    """
    if model_name not in MODEL_REGISTRY:
        available = ", ".join(sorted(MODEL_REGISTRY.keys()))
        raise ValueError(f"Unknown model '{model_name}'. Available: {available}")

    return MODEL_REGISTRY[model_name](**kwargs)

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.

discover_configs cached

discover_configs() -> dict[str, tuple[str, str | None]]

Map model name -> (config path, variant name).

Torch-free: only reads and parses TOML.

build_model_class cached

build_model_class(name: str) -> type

Build (and cache) the model class for a TOML-declared architecture.

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

Serial(sublayers: list[Module])

Bases: Sequential

Strictly sequential container (bonito-compatible).

Stack

Stack(sublayers: list[Module])

Bases: Serial

Serial formed by repeating one layer depth times.

Parallel

Parallel(sublayers: dict[str, Module], mode: str = 'cat', dim: int = 1)

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" (default) or "sum".

'cat'
dim int

Concatenation dimension when mode="cat".

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 in state_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 to name). Rebinding an existing name (seq_feat -> seq_feat) makes optional nodes such as seq_pool drop out cleanly when their when condition 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(layer: LayerT) -> LayerT

Register a layer class under its lower-cased class name.

to_dict

to_dict(layer: Any, include_weights: bool = False) -> dict

Serialize a registered layer to a plain dict.

from_dict

from_dict(model_dict: Any, layer_types: dict[str, type] | None = None) -> Any

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(signal: Tensor, sequence: Tensor, features: Tensor) -> torch.Tensor

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_proba(*args, **kwargs) -> torch.Tensor

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(signal: Tensor, sequence: Tensor) -> torch.Tensor

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_proba(*args, **kwargs) -> torch.Tensor

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(signal: Tensor, sequence: Tensor, features: Tensor) -> torch.Tensor

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

TransformerDwellResidual(*, signal_in_channels: int = 2, **kwargs)

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(signal: Tensor, sequence: Tensor, features: Tensor) -> torch.Tensor

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(signal: Tensor, sequence: Tensor, features: Tensor) -> torch.Tensor

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

ModelInferenceWrapper(model: Module, model_type: str)

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

parameters property

parameters

Access model parameters for optimizer.

enable_repr_capture

enable_repr_capture() -> int

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_batch(batch: dict, device: str) -> torch.Tensor

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

train

train() -> None

Set model to training mode.

eval

eval() -> None

Set model to evaluation mode.

to

to(device: str) -> ModelInferenceWrapper

Move model to device.

Parameters:

Name Type Description Default
device str

Device to move to

required

Returns:

Type Description
ModelInferenceWrapper

Self for chaining

state_dict

state_dict()

Get model state dict for checkpointing.

load_state_dict

load_state_dict(state_dict)

Load model state dict from checkpoint.

RemoraModelWrapper

RemoraModelWrapper(model_path: str | Path, device: str = 'cpu')

Wraps a TorchScript Remora model for leech's inference engine.

Remora models expect: - signal: (B, 1, signal_len) — note the channel dim - enc_kmer: (B, 36, signal_len) — signal-level kmer encoding

And output (B, 2) logits for two-class classification.

forward_batch

forward_batch(batch: dict, device: str) -> torch.Tensor

Run batched inference.

Parameters:

Name Type Description Default
batch dict

Dict with "signal" (B, signal_len) and "sequence" (B, 36, signal_len)

required
device str

Target device

required

Returns:

Type Description
Tensor

Logits of shape (B, 1)