Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,21 @@ 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).

## [4.1.1] - 2026-08-26

### Changed

- The default model is now the fused-trunk multitask model trained across 6,543 LC
setups (`multitask_flexcnn_model.pt`, bundled since 4.1.0 as an opt-in). Every core
function and the command line use it when no `model` is given. The 4.0 default,
`multitask_model.pt`, stays bundled as `deeplc.core.LEGACY_MULTITASK_MODEL`; pass it
as `model=` to reproduce 4.0 and 4.1.0 predictions exactly.
- Uncalibrated `predict()` on a multitask model that carries setup names reports the
setup named by `deeplc.core.DEFAULT_TASK_NAME` (`PXD005573_mcp`, the 200-minute
gradient the DeepLC 1.x to 3.x models were trained on) instead of head 0, which for
the new default was an arbitrary setup. `return_matrix=True` is unchanged, and so are
calibration and fine-tuning, which select or fit the setup from the reference.

## [4.1.0] - 2026-08-20

### Added
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@

DeepLC predicts retention times for peptides carrying any modification. It does this by leveraging
a deep learning model based on atomic composition features. Starting with v4, DeepLC comes with a
multitask pretrained model covering multiple LC setups, enabling accurate predictions out of the
box. For best results on a specific dataset, predictions can be calibrated or fine-tuned
multitask pretrained model covering multiple LC setups (6,543 setups since v4.1.1), enabling
accurate predictions out of the box. For best results on a specific dataset, predictions can be calibrated or fine-tuned
using a small reference set of identified PSMs.

## Citation
Expand Down
46 changes: 37 additions & 9 deletions deeplc/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,20 @@
LOGGER = logging.getLogger(__name__)

DEEPLC_DIR = Path(__file__).resolve().parent
DEFAULT_MODEL = DEEPLC_DIR / "package_data" / "models" / "multitask_model.pt"
#: The model every core function uses when none is given: the fused-trunk multitask
#: model trained across 6,543 LC setups (see :data:`FLEXCNN_MULTITASK_MODEL`).
DEFAULT_MODEL = DEEPLC_DIR / "package_data" / "models" / "multitask_flexcnn_model.pt"

#: The 4.0 default, a shared-trunk model with one head per LC setup, kept so that
#: existing workflows can pin it: ``predict(psms, model=LEGACY_MULTITASK_MODEL)``.
LEGACY_MULTITASK_MODEL = DEEPLC_DIR / "package_data" / "models" / "multitask_model.pt"

#: The LC setup an uncalibrated ``predict()`` reports for a multitask model. The
#: default model has 6,543 setups and no reference to choose between them, so the
#: setup of the DeepLC 1.x to 3.x training data (PXD005573, 200-minute gradient) is
#: used, which keeps uncalibrated output on the gradient earlier versions reported.
#: Calibration and fine-tuning pick or fit the setup from the reference instead.
DEFAULT_TASK_NAME = "PXD005573_mcp"

#: Below this many reference PSMs, fine-tuning measured worse than calibration on
#: every held-out setup tried, so it is warned about rather than silently attempted.
Expand All @@ -40,10 +53,9 @@
#: near 1 %.
MAX_FINETUNE_ERROR_FRACTION = 0.15

#: Fused-trunk multitask model, trained across 6,543 LC setups. Not the default:
#: switching would change every prediction, so the choice is left to the caller
#: until the calibration path is adapted to its low-rank head.
FLEXCNN_MULTITASK_MODEL = DEEPLC_DIR / "package_data" / "models" / "multitask_flexcnn_model.pt"
#: Fused-trunk multitask model, trained across 6,543 LC setups. The default since
#: 4.1.1; the name is kept for callers that pass it explicitly.
FLEXCNN_MULTITASK_MODEL = DEFAULT_MODEL


def predict(
Expand All @@ -65,8 +77,10 @@ def predict(
Additional keyword arguments to pass to the prediction function.
return_matrix
If True, return the full prediction matrix of shape ``(n, n_heads)`` when using a
multitask model. If False (default), return a 1D array of shape ``(n,)`` using
head 0 when model output is 2D.
multitask model. If False (default), return a 1D array of shape ``(n,)`` for the
setup named by :data:`DEFAULT_TASK_NAME` when the model knows its setups, and head
0 otherwise. Uncalibrated output is on that setup's gradient; use
:func:`predict_and_calibrate` to map it onto your own.

Returns
-------
Expand Down Expand Up @@ -96,7 +110,7 @@ def predict(
and "task_idx" not in kwargs
and _model_ops.supports_task_subset(loaded_model)
):
kwargs["task_idx"] = [0]
kwargs["task_idx"] = [_default_task_idx(loaded_model)]

result = _model_ops.predict(
model=loaded_model,
Expand All @@ -106,10 +120,24 @@ def predict(
**kwargs,
).numpy()
if not return_matrix:
return result[:, 0]
return result[:, 0 if "task_idx" in kwargs else _default_task_idx(loaded_model)]
return result


def _default_task_idx(model: torch.nn.Module) -> int:
"""
Index of the setup an uncalibrated prediction reports.

:data:`DEFAULT_TASK_NAME` when the model carries setup names and lists it, else 0.
A model without names, or a model of a single setup, has nothing to choose from.
"""
names = getattr(model, "task_names", None) or []
try:
return list(names).index(DEFAULT_TASK_NAME)
except ValueError:
return 0


def calibrate(
psm_list_reference: PSMList,
model: torch.nn.Module | PathLike | str | None = None,
Expand Down
7 changes: 4 additions & 3 deletions docs/source/migration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,10 @@ Model checkpoints
=================

Legacy ``.hdf5`` checkpoints from v3 are not compatible with v4. The bundled model
has been retrained as a PyTorch multitask model (``multitask_model.pt``). Custom
``.hdf5`` checkpoints cannot be loaded; retrain using the v4 API. The new model
and should serve as an ideal starting point for fine-tuning to any custom setup.
has been retrained as a PyTorch multitask model (``multitask_flexcnn_model.pt``
since 4.1.1, covering 6,543 LC setups; ``multitask_model.pt`` in 4.0 and 4.1.0).
Custom ``.hdf5`` checkpoints cannot be loaded; retrain using the v4 API. The new
model should serve as an ideal starting point for fine-tuning to any custom setup.


Backend: TensorFlow → PyTorch
Expand Down
21 changes: 16 additions & 5 deletions docs/source/models.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,22 @@ Prediction models
Default model
=============

DeepLC 4.0 ships a pretrained multitask model (``multitask_model.pt``) as the
default. This model was trained jointly across multiple LC setups and outputs
one retention time prediction per setup. The best-fitting output head is
selected automatically during calibration based on Pearson correlation to the
observed retention times in the reference set.
DeepLC ships a pretrained multitask model as the default. Since 4.1.1 this is
``multitask_flexcnn_model.pt``: a fused-trunk convolutional model with a low-rank
multitask head, trained jointly across 6,543 LC setups from public repositories.
It outputs one retention time prediction (in minutes) per setup. The best-fitting
setup is selected automatically during calibration based on Pearson correlation to
the observed retention times in the reference set, and fine-tuning fits a new setup
head (66 parameters) on the reference with the trunk frozen.

Without calibration, :func:`deeplc.predict` reports the setup named by
:data:`deeplc.core.DEFAULT_TASK_NAME` (``PXD005573_mcp``, the 200-minute gradient
that DeepLC 1.x to 3.x models were trained on), or the full matrix with
``return_matrix=True``. The setup names are available as ``model.task_names``.

The 4.0 default, ``multitask_model.pt`` (shared trunk, one head per setup), stays
bundled as :data:`deeplc.core.LEGACY_MULTITASK_MODEL` and can be passed as
``model=`` to any core function to reproduce 4.0 and 4.1.0 predictions.

Training a model from scratch
==============================
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "deeplc"
version = "4.1.0"
version = "4.1.1"
description = "DeepLC: Retention time prediction for (modified) peptides using Deep Learning."
readme = "README.md"
license = { file = "LICENSE" }
Expand Down
39 changes: 39 additions & 0 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,45 @@ def test_predict_returns_matrix_when_flag_set():
assert result.shape[1] > 1


def test_default_model_is_the_flexcnn_multitask_model():
"""Since 4.1.1 the fused-trunk model of 6,543 setups is what a bare call loads."""
assert deeplc.core.DEFAULT_MODEL == deeplc.core.FLEXCNN_MULTITASK_MODEL
assert deeplc.core.DEFAULT_MODEL.name == "multitask_flexcnn_model.pt"
assert deeplc.core.LEGACY_MULTITASK_MODEL.name == "multitask_model.pt"
assert deeplc.core.LEGACY_MULTITASK_MODEL.exists()


def test_uncalibrated_predict_reports_the_default_setup():
"""
A bare ``predict`` returns the column of :data:`DEFAULT_TASK_NAME`, not head 0.

The default model lists thousands of setups, and head 0 is whichever sorted first.
The PXD005573 setup keeps uncalibrated output on the gradient DeepLC 1.x to 3.x
reported, so downstream code that never calibrated sees comparable numbers.
"""
model = deeplc.core._model_ops.load_model(deeplc.core.DEFAULT_MODEL, device="cpu")
idx = list(model.task_names).index(deeplc.core.DEFAULT_TASK_NAME)
assert idx != 0

psm_list = _make_psm_list(_PEPTIDES)
single = deeplc.core.predict(psm_list, predict_kwargs={"device": "cpu"})
matrix = deeplc.core.predict(psm_list, return_matrix=True, predict_kwargs={"device": "cpu"})
np.testing.assert_allclose(single, matrix[:, idx], rtol=1e-5, atol=1e-4)
assert not np.allclose(single, matrix[:, 0])
assert np.isfinite(single).all()


def test_legacy_multitask_model_still_loads_and_predicts():
"""The 4.0 default remains bundled and usable when pinned explicitly."""
result = deeplc.core.predict(
_make_psm_list(_PEPTIDES),
model=deeplc.core.LEGACY_MULTITASK_MODEL,
predict_kwargs={"device": "cpu"},
)
assert result.shape == (len(_PEPTIDES),)
assert np.isfinite(result).all()


def test_predict_and_calibrate_auto_selects_reference():
# 200 PSMs cycling through _PEPTIDES; 100 with qvalue<=0.01, 100 with qvalue=1.0.
# auto-selection picks the 100 low-qvalue PSMs as reference.
Expand Down
4 changes: 3 additions & 1 deletion tests/test_flexcnn.py
Original file line number Diff line number Diff line change
Expand Up @@ -549,8 +549,10 @@ def test_bundled_model_predicts_in_minutes():
assert -100.0 < out.min() < 60.0
assert 20.0 < out.max() < 1000.0

# Uncalibrated output reports the DeepLC 1.x to 3.x setup, not whichever sorted first.
default_idx = list(model.task_names).index(core.DEFAULT_TASK_NAME)
single = core.predict(peptides, model=path)
np.testing.assert_allclose(single, out[:, 0], rtol=1e-5)
np.testing.assert_allclose(single, out[:, default_idx], rtol=1e-5)


def test_small_reference_set_warns_and_widens_validation(tmp_path, caplog):
Expand Down
6 changes: 3 additions & 3 deletions tests/test_model_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from torch.utils.data import Dataset

from deeplc._architecture import DeepLCModel
from deeplc.core import DEFAULT_MODEL
from deeplc.core import LEGACY_MULTITASK_MODEL
from deeplc.data import split_datasets


Expand Down Expand Up @@ -59,15 +59,15 @@ def test_train_rejects_empty_validation_loader():


@pytest.mark.skipif(
not DEFAULT_MODEL.exists(),
not LEGACY_MULTITASK_MODEL.exists(),
reason="multitask model not bundled",
)
def test_load_multitask_model_without_prior_shim():
"""multitask_model.pt must load even when the legacy module is not pre-registered."""
# Remove any previously registered shim so the test is self-contained.
sys.modules.pop("multitask_model", None)

model = _model_ops.load_model(DEFAULT_MODEL, device="cpu")
model = _model_ops.load_model(LEGACY_MULTITASK_MODEL, device="cpu")

assert isinstance(model, DeepLCModel)

Expand Down
Loading