From f907cf165b2620855c6493b7cd032b9d4003d1cc Mon Sep 17 00:00:00 2001 From: RalfG Date: Thu, 12 Mar 2026 16:07:20 +0100 Subject: [PATCH 01/15] Remove calibration parameter from finetune_and_predict This option is never useful as you can't have a fitted calibration for a model that doesn't exist yet. --- deeplc/core.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/deeplc/core.py b/deeplc/core.py index ab6d0ce..b2050ae 100644 --- a/deeplc/core.py +++ b/deeplc/core.py @@ -131,7 +131,6 @@ def finetune_and_predict( psm_list: PSMList, psm_list_reference: PSMList, model: torch.nn.Module | PathLike | str | None = None, - calibration: Calibration | None = None, partial_freeze: bool = False, train_kwargs: dict | None = None, predict_kwargs: dict | None = None, @@ -147,9 +146,6 @@ def finetune_and_predict( List of PSMs to use as reference for fine-tuning and calibration. model Trained model or path to model file. - calibration - Calibration instance to use. If already fitted, it will be re-fitted after fine-tuning - using the same options. If None, a simple PiecewiseLinearCalibration is used. partial_freeze If True, only the final layer of the model will be finetuned. train_kwargs @@ -180,23 +176,13 @@ def finetune_and_predict( ) # Fit calibration - # Validate passed calibration instance or create default if None - if calibration is None: - LOGGER.info("No calibration provided, using PiecewiseLinearCalibration by default.") - calibration = PiecewiseLinearCalibration() - elif not isinstance(calibration, Calibration): - raise ValueError( - f"Expected calibration to be of type Calibration, got {type(calibration)}" - ) - - # TODO: Is this necessary? Should it work equally well without calibration? - # Fit calibration if not already fitted LOGGER.info("Fitting calibration with fine-tuned model predictions...") if any(psm_list_reference["is_decoy"]): # remove this one since already in finetune? LOGGER.warning( "Reference PSM list contains decoy PSMs. " "These will be included in the calibration fitting." ) + calibration = PiecewiseLinearCalibration() target_rt_cal = np.array(psm_list_reference["retention_time"], dtype=np.float32) source_rt_cal = predict( psm_list=psm_list_reference, @@ -253,7 +239,6 @@ def finetune( training_dataset, validation_dataset = split_datasets( training_data, validation_data=validation_data, validation_split=validation_split ) - LOGGER.info("Training new model...") finetuned_model = _model_ops.train( model=model or DEFAULT_MODEL, train_dataset=training_dataset, From d0ef7f18d2f56eedeb3599e108d6639c36876b41 Mon Sep 17 00:00:00 2001 From: RalfG Date: Thu, 12 Mar 2026 16:27:08 +0100 Subject: [PATCH 02/15] Simplify and improve progress bar --- deeplc/_model_ops.py | 108 +++++++++++++++++++++++++------------------ 1 file changed, 62 insertions(+), 46 deletions(-) diff --git a/deeplc/_model_ops.py b/deeplc/_model_ops.py index 1f80eec..a9e1e01 100644 --- a/deeplc/_model_ops.py +++ b/deeplc/_model_ops.py @@ -7,7 +7,14 @@ from pathlib import Path import torch -from rich.progress import track +from rich.progress import ( + BarColumn, + MofNCompleteColumn, + Progress, + TextColumn, + TimeRemainingColumn, + track, +) from torch.utils.data import DataLoader, Dataset, Subset from deeplc._architecture import DeepLCModel @@ -74,25 +81,20 @@ def load_model( ) -> torch.nn.Module: """Load a model from a file or return a randomly initialized model if none is provided.""" # If device is not specified, use the default device (GPU if available, else CPU) - selected_device = device or torch.device( - "cuda" if torch.cuda.is_available() else "cpu" - ) + selected_device = device or torch.device("cuda" if torch.cuda.is_available() else "cpu") # Load model from file if a path is provided if isinstance(model, str | Path): - loaded_model = torch.load( - model, weights_only=False, map_location=selected_device - ) + loaded_model = torch.load(model, weights_only=False, map_location=selected_device) elif isinstance(model, torch.nn.Module): loaded_model = model + logger.debug("Using provided PyTorch model instance") elif model is None: # Initialize a new model with default architecture loaded_model = DeepLCModel() logger.debug("Initialized new DeepLCModel with default architecture") else: - raise TypeError( - f"Expected a PyTorch Module or a file path, got {type(model)} instead." - ) + raise TypeError(f"Expected a PyTorch Module or a file path, got {type(model)} instead.") # Ensure the model is on the specified device loaded_model.to(selected_device) @@ -111,6 +113,7 @@ def train( batch_size: int = 512, patience: int = 10, trainable_layers: str | None = None, + show_progress: bool = True, ) -> torch.nn.Module: """ Train or fine-tune the model. @@ -138,6 +141,8 @@ def train( trainable_layers If provided, only layers containing this keyword in their name will be trainable. All other layers will be frozen. If None, all layers are trainable. + show_progress + If True, display a Rich progress bar during training. If False, run silently. Returns ------- @@ -150,8 +155,6 @@ def train( # Promote ONNX initializer buffers (dense head) to trainable parameters model = promote_buffers_to_parameters(model) - # Freeze layers if requested - # Freeze layers if requested if trainable_layers is not None: _freeze_layers(model, trainable_layers) @@ -175,24 +178,28 @@ def train( best_val_loss = float("inf") epochs_no_improve = 0 - for epoch in range(epochs): - avg_loss = _train_epoch(model, train_loader, optimizer, loss_fn, device) - avg_val_loss = _validate_epoch(model, val_loader, loss_fn, device) - - logger.debug( - f"Epoch {epoch + 1}/{epochs}, " - f"Loss: {avg_loss:.4f}, " - f"Validation Loss: {avg_val_loss:.4f}" - ) - - if avg_val_loss < best_val_loss: - best_val_loss = avg_val_loss - best_model_wts = copy.deepcopy(model.state_dict()) - epochs_no_improve = 0 - else: - epochs_no_improve += 1 + with _create_progress(disable=not show_progress) as progress: + epoch_task = progress.add_task("Epochs", total=epochs, status="") + + for _epoch in range(epochs): + avg_loss = _train_epoch(model, train_loader, optimizer, loss_fn, device) + avg_val_loss = _validate_epoch(model, val_loader, loss_fn, device) + + # Early stopping check + if avg_val_loss < best_val_loss: + best_val_loss = avg_val_loss + best_model_wts = copy.deepcopy(model.state_dict()) + epochs_no_improve = 0 + else: + epochs_no_improve += 1 + + # Update epoch bar with loss info + status = f"loss={avg_loss:.4f} val_loss={avg_val_loss:.4f} best={best_val_loss:.4f}" + if epochs_no_improve >= patience: + status += " [yellow]early stop[/yellow]" + progress.update(epoch_task, advance=1, status=status) + if epochs_no_improve >= patience: - logger.debug(f"Early stopping triggered at epoch {epoch + 1}") break model.load_state_dict(best_model_wts) @@ -208,10 +215,8 @@ def predict( ) -> torch.Tensor: """Predict using the model for the given dataset.""" model = load_model(model, device) - data_loader = DataLoader( - data, batch_size=batch_size, shuffle=False, num_workers=num_workers - ) - predictions = _predict_epoch(model, data_loader, device) + data_loader = DataLoader(data, batch_size=batch_size, shuffle=False, num_workers=num_workers) + predictions = _predict_epoch(model, data_loader, device, show_progress=True) return predictions.cpu().detach() @@ -224,9 +229,7 @@ def evaluate( ) -> float: """Evaluate the model on the given dataset.""" model = load_model(model, device) - data_loader = DataLoader( - data, batch_size=batch_size, shuffle=False, num_workers=num_workers - ) + data_loader = DataLoader(data, batch_size=batch_size, shuffle=False, num_workers=num_workers) loss_fn = torch.nn.L1Loss() avg_loss = _validate_epoch(model, data_loader, loss_fn, device) return avg_loss @@ -238,9 +241,7 @@ def _freeze_layers(model: torch.nn.Module, unfreeze_keyword: str) -> None: param.requires_grad = unfreeze_keyword in name -def _get_optimizer( - model: torch.nn.Module, learning_rate: float -) -> torch.optim.Optimizer: +def _get_optimizer(model: torch.nn.Module, learning_rate: float) -> torch.optim.Optimizer: return torch.optim.Adam( filter(lambda p: p.requires_grad, model.parameters()), lr=learning_rate, @@ -257,7 +258,7 @@ def _train_epoch( """Train the model for one epoch.""" model.train() running_loss = 0.0 - for features, targets in track(data_loader): + for features, targets in data_loader: features = [feature_tensor.to(device) for feature_tensor in features] targets = targets.to(device).view(-1, 1) optimizer.zero_grad() @@ -266,8 +267,7 @@ def _train_epoch( loss.backward() optimizer.step() running_loss += loss.item() - avg_loss = float(running_loss / len(data_loader)) - return avg_loss + return float(running_loss / len(data_loader)) def _validate_epoch( @@ -280,26 +280,42 @@ def _validate_epoch( model.eval() val_loss = 0.0 with torch.no_grad(): - for features, targets in track(data_loader): + for features, targets in data_loader: features = [feature_tensor.to(device) for feature_tensor in features] targets = targets.to(device).view(-1, 1) outputs = model(*features) val_loss += loss_fn(outputs, targets).item() - avg_val_loss = float(val_loss / len(data_loader)) - return avg_val_loss + return float(val_loss / len(data_loader)) def _predict_epoch( model: torch.nn.Module, data_loader: DataLoader, device: str, + show_progress: bool = False, ) -> torch.Tensor: """Predict using the model for one epoch.""" model.eval() predictions = [] with torch.no_grad(): - for features, _ in track(data_loader): + for features, _ in track( + data_loader, description="Predicting...", transient=True, disable=not show_progress + ): features = [feature_tensor.to(device) for feature_tensor in features] outputs = model(*features) predictions.append(outputs.cpu()) return torch.cat(predictions, dim=0).squeeze() + + +def _create_progress(disable: bool = False) -> Progress: + """Create a Rich progress bar for training.""" + return Progress( + TextColumn("[bold blue]{task.description}"), + BarColumn(), + MofNCompleteColumn(), + TextColumn("|"), + TimeRemainingColumn(), + TextColumn("|"), + TextColumn("{task.fields[status]}"), + disable=disable, + ) From be62266b496a14ddf0d6b96d6b2ada43a6f8e5f0 Mon Sep 17 00:00:00 2001 From: RalfG Date: Thu, 12 Mar 2026 16:30:31 +0100 Subject: [PATCH 03/15] Remove partial freeze support, as this mostly reduces finetuning performance --- deeplc/_model_ops.py | 16 +--------------- deeplc/core.py | 10 +--------- 2 files changed, 2 insertions(+), 24 deletions(-) diff --git a/deeplc/_model_ops.py b/deeplc/_model_ops.py index a9e1e01..b634705 100644 --- a/deeplc/_model_ops.py +++ b/deeplc/_model_ops.py @@ -68,7 +68,7 @@ def promote_buffers_to_parameters( init_mod._parameters[name] = torch.nn.Parameter(buf) promoted += 1 - logger.info( + logger.debug( f"Promoted {promoted} buffers to parameters. " f"Total trainable params: {sum(p.numel() for p in model.parameters() if p.requires_grad)}" ) @@ -112,7 +112,6 @@ def train( epochs: int = 25, batch_size: int = 512, patience: int = 10, - trainable_layers: str | None = None, show_progress: bool = True, ) -> torch.nn.Module: """ @@ -138,9 +137,6 @@ def train( Batch size for training and validation. patience Number of epochs with no improvement before early stopping. - trainable_layers - If provided, only layers containing this keyword in their name will be trainable. - All other layers will be frozen. If None, all layers are trainable. show_progress If True, display a Rich progress bar during training. If False, run silently. @@ -155,10 +151,6 @@ def train( # Promote ONNX initializer buffers (dense head) to trainable parameters model = promote_buffers_to_parameters(model) - # Freeze layers if requested - if trainable_layers is not None: - _freeze_layers(model, trainable_layers) - logger.debug(f"Frozen all layers except those containing '{trainable_layers}'") # Parse datasets; setup loaders train_loader = DataLoader( @@ -235,12 +227,6 @@ def evaluate( return avg_loss -def _freeze_layers(model: torch.nn.Module, unfreeze_keyword: str) -> None: - """Freeze all layers except those containing the unfreeze_keyword in their name.""" - for name, param in model.named_parameters(): - param.requires_grad = unfreeze_keyword in name - - def _get_optimizer(model: torch.nn.Module, learning_rate: float) -> torch.optim.Optimizer: return torch.optim.Adam( filter(lambda p: p.requires_grad, model.parameters()), diff --git a/deeplc/core.py b/deeplc/core.py index b2050ae..6f8188e 100644 --- a/deeplc/core.py +++ b/deeplc/core.py @@ -131,7 +131,6 @@ def finetune_and_predict( psm_list: PSMList, psm_list_reference: PSMList, model: torch.nn.Module | PathLike | str | None = None, - partial_freeze: bool = False, train_kwargs: dict | None = None, predict_kwargs: dict | None = None, ) -> np.ndarray: @@ -146,8 +145,6 @@ def finetune_and_predict( List of PSMs to use as reference for fine-tuning and calibration. model Trained model or path to model file. - partial_freeze - If True, only the final layer of the model will be finetuned. train_kwargs Additional keyword arguments to pass to the training function. predict_kwargs @@ -163,7 +160,6 @@ def finetune_and_predict( finetuned_model = finetune( psm_list=psm_list_reference, model=model, - partial_freeze=partial_freeze, train_kwargs=train_kwargs, ) @@ -177,7 +173,7 @@ def finetune_and_predict( # Fit calibration LOGGER.info("Fitting calibration with fine-tuned model predictions...") - if any(psm_list_reference["is_decoy"]): # remove this one since already in finetune? + if any(psm_list_reference["is_decoy"]): # TODO: remove this one since already in finetune? LOGGER.warning( "Reference PSM list contains decoy PSMs. " "These will be included in the calibration fitting." @@ -202,7 +198,6 @@ def finetune( psm_list_validation: PSMList | None = None, validation_split: float = 0.1, model: torch.nn.Module | PathLike | str | None = None, - partial_freeze: bool = False, train_kwargs: dict | None = None, ) -> torch.nn.Module: """ @@ -217,8 +212,6 @@ def finetune( used. model Trained model or path to model file. - partial_freeze - If True, only the final layer of the model will be finetuned. train_kwargs Additional keyword arguments to pass to the training function. @@ -243,7 +236,6 @@ def finetune( model=model or DEFAULT_MODEL, train_dataset=training_dataset, validation_dataset=validation_dataset, - trainable_layers="33_1" if partial_freeze else None, # TODO: Don't hardcode **(train_kwargs or {}), ) return finetuned_model From d79533099de13b0a5b1e27b6d223d7449401a506 Mon Sep 17 00:00:00 2001 From: RalfG Date: Thu, 12 Mar 2026 17:06:24 +0100 Subject: [PATCH 04/15] Add plain calibration function to core to avoid code duplication and add user friendly entry point for calibration of PSMs --- deeplc/core.py | 100 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 71 insertions(+), 29 deletions(-) diff --git a/deeplc/core.py b/deeplc/core.py index 6f8188e..6bff8d5 100644 --- a/deeplc/core.py +++ b/deeplc/core.py @@ -56,6 +56,67 @@ def predict( ).numpy() +def calibrate( + psm_list_reference: PSMList, + model: torch.nn.Module | PathLike | str | None = None, + calibration: Calibration | None = None, + predict_kwargs: dict | None = None, +) -> Calibration: + """ + Return a `Calibration` instance fitted to the reference dataset. + + Parameters + ---------- + psm_list_reference + List of PSMs to use as reference for calibration. + model + Trained model or path to model file. + calibration + Calibration instance to use. If None, SplineTransformerCalibration is used. + predict_kwargs + Additional keyword arguments to pass to the prediction function. + + Returns + ------- + Calibration + Fitted calibration instance. + + """ + # Get calibration + if calibration is None: + LOGGER.debug("No calibration provided, using SplineTransformerCalibration by default.") + calibration = SplineTransformerCalibration() + elif not isinstance(calibration, Calibration): + raise ValueError( + f"Expected calibration to be of type `Calibration`, got {type(calibration)}" + ) + if calibration.is_fitted: + LOGGER.warning( + "Provided Calibration is already fitted. Refitting will overwrite existing fit." + ) + + if any(psm_list_reference["is_decoy"]): + LOGGER.warning( + "Reference PSM list contains decoy PSMs. " + "These will be included in the calibration fitting." + ) + + # Predict initial retention times for the reference dataset + LOGGER.info("Predicting retention times for reference...") + source_rt_cal = predict( + psm_list=psm_list_reference, + model=model, + predict_kwargs=predict_kwargs, + ) + + # Fit calibration + LOGGER.info("Fitting calibration...") + target_rt_cal = np.array(psm_list_reference["retention_time"], dtype=np.float32) + calibration.fit(target=target_rt_cal, source=source_rt_cal) + + return calibration + + def predict_and_calibrate( psm_list: PSMList, psm_list_reference: PSMList, @@ -93,31 +154,19 @@ def predict_and_calibrate( predict_kwargs=predict_kwargs, ) - # Fit calibration - # Validate passed calibration instance or create default if None - if calibration is None: - LOGGER.debug("No calibration provided, using SplineTransformerCalibration by default.") - calibration = SplineTransformerCalibration() - elif not isinstance(calibration, Calibration): + if calibration is not None and not isinstance(calibration, Calibration): raise ValueError( - f"Expected calibration to be of type Calibration, got {type(calibration)}" + f"Expected calibration to be of type `Calibration`, got {type(calibration)}" ) # Fit calibration if not already fitted - if not calibration.is_fitted: - LOGGER.info("Fitting calibration...") - if any(psm_list_reference["is_decoy"]): - LOGGER.warning( - "Reference PSM list contains decoy PSMs. " - "These will be included in the calibration fitting." - ) - target_rt_cal = np.array(psm_list_reference["retention_time"], dtype=np.float32) - source_rt_cal = predict( - psm_list=psm_list_reference, + if calibration is None or not calibration.is_fitted: + calibration = calibrate( + psm_list_reference=psm_list_reference, model=model, + calibration=calibration, predict_kwargs=predict_kwargs, ) - calibration.fit(target=target_rt_cal, source=source_rt_cal) else: LOGGER.info("Calibration is already fitted, skipping fitting step.") @@ -171,21 +220,14 @@ def finetune_and_predict( predict_kwargs=predict_kwargs, ) - # Fit calibration + # Fit calibration with simple PiecewiseLinearCalibration to the fine-tuned model predictions LOGGER.info("Fitting calibration with fine-tuned model predictions...") - if any(psm_list_reference["is_decoy"]): # TODO: remove this one since already in finetune? - LOGGER.warning( - "Reference PSM list contains decoy PSMs. " - "These will be included in the calibration fitting." - ) - calibration = PiecewiseLinearCalibration() - target_rt_cal = np.array(psm_list_reference["retention_time"], dtype=np.float32) - source_rt_cal = predict( - psm_list=psm_list_reference, + calibration = calibrate( + psm_list_reference=psm_list_reference, model=finetuned_model, + calibration=PiecewiseLinearCalibration(), predict_kwargs=predict_kwargs, ) - calibration.fit(target=target_rt_cal, source=source_rt_cal) # Apply calibration to predictions calibrated_rt = calibration.transform(predicted_rt) From 09a2131c32b2ae6f6600528cec82ad8d300d5a3d Mon Sep 17 00:00:00 2001 From: RalfG Date: Thu, 12 Mar 2026 18:08:10 +0100 Subject: [PATCH 05/15] Fix missing encoding for terminal modifications --- deeplc/_features.py | 144 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 123 insertions(+), 21 deletions(-) diff --git a/deeplc/_features.py b/deeplc/_features.py index 3f01e5d..25214bd 100644 --- a/deeplc/_features.py +++ b/deeplc/_features.py @@ -98,6 +98,40 @@ def _fill_pos_matrix( return pos_mat +def _apply_composition_to_matrices( + mat: np.ndarray, + pos_mat: np.ndarray, + composition: mass.Composition, + i: int, + seq_len: int, + dict_index: dict[str, int], + dict_index_pos: dict[str, int], + positions: set[int], +) -> None: + """Apply a composition delta to the standard and positional matrices.""" + for atom_comp, change in composition.items(): + try: + mat[i, dict_index[atom_comp]] += change + if i in positions: + pos_mat[i, dict_index_pos[atom_comp]] += change + elif (i - seq_len) in positions: + pos_mat[i - seq_len, dict_index_pos[atom_comp]] += change + except KeyError: + try: + warnings.warn(f"Replacing pattern for atom: {atom_comp}", stacklevel=2) + atom_comp_clean = sub(r"\[.*?\]", "", atom_comp) + mat[i, dict_index[atom_comp_clean]] += change + if i in positions: + pos_mat[i, dict_index_pos[atom_comp_clean]] += change + elif (i - seq_len) in positions: + pos_mat[i - seq_len, dict_index_pos[atom_comp_clean]] += change + except KeyError: + warnings.warn(f"Ignoring atom {atom_comp} at pos {i}", stacklevel=2) + continue + except IndexError: + warnings.warn(f"Index error for atom {atom_comp} at pos {i}", stacklevel=2) + + def _apply_modifications( mat: np.ndarray, pos_mat: np.ndarray, @@ -118,27 +152,54 @@ def _apply_modifications( f"Skipping modification without known composition: {token[1]}", stacklevel=2 ) continue - for atom_comp, change in mod_comp.items(): + _apply_composition_to_matrices( + mat, + pos_mat, + mod_comp, + i, + seq_len, + dict_index, + dict_index_pos, + positions, + ) + + +def _apply_terminal_modifications( + mat: np.ndarray, + pos_mat: np.ndarray, + peptidoform: Peptidoform, + seq_len: int, + dict_index: dict[str, int], + dict_index_pos: dict[str, int], + positions: set[int], +) -> None: + """Apply N- and C-terminal modification changes to the matrices.""" + terminal_mods = [ + (0, peptidoform.properties.get("n_term")), # N-terminus at position 0 + (seq_len - 1, peptidoform.properties.get("c_term")), # C-terminus at last position + ] + for i, mods in terminal_mods: + if not mods: + continue + for tag in mods: try: - mat[i, dict_index[atom_comp]] += change - if i in positions: - pos_mat[i, dict_index_pos[atom_comp]] += change - elif (i - seq_len) in positions: - pos_mat[i - seq_len, dict_index_pos[atom_comp]] += change - except KeyError: - try: - warnings.warn(f"Replacing pattern for atom: {atom_comp}", stacklevel=2) - atom_comp_clean = sub(r"\[.*?\]", "", atom_comp) - mat[i, dict_index[atom_comp_clean]] += change - if i in positions: - pos_mat[i, dict_index_pos[atom_comp_clean]] += change - elif (i - seq_len) in positions: - pos_mat[i - seq_len, dict_index_pos[atom_comp_clean]] += change - except KeyError: - warnings.warn(f"Ignoring atom {atom_comp} at pos {i}", stacklevel=2) - continue - except IndexError: - warnings.warn(f"Index error for atom {atom_comp} at pos {i}", stacklevel=2) + mod_comp = tag.composition + except Exception: + warnings.warn( + f"Skipping terminal modification without known composition: {tag}", + stacklevel=2, + ) + continue + _apply_composition_to_matrices( + mat, + pos_mat, + mod_comp, + i, + seq_len, + dict_index, + dict_index_pos, + positions, + ) def _compute_rolling_sum(matrix: np.ndarray, n: int = 2) -> np.ndarray: @@ -159,7 +220,39 @@ def encode_peptidoform( dict_index_pos: dict[str, int] | None = None, dict_index: dict[str, int] | None = None, ) -> dict[str, np.ndarray]: - """Extract features from a single peptidoform.""" + """ + Extract features from a single peptidoform. + + Parameters + ---------- + peptidoform + The peptidoform to encode, either as a Peptidoform object or a string. + add_ccs_features + Whether to include CCS features. Default is False. + padding_length + The maximum length of the sequence after padding. Default is 60. + positions + The positions to consider for feature extraction. Default is DEFAULT_POSITIONS. + positions_pos + The positive positions to consider for feature extraction. Default is + DEFAULT_POSITIONS_POS. + positions_neg + The negative positions to consider for feature extraction. Default is + DEFAULT_POSITIONS_NEG. + dict_aa + A dictionary mapping amino acids to indices. Default is DEFAULT_DICT_AA. + dict_index_pos + A dictionary mapping atoms to indices for the positional matrix. Default is + DEFAULT_DICT_INDEX_POS. + dict_index + A dictionary mapping atoms to indices. Default is DEFAULT_DICT_INDEX. + + Returns + ------- + dict[str, np.ndarray] + A dictionary of Numpy arrays containing the extracted features. + + """ positions = positions or DEFAULT_POSITIONS positions_pos = positions_pos or DEFAULT_POSITIONS_POS positions_neg = positions_neg or DEFAULT_POSITIONS_NEG @@ -187,6 +280,15 @@ def encode_peptidoform( dict_index_pos, positions, ) + _apply_terminal_modifications( + std_matrix, + pos_matrix, + peptidoform, + seq_len, + dict_index, + dict_index_pos, + positions, + ) matrix_all = np.sum(std_matrix, axis=0) matrix_all = np.append(matrix_all, seq_len) From 5a2af0702f41b77518d0597a22396f57a54998dd Mon Sep 17 00:00:00 2001 From: RalfG Date: Thu, 12 Mar 2026 18:09:59 +0100 Subject: [PATCH 06/15] Update function order in _features; add todo for fixed modifications --- deeplc/_features.py | 214 ++++++++++++++++++++++---------------------- 1 file changed, 108 insertions(+), 106 deletions(-) diff --git a/deeplc/_features.py b/deeplc/_features.py index 25214bd..d10745b 100644 --- a/deeplc/_features.py +++ b/deeplc/_features.py @@ -1,5 +1,7 @@ """Feature extraction for DeepLC.""" +# TODO: Consider ProForma fixed modifications (that are not applied yet) for feature extraction. + from __future__ import annotations import logging @@ -26,6 +28,112 @@ # fmt: on +def encode_peptidoform( + peptidoform: Peptidoform | str, + add_ccs_features: bool = False, + padding_length: int = 60, + positions: set[int] | None = None, + positions_pos: set[int] | None = None, + positions_neg: set[int] | None = None, + dict_aa: dict[str, int] | None = None, + dict_index_pos: dict[str, int] | None = None, + dict_index: dict[str, int] | None = None, +) -> dict[str, np.ndarray]: + """ + Extract features from a single peptidoform. + + Parameters + ---------- + peptidoform + The peptidoform to encode, either as a Peptidoform object or a string. + add_ccs_features + Whether to include CCS features. Default is False. + padding_length + The maximum length of the sequence after padding. Default is 60. + positions + The positions to consider for feature extraction. Default is DEFAULT_POSITIONS. + positions_pos + The positive positions to consider for feature extraction. Default is + DEFAULT_POSITIONS_POS. + positions_neg + The negative positions to consider for feature extraction. Default is + DEFAULT_POSITIONS_NEG. + dict_aa + A dictionary mapping amino acids to indices. Default is DEFAULT_DICT_AA. + dict_index_pos + A dictionary mapping atoms to indices for the positional matrix. Default is + DEFAULT_DICT_INDEX_POS. + dict_index + A dictionary mapping atoms to indices. Default is DEFAULT_DICT_INDEX. + + Returns + ------- + dict[str, np.ndarray] + A dictionary of Numpy arrays containing the extracted features. + + """ + positions = positions or DEFAULT_POSITIONS + positions_pos = positions_pos or DEFAULT_POSITIONS_POS + positions_neg = positions_neg or DEFAULT_POSITIONS_NEG + dict_aa = dict_aa or DEFAULT_DICT_AA + dict_index_pos = dict_index_pos or DEFAULT_DICT_INDEX_POS + dict_index = dict_index or DEFAULT_DICT_INDEX + + if isinstance(peptidoform, str): + peptidoform = Peptidoform(peptidoform) + seq = peptidoform.sequence + charge = peptidoform.precursor_charge + seq, seq_len = _truncate_sequence(seq, padding_length) + + std_matrix = _fill_standard_matrix(seq, padding_length, dict_index) + onehot_matrix = _fill_onehot_matrix(peptidoform.parsed_sequence, padding_length, dict_aa) + pos_matrix = _fill_pos_matrix( + seq, seq_len, positions_pos, positions_neg, dict_index, dict_index_pos + ) + _apply_modifications( + std_matrix, + pos_matrix, + peptidoform.parsed_sequence, + seq_len, + dict_index, + dict_index_pos, + positions, + ) + _apply_terminal_modifications( + std_matrix, + pos_matrix, + peptidoform, + seq_len, + dict_index, + dict_index_pos, + positions, + ) + + matrix_all = np.sum(std_matrix, axis=0) + matrix_all = np.append(matrix_all, seq_len) + if add_ccs_features: + if not charge: + raise ValueError(f"Peptidoform has no charge: {peptidoform}") + matrix_all = np.append(matrix_all, (seq.count("H")) / seq_len) + matrix_all = np.append( + matrix_all, (seq.count("F") + seq.count("W") + seq.count("Y")) / seq_len + ) + matrix_all = np.append(matrix_all, (seq.count("D") + seq.count("E")) / seq_len) + matrix_all = np.append(matrix_all, (seq.count("K") + seq.count("R")) / seq_len) + matrix_all = np.append(matrix_all, charge) + + matrix_sum = _compute_rolling_sum(std_matrix.T, n=2)[:, ::2].T + + matrix_global = np.concatenate([matrix_all, pos_matrix.flatten()]) + + return { + "matrix": std_matrix, + "matrix_sum": matrix_sum, + "matrix_global": matrix_global, + "matrix_hc": onehot_matrix, + } + + def _truncate_sequence(seq: str, max_length: int) -> tuple[str, int]: """Truncate the sequence if it exceeds the max_length.""" if len(seq) > max_length: @@ -207,109 +315,3 @@ def _compute_rolling_sum(matrix: np.ndarray, n: int = 2) -> np.ndarray: ret = np.cumsum(matrix, axis=1, dtype=np.float32) ret[:, n:] = ret[:, n:] - ret[:, :-n] return ret[:, n - 1 :] - - -def encode_peptidoform( - peptidoform: Peptidoform | str, - add_ccs_features: bool = False, - padding_length: int = 60, - positions: set[int] | None = None, - positions_pos: set[int] | None = None, - positions_neg: set[int] | None = None, - dict_aa: dict[str, int] | None = None, - dict_index_pos: dict[str, int] | None = None, - dict_index: dict[str, int] | None = None, -) -> dict[str, np.ndarray]: - """ - Extract features from a single peptidoform. - - Parameters - ---------- - peptidoform - The peptidoform to encode, either as a Peptidoform object or a string. - add_ccs_features - Whether to include CCS features. Default is False. - padding_length - The maximum length of the sequence after padding. Default is 60. - positions - The positions to consider for feature extraction. Default is DEFAULT_POSITIONS. - positions_pos - The positive positions to consider for feature extraction. Default is - DEFAULT_POSITIONS_POS. - positions_neg - The negative positions to consider for feature extraction. Default is - DEFAULT_POSITIONS_NEG. - dict_aa - A dictionary mapping amino acids to indices. Default is DEFAULT_DICT_AA. - dict_index_pos - A dictionary mapping atoms to indices for the positional matrix. Default is - DEFAULT_DICT_INDEX_POS. - dict_index - A dictionary mapping atoms to indices. Default is DEFAULT_DICT_INDEX. - - Returns - ------- - dict[str, np.ndarray] - A dictionary of Numpy arrays containing the extracted features. - - """ - positions = positions or DEFAULT_POSITIONS - positions_pos = positions_pos or DEFAULT_POSITIONS_POS - positions_neg = positions_neg or DEFAULT_POSITIONS_NEG - dict_aa = dict_aa or DEFAULT_DICT_AA - dict_index_pos = dict_index_pos or DEFAULT_DICT_INDEX_POS - dict_index = dict_index or DEFAULT_DICT_INDEX - - if isinstance(peptidoform, str): - peptidoform = Peptidoform(peptidoform) - seq = peptidoform.sequence - charge = peptidoform.precursor_charge - seq, seq_len = _truncate_sequence(seq, padding_length) - - std_matrix = _fill_standard_matrix(seq, padding_length, dict_index) - onehot_matrix = _fill_onehot_matrix(peptidoform.parsed_sequence, padding_length, dict_aa) - pos_matrix = _fill_pos_matrix( - seq, seq_len, positions_pos, positions_neg, dict_index, dict_index_pos - ) - _apply_modifications( - std_matrix, - pos_matrix, - peptidoform.parsed_sequence, - seq_len, - dict_index, - dict_index_pos, - positions, - ) - _apply_terminal_modifications( - std_matrix, - pos_matrix, - peptidoform, - seq_len, - dict_index, - dict_index_pos, - positions, - ) - - matrix_all = np.sum(std_matrix, axis=0) - matrix_all = np.append(matrix_all, seq_len) - if add_ccs_features: - if not charge: - raise ValueError(f"Peptidoform has no charge: {peptidoform}") - matrix_all = np.append(matrix_all, (seq.count("H")) / seq_len) - matrix_all = np.append( - matrix_all, (seq.count("F") + seq.count("W") + seq.count("Y")) / seq_len - ) - matrix_all = np.append(matrix_all, (seq.count("D") + seq.count("E")) / seq_len) - matrix_all = np.append(matrix_all, (seq.count("K") + seq.count("R")) / seq_len) - matrix_all = np.append(matrix_all, charge) - - matrix_sum = _compute_rolling_sum(std_matrix.T, n=2)[:, ::2].T - - matrix_global = np.concatenate([matrix_all, pos_matrix.flatten()]) - - return { - "matrix": std_matrix, - "matrix_sum": matrix_sum, - "matrix_global": matrix_global, - "matrix_hc": onehot_matrix, - } From 6bbffa35c5d627beb87fca6263f71ab31bbb0790 Mon Sep 17 00:00:00 2001 From: RalfG Date: Fri, 13 Mar 2026 16:45:26 +0100 Subject: [PATCH 07/15] Improve linear calibration; remove 'simplified' option in spline calibration. Linear calibration improvements: - Set defaults to 20 splits and use_median false to avoid jagged calibration lines - Introduce min_samples_per_segment to avoid setting badly fit anchor points in sparse RT regions --- deeplc/calibration.py | 58 +++++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/deeplc/calibration.py b/deeplc/calibration.py index 9621638..b85c798 100644 --- a/deeplc/calibration.py +++ b/deeplc/calibration.py @@ -57,9 +57,10 @@ def transform(self, source: np.ndarray) -> np.ndarray: class PiecewiseLinearCalibration(Calibration): def __init__( self, - number_of_splits: int = 50, + number_of_splits: int = 20, extrapolate: bool = True, - use_median: bool = True, + use_median: bool = False, + min_samples_per_segment: int = 10, ) -> None: """ Piece-wise linear calibration based on per-split anchors. @@ -74,11 +75,19 @@ def __init__( If False, clips input values to the fitted range. use_median : bool If True, uses the median of each segment to define anchors. If False, uses the mean. + min_samples_per_segment : int + Minimum number of samples required for a segment to contribute an anchor. + Segments with fewer samples are skipped, which helps avoid unstable anchors in + sparse regions when using many splits. """ super().__init__() self.number_of_splits = int(number_of_splits) self.extrapolate = bool(extrapolate) self.use_median = bool(use_median) + self.min_samples_per_segment = int(min_samples_per_segment) + + if self.min_samples_per_segment < 1: + raise ValueError("`min_samples_per_segment` must be >= 1.") self._calibrate_min: float | None = None self._calibrate_max: float | None = None @@ -117,8 +126,9 @@ def fit(self, target: np.ndarray, source: np.ndarray) -> None: starts: np.ndarray = np.searchsorted(source, boundaries[:-1], side="left") # type: ignore[var-annotated] ends: np.ndarray = np.searchsorted(source, boundaries[1:], side="left") # type: ignore[var-annotated] - # Filter out empty segments - valid_segments = ends > starts + # Filter out sparse segments + counts = ends - starts + valid_segments = counts >= self.min_samples_per_segment starts = starts[valid_segments] ends = ends[valid_segments] @@ -234,33 +244,27 @@ def fit( self, target: np.ndarray, source: np.ndarray, - simplified: bool = False, ) -> None: """Fit a spline-based model mapping source to target values.""" target, source = _prepare_series(target, source) - # TODO: What's the use of `simplified`? Was taken from original code. - if simplified: - linear_model = LinearRegression() - linear_model.fit(source.reshape(-1, 1), target) - linear_model_left = linear_model - spline_model = linear_model - linear_model_right = linear_model - else: - spline = SplineTransformer(degree=4, n_knots=int(len(source) / 500) + 5) - spline_model = make_pipeline(spline, LinearRegression()) - spline_model.fit(source.reshape(-1, 1), target) - - n_top = int(len(source) * 0.1) - X_left = source[:n_top] - y_left = target[:n_top] - linear_model_left = LinearRegression() - linear_model_left.fit(X_left.reshape(-1, 1), y_left) - - X_right = source[-n_top:] - y_right = target[-n_top:] - linear_model_right = LinearRegression() - linear_model_right.fit(X_right.reshape(-1, 1), y_right) + # Spline model + spline = SplineTransformer(degree=4, n_knots=int(len(source) / 500) + 5) + spline_model = make_pipeline(spline, LinearRegression()) + spline_model.fit(source.reshape(-1, 1), target) + + # Linear fit for left trail + n_top = int(len(source) * 0.1) + X_left = source[:n_top] + y_left = target[:n_top] + linear_model_left = LinearRegression() + linear_model_left.fit(X_left.reshape(-1, 1), y_left) + + # Linear fit for right trail + X_right = source[-n_top:] + y_right = target[-n_top:] + linear_model_right = LinearRegression() + linear_model_right.fit(X_right.reshape(-1, 1), y_right) self._calibrate_min = float(np.min(source)) self._calibrate_max = float(np.max(source)) From 1401fe27ea7ca5d78fe5621bcebfd8257e378af8 Mon Sep 17 00:00:00 2001 From: RalfG Date: Fri, 13 Mar 2026 16:49:28 +0100 Subject: [PATCH 08/15] Improve logging and progress bar --- deeplc/_model_ops.py | 2 +- deeplc/core.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/deeplc/_model_ops.py b/deeplc/_model_ops.py index b634705..a4de6ef 100644 --- a/deeplc/_model_ops.py +++ b/deeplc/_model_ops.py @@ -151,7 +151,6 @@ def train( # Promote ONNX initializer buffers (dense head) to trainable parameters model = promote_buffers_to_parameters(model) - # Parse datasets; setup loaders train_loader = DataLoader( train_dataset, batch_size=batch_size, shuffle=True, num_workers=num_workers @@ -304,4 +303,5 @@ def _create_progress(disable: bool = False) -> Progress: TextColumn("|"), TextColumn("{task.fields[status]}"), disable=disable, + transient=True, ) diff --git a/deeplc/core.py b/deeplc/core.py index 6bff8d5..42e1a74 100644 --- a/deeplc/core.py +++ b/deeplc/core.py @@ -48,7 +48,6 @@ def predict( Retention time predictions. """ - LOGGER.info("Predicting retention times...") return _model_ops.predict( model=model or DEFAULT_MODEL, data=DeepLCDataset.from_psm_list(psm_list), @@ -102,7 +101,7 @@ def calibrate( ) # Predict initial retention times for the reference dataset - LOGGER.info("Predicting retention times for reference...") + LOGGER.debug("Predicting retention times for reference...") source_rt_cal = predict( psm_list=psm_list_reference, model=model, @@ -110,7 +109,7 @@ def calibrate( ) # Fit calibration - LOGGER.info("Fitting calibration...") + LOGGER.debug("Fitting calibration...") target_rt_cal = np.array(psm_list_reference["retention_time"], dtype=np.float32) calibration.fit(target=target_rt_cal, source=source_rt_cal) From ac7e53def3b47967e3da7910b73dc3ebed980b0b Mon Sep 17 00:00:00 2001 From: RalfG Date: Fri, 13 Mar 2026 22:09:44 +0100 Subject: [PATCH 09/15] Change to SplineCalibration by default after finetuning --- deeplc/core.py | 1 - 1 file changed, 1 deletion(-) diff --git a/deeplc/core.py b/deeplc/core.py index 42e1a74..8034a6a 100644 --- a/deeplc/core.py +++ b/deeplc/core.py @@ -224,7 +224,6 @@ def finetune_and_predict( calibration = calibrate( psm_list_reference=psm_list_reference, model=finetuned_model, - calibration=PiecewiseLinearCalibration(), predict_kwargs=predict_kwargs, ) From fe9bb733b7b523668b676e52516c9f512dd1cf0d Mon Sep 17 00:00:00 2001 From: RalfG Date: Fri, 13 Mar 2026 22:10:34 +0100 Subject: [PATCH 10/15] Hard fix promote buffers to parameters in model files, avoiding fix on every model load --- deeplc/_model_ops.py | 55 ------------------ ...73_pub_1fd8363d9af9dcad3be7553c39396960.pt | Bin 5705546 -> 5712181 bytes ...73_pub_8c22d89667368f2f02ad996469ba157e.pt | Bin 2620298 -> 2626933 bytes ...73_pub_cb975cfdd4105f97efa0b3afffe075cc.pt | Bin 3649354 -> 3655989 bytes 4 files changed, 55 deletions(-) diff --git a/deeplc/_model_ops.py b/deeplc/_model_ops.py index a4de6ef..99aa922 100644 --- a/deeplc/_model_ops.py +++ b/deeplc/_model_ops.py @@ -23,58 +23,6 @@ logger = logging.getLogger(__name__) -# TODO: Implement Lightning? - - -def promote_buffers_to_parameters( - model: torch.nn.Module, - buffer_indices: list[int] | None = None, -) -> torch.nn.Module: - """ - Promote ONNX initializer buffers to nn.Parameters so they become trainable. - - ONNX-converted GraphModules (from onnx2torch) store dense/FC layer weights as - buffers on an ``initializers`` submodule, making them invisible to the optimizer. - This function converts selected buffers to nn.Parameters so they can be fine-tuned. - - Parameters - ---------- - model - The loaded GraphModule from onnx2torch. - buffer_indices - Indices of ``onnx_initializer_*`` buffers to promote. If None, promotes the - global feature branch (0-5) and the final dense head (34-45). - - Returns - ------- - torch.nn.Module - The same model with buffers promoted to parameters. - - """ - if buffer_indices is None: - # Dense head (34-45) + global feature branch (0-5) - buffer_indices = list(range(0, 6)) + list(range(34, 46)) - - init_mod = dict(model.named_modules()).get("initializers") - if init_mod is None: - logger.debug("No 'initializers' submodule found; skipping buffer promotion.") - return model - - promoted = 0 - for idx in buffer_indices: - name = f"onnx_initializer_{idx}" - if name in init_mod._buffers: - buf = init_mod._buffers.pop(name) - init_mod._parameters[name] = torch.nn.Parameter(buf) - promoted += 1 - - logger.debug( - f"Promoted {promoted} buffers to parameters. " - f"Total trainable params: {sum(p.numel() for p in model.parameters() if p.requires_grad)}" - ) - return model - - def load_model( model: torch.nn.Module | PathLike | str | None = None, device: str | None = None, @@ -148,9 +96,6 @@ def train( """ model = load_model(model, device) - # Promote ONNX initializer buffers (dense head) to trainable parameters - model = promote_buffers_to_parameters(model) - # Parse datasets; setup loaders train_loader = DataLoader( train_dataset, batch_size=batch_size, shuffle=True, num_workers=num_workers diff --git a/deeplc/package_data/models/full_hc_PXD005573_pub_1fd8363d9af9dcad3be7553c39396960.pt b/deeplc/package_data/models/full_hc_PXD005573_pub_1fd8363d9af9dcad3be7553c39396960.pt index 82399a45e1a36cfb48c61838d826308efc288854..9f2a7db0570ca7ecbd1a1857b2b777140e5178a6 100644 GIT binary patch delta 43440 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 delta 36906 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

3rfdLUrihmo}=tRB)za6p1a}! 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 delta 37168 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 Date: Mon, 16 Mar 2026 13:50:24 +0100 Subject: [PATCH 11/15] Improve piece-wise linear calibration: Introduce min_samples_per_segment option to avoid overfitting in sparsely populated sections; set sensible default parameters to 10 splits and 20 min_samples_per_segment. --- deeplc/calibration.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/deeplc/calibration.py b/deeplc/calibration.py index b85c798..cf344cb 100644 --- a/deeplc/calibration.py +++ b/deeplc/calibration.py @@ -57,10 +57,10 @@ def transform(self, source: np.ndarray) -> np.ndarray: class PiecewiseLinearCalibration(Calibration): def __init__( self, - number_of_splits: int = 20, + number_of_splits: int = 10, extrapolate: bool = True, use_median: bool = False, - min_samples_per_segment: int = 10, + min_samples_per_segment: int = 20, ) -> None: """ Piece-wise linear calibration based on per-split anchors. @@ -123,14 +123,20 @@ def fit(self, target: np.ndarray, source: np.ndarray) -> None: raise CalibrationError("Source values have zero or invalid range; cannot calibrate.") boundaries = np.linspace(cal_min, cal_max, self.number_of_splits + 1, dtype=np.float32) - starts: np.ndarray = np.searchsorted(source, boundaries[:-1], side="left") # type: ignore[var-annotated] - ends: np.ndarray = np.searchsorted(source, boundaries[1:], side="left") # type: ignore[var-annotated] - - # Filter out sparse segments - counts = ends - starts - valid_segments = counts >= self.min_samples_per_segment - starts = starts[valid_segments] - ends = ends[valid_segments] + starts_raw: np.ndarray = np.searchsorted(source, boundaries[:-1], side="left") # type: ignore[var-annotated] + ends_raw: np.ndarray = np.searchsorted(source, boundaries[1:], side="left") # type: ignore[var-annotated] + + # Merge adjacent sparse segments by assigning each segment to a group based on + # how many min_samples-sized chunks the cumulative count has crossed so far. + # Segments whose cumulative count falls within the same chunk share a group id + # and are merged into a single anchor. + counts = ends_raw - starts_raw + group_ids = (np.cumsum(counts) - 1) // self.min_samples_per_segment + group_start_indices = np.concatenate(([0], np.flatnonzero(np.diff(group_ids)) + 1)) + group_end_indices = np.concatenate((group_start_indices[1:] - 1, [len(starts_raw) - 1])) + + starts = starts_raw[group_start_indices] + ends = ends_raw[group_end_indices] # Compute anchors for all segments aggregate_func = np.median if self.use_median else np.mean From 9c40b0db8a2d339b09d94789b0d255bce5a922cd Mon Sep 17 00:00:00 2001 From: RalfG Date: Mon, 16 Mar 2026 13:50:55 +0100 Subject: [PATCH 12/15] Add show_progress parameter to predict --- deeplc/_model_ops.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/deeplc/_model_ops.py b/deeplc/_model_ops.py index 99aa922..0b334f2 100644 --- a/deeplc/_model_ops.py +++ b/deeplc/_model_ops.py @@ -148,11 +148,12 @@ def predict( device: str = "cpu", batch_size: int = 512, num_workers: int = 0, + show_progress: bool = True, ) -> torch.Tensor: """Predict using the model for the given dataset.""" model = load_model(model, device) data_loader = DataLoader(data, batch_size=batch_size, shuffle=False, num_workers=num_workers) - predictions = _predict_epoch(model, data_loader, device, show_progress=True) + predictions = _predict_epoch(model, data_loader, device, show_progress=show_progress) return predictions.cpu().detach() From d1daeea2a7044b8186abb6c15a49c321881c43f2 Mon Sep 17 00:00:00 2001 From: RalfG Date: Mon, 16 Mar 2026 13:51:00 +0100 Subject: [PATCH 13/15] Update tests --- tests/test_calibration.py | 24 ++++ tests/test_features.py | 264 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 288 insertions(+) create mode 100644 tests/test_features.py diff --git a/tests/test_calibration.py b/tests/test_calibration.py index 15b854d..229e0d5 100644 --- a/tests/test_calibration.py +++ b/tests/test_calibration.py @@ -77,3 +77,27 @@ def test_zero_range_predicted_raises(): cal = PiecewiseLinearCalibration(number_of_splits=10) with pytest.raises(CalibrationError): cal.fit(target=y, source=x) + + +def test_piecewise_skips_sparse_segments_with_min_samples_threshold(): + source_dense = np.linspace(0.0, 80.0, 1000, dtype=np.float32) + source_sparse = np.array([95.0, 97.0, 99.0], dtype=np.float32) + source = np.concatenate([source_dense, source_sparse]).astype(np.float32) + target = (1.2 * source) + 3.0 + + cal_no_threshold = PiecewiseLinearCalibration( + number_of_splits=100, + min_samples_per_segment=1, + ) + cal_no_threshold.fit(target=target, source=source) + x_no_threshold, _ = cal_no_threshold.get_calibration_curve() + + cal_threshold = PiecewiseLinearCalibration( + number_of_splits=100, + min_samples_per_segment=10, + ) + cal_threshold.fit(target=target, source=source) + x_threshold, _ = cal_threshold.get_calibration_curve() + + assert x_threshold.size > 1 + assert x_threshold.size < x_no_threshold.size diff --git a/tests/test_features.py b/tests/test_features.py new file mode 100644 index 0000000..a396874 --- /dev/null +++ b/tests/test_features.py @@ -0,0 +1,264 @@ +"""Tests for deeplc._features.encode_peptidoform.""" + +from __future__ import annotations + +import warnings + +import numpy as np +import pytest +from psm_utils import Peptidoform +from pyteomics import mass + +from deeplc._features import ( + DEFAULT_DICT_AA, + DEFAULT_DICT_INDEX, + DEFAULT_DICT_INDEX_POS, + DEFAULT_POSITIONS, + DEFAULT_POSITIONS_NEG, + DEFAULT_POSITIONS_POS, + encode_peptidoform, +) + +# HELPERS + +PADDING = 60 + +# Number of rows in pos_matrix: max(positions) - min(positions) + 1 +# DEFAULT_POSITIONS = {0,1,2,3,-1,-2,-3,-4} → 3 - (-4) + 1 = 8 +_POS_ROWS = max(DEFAULT_POSITIONS) - min(DEFAULT_POSITIONS) + 1 +# matrix_global = sum(std_matrix, axis=0) [6] + seq_len [1] + pos_matrix.flatten() [8*6] +_GLOBAL_BASE_LEN = len(DEFAULT_DICT_INDEX) + 1 + _POS_ROWS * len(DEFAULT_DICT_INDEX_POS) +# _compute_rolling_sum(std_matrix.T, n=2)[:, ::2].T → (30, 6) +_SUM_ROWS = (PADDING - 1) // 2 # == 29 for n=2, stride 2 on 59 cols + + +class TestReturnStructure: + """Tests that encode_peptidoform returns the expected keys and shapes.""" + + def test_returns_four_keys(self): + result = encode_peptidoform("ACDE") + assert set(result.keys()) == {"matrix", "matrix_sum", "matrix_global", "matrix_hc"} + + def test_matrix_shape(self): + result = encode_peptidoform("ACDE") + assert result["matrix"].shape == (PADDING, len(DEFAULT_DICT_INDEX)) + + def test_matrix_hc_shape(self): + result = encode_peptidoform("ACDE") + assert result["matrix_hc"].shape == (PADDING, len(DEFAULT_DICT_AA)) + + def test_matrix_global_shape_no_ccs(self): + result = encode_peptidoform("ACDE") + assert result["matrix_global"].shape == (_GLOBAL_BASE_LEN,) + + def test_matrix_global_shape_with_ccs(self): + result = encode_peptidoform("ACDE/2", add_ccs_features=True) + # add_ccs_features appends 5 extra values (H%, FWY%, DE%, KR%, charge) + assert result["matrix_global"].shape == (_GLOBAL_BASE_LEN + 5,) + + def test_matrix_sum_shape(self): + result = encode_peptidoform("ACDE") + assert result["matrix_sum"].ndim == 2 + assert result["matrix_sum"].shape[1] == len(DEFAULT_DICT_INDEX) + + def test_matrix_dtype(self): + result = encode_peptidoform("ACDE") + assert result["matrix"].dtype == np.float16 + + def test_matrix_hc_dtype(self): + result = encode_peptidoform("ACDE") + assert result["matrix_hc"].dtype == np.float16 + + +class TestStringInput: + """Tests that both str and Peptidoform inputs are accepted and equivalent.""" + + def test_str_and_peptidoform_are_equivalent(self): + str_result = encode_peptidoform("ACDE") + pf_result = encode_peptidoform(Peptidoform("ACDE")) + for key in str_result: + np.testing.assert_array_equal(str_result[key], pf_result[key]) + + +class TestPaddingAndSeqLen: + """Tests that padding and sequence length are handled correctly.""" + + def test_padded_rows_are_zero(self): + seq = "ACDE" + result = encode_peptidoform(seq) + # Rows beyond seq length should be all zeros in standard matrix + assert np.all(result["matrix"][len(seq) :] == 0) + + def test_padded_rows_are_zero_onehot(self): + seq = "ACDE" + result = encode_peptidoform(seq) + assert np.all(result["matrix_hc"][len(seq) :] == 0) + + def test_seq_len_encoded_in_matrix_global(self): + seq = "ACDE" + result = encode_peptidoform(seq) + # matrix_global[len(DEFAULT_DICT_INDEX)] holds seq_len + assert result["matrix_global"][len(DEFAULT_DICT_INDEX)] == len(seq) + + def test_truncation_warns(self): + long_seq = "A" * (PADDING + 5) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + result = encode_peptidoform(long_seq) + assert any("Truncating" in str(warning.message) for warning in w) + # After truncation seq_len == PADDING + assert result["matrix_global"][len(DEFAULT_DICT_INDEX)] == PADDING + + +class TestOneHotEncoding: + """Tests the one-hot (matrix_hc) component.""" + + def test_first_residue_one_hot(self): + # "A" is index 5 in DEFAULT_DICT_AA + result = encode_peptidoform("ACDE") + assert result["matrix_hc"][0, DEFAULT_DICT_AA["A"]] == 1.0 + + def test_second_residue_one_hot(self): + result = encode_peptidoform("ACDE") + assert result["matrix_hc"][1, DEFAULT_DICT_AA["C"]] == 1.0 + + def test_each_residue_has_exactly_one_hot(self): + seq = "ACDE" + result = encode_peptidoform(seq) + for i in range(len(seq)): + assert result["matrix_hc"][i].sum() == 1.0 + + def test_padded_rows_are_zero_and_no_hot(self): + seq = "AC" + result = encode_peptidoform(seq) + assert result["matrix_hc"][2:].sum() == 0.0 + + +class TestStandardMatrixComposition: + """Tests that atomic composition in std_matrix is correct.""" + + def test_glycine_carbon_count(self): + # Glycine (G): C2 H3 N1 O1 — check carbon at index 0 + result = encode_peptidoform("G") + c_idx = DEFAULT_DICT_INDEX["C"] + expected_c = mass.std_aa_comp["G"]["C"] + assert result["matrix"][0, c_idx] == expected_c + + def test_unmodified_and_modified_differ_in_affected_residue(self): + # Oxidized methionine adds one O + unmod = encode_peptidoform("ACMDE") + mod = encode_peptidoform("ACM[Oxidation]DE") + o_idx = DEFAULT_DICT_INDEX["O"] + assert mod["matrix"][2, o_idx] > unmod["matrix"][2, o_idx] + + def test_modification_does_not_affect_other_residues(self): + unmod = encode_peptidoform("ACMDE") + mod = encode_peptidoform("ACM[Oxidation]DE") + for i in [0, 1, 3, 4]: + np.testing.assert_array_equal(mod["matrix"][i], unmod["matrix"][i]) + + +class TestNTerminalModification: + """Tests that N-terminal modifications are applied to position 0.""" + + def test_nterm_mod_changes_position_zero(self): + unmod = encode_peptidoform("ACDE") + mod = encode_peptidoform("[Acetyl]-ACDE") + # Acetyl adds C2H2O to position 0; at least carbon should increase + c_idx = DEFAULT_DICT_INDEX["C"] + assert mod["matrix"][0, c_idx] > unmod["matrix"][0, c_idx] + + def test_nterm_mod_does_not_affect_other_positions(self): + unmod = encode_peptidoform("ACDE") + mod = encode_peptidoform("[Acetyl]-ACDE") + for i in range(1, 4): + np.testing.assert_array_equal(mod["matrix"][i], unmod["matrix"][i]) + + def test_nterm_mod_reflected_in_matrix_global(self): + unmod = encode_peptidoform("ACDE") + mod = encode_peptidoform("[Acetyl]-ACDE") + # matrix_global contains the column sums so modification must change it + assert not np.array_equal(mod["matrix_global"], unmod["matrix_global"]) + + def test_nterm_mod_reflected_in_pos_matrix_part(self): + # Position 0 is in DEFAULT_POSITIONS_POS so pos_matrix row 0 must change + unmod = encode_peptidoform("ACDE") + mod = encode_peptidoform("[Acetyl]-ACDE") + # pos_matrix is concatenated at the end of matrix_global after the base part + base = len(DEFAULT_DICT_INDEX) + 1 # col sums + seq_len + pos_flat_unmod = unmod["matrix_global"][base:] + pos_flat_mod = mod["matrix_global"][base:] + assert not np.array_equal(pos_flat_unmod, pos_flat_mod) + + +class TestCTerminalModification: + """Tests that C-terminal modifications are applied to the last residue position.""" + + def test_cterm_mod_changes_last_residue_position(self): + seq = "ACDE" + unmod = encode_peptidoform(seq) + mod = encode_peptidoform("ACDE-[Amidation]") + last = len(seq) - 1 + # Amidation changes N count (replaces O with NH2) + assert not np.array_equal(mod["matrix"][last], unmod["matrix"][last]) + + def test_cterm_mod_does_not_affect_other_positions(self): + unmod = encode_peptidoform("ACDE") + mod = encode_peptidoform("ACDE-[Amidation]") + for i in range(0, 3): + np.testing.assert_array_equal(mod["matrix"][i], unmod["matrix"][i]) + + def test_cterm_mod_reflected_in_matrix_global(self): + unmod = encode_peptidoform("ACDE") + mod = encode_peptidoform("ACDE-[Amidation]") + assert not np.array_equal(mod["matrix_global"], unmod["matrix_global"]) + + +class TestBothTerminalModifications: + """Tests a peptide carrying both N- and C-terminal modifications.""" + + def test_both_term_mods_change_both_ends(self): + unmod = encode_peptidoform("ACDE") + mod = encode_peptidoform("[Acetyl]-ACDE-[Amidation]") + assert not np.array_equal(mod["matrix"][0], unmod["matrix"][0]) + assert not np.array_equal(mod["matrix"][3], unmod["matrix"][3]) + + def test_middle_residues_unchanged(self): + unmod = encode_peptidoform("ACDE") + mod = encode_peptidoform("[Acetyl]-ACDE-[Amidation]") + for i in [1, 2]: + np.testing.assert_array_equal(mod["matrix"][i], unmod["matrix"][i]) + + +class TestCCSFeatures: + """Tests the add_ccs_features flag.""" + + def test_ccs_features_requires_charge(self): + with pytest.raises(ValueError, match="no charge"): + encode_peptidoform("ACDE", add_ccs_features=True) + + def test_ccs_features_appends_five_values(self): + base = encode_peptidoform("ACDE/2") + ccs = encode_peptidoform("ACDE/2", add_ccs_features=True) + assert ccs["matrix_global"].shape[0] == base["matrix_global"].shape[0] + 5 + + def test_ccs_charge_value_position(self): + # matrix_global layout with CCS: + # [col_sums(6), seq_len(1), H%(1), FWY%(1), DE%(1), KR%(1), charge(1), pos_flat(48)] + charge = 3 + result = encode_peptidoform(f"ACDE/{charge}", add_ccs_features=True) + charge_idx = len(DEFAULT_DICT_INDEX) + 1 + 4 # 6 col sums + seq_len + 4 ratios + assert result["matrix_global"][charge_idx] == charge + + +class TestShortPeptide: + """Tests edge cases for short peptides.""" + + def test_single_residue(self): + result = encode_peptidoform("A") + assert result["matrix"].shape == (PADDING, len(DEFAULT_DICT_INDEX)) + assert result["matrix_hc"][0, DEFAULT_DICT_AA["A"]] == 1.0 + + def test_two_residues_no_crash(self): + result = encode_peptidoform("AC") + assert result["matrix_global"].shape == (_GLOBAL_BASE_LEN,) From 09fc121b20e888afe919af4c1dc55042f092dc6d Mon Sep 17 00:00:00 2001 From: RalfG Date: Mon, 16 Mar 2026 14:09:56 +0100 Subject: [PATCH 14/15] Packaging: Update manifest file --- MANIFEST.in | 2 -- 1 file changed, 2 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 49ee242..b05205c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1 @@ -include deeplc/models/* include deeplc/package_data/**/* -include deeplc/baseline_performance/* From c9689d1587792d53256bac62b0f5c768328b04c8 Mon Sep 17 00:00:00 2001 From: RalfG Date: Mon, 16 Mar 2026 14:13:44 +0100 Subject: [PATCH 15/15] Update changelog for alpha.1 release; fix linting --- CHANGELOG.md | 3 ++- deeplc/core.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa40f8a..95ef80f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## [4.0.0-alpha.1] ### Changed @@ -14,6 +14,7 @@ and this project adheres to - Switched deep learning framework from Tensorflow to PyTorch - Speed up predictions by removing ensemble method where output from three models with differing kernel sizes was averaged to one prediction - Separated calibration logic to dedicated reusable module with sklearn-like API. +- Improved computational efficiency of piece-wise linear calibration and set sensible default parameters - Built-in transfer learning functionality, instead of using external `deeplcretrainer` package. - Cleaned up package, removing legacy and unused code and files, and improving modularity - Modernized CI workflows to use `uv` diff --git a/deeplc/core.py b/deeplc/core.py index 8034a6a..0c93f8d 100644 --- a/deeplc/core.py +++ b/deeplc/core.py @@ -13,7 +13,6 @@ from deeplc import _model_ops from deeplc.calibration import ( Calibration, - PiecewiseLinearCalibration, SplineTransformerCalibration, ) from deeplc.data import DeepLCDataset, split_datasets