diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 522e183e..c833216b 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: ["3.8", "3.9", "3.10", "3.11"]
+ python-version: ["3.9", "3.10", "3.11"]
steps:
- uses: actions/checkout@v4
diff --git a/README.md b/README.md
index f2ed978e..8539fb32 100644
--- a/README.md
+++ b/README.md
@@ -10,11 +10,6 @@
Modular and user-friendly platform for AI-assisted rescoring of peptide identifications
-> ⚠️ Note: This is the documentation for the fully redeveloped version 3.0 of MS²Rescore. While
-> MS²Rescore 3.0 has been drastically improved over the previous version, you might run into some
-> unforeseen issues. Please report any issues you encounter on the [issue tracker][issues] or post
-> your questions on the [GitHub Discussions][discussions] forum.
-
## About MS²Rescore
MS²Rescore performs ultra-sensitive peptide identification rescoring with LC-MS predictors such as
@@ -52,8 +47,7 @@ timsTOF fragmentation and IM2Deep for ion mobility separation. Bruker .d and min
files are directly supported through the [timsrust](https://github.com/MannLabs/timsrust) library.
Checkout our [preprint](https://doi.org/10.1101/2024.05.29.596400) for more information and the
-[TIMS²Rescore documentation](https://ms2rescore.readthedocs.io/en/stable/userguide/tims2rescore)
-to get started.
+[TIMS²Rescore documentation][tims2rescore] to get started.
## Citing
@@ -104,11 +98,11 @@ make a [pull request][pr]!
[issues]: https://github.com/compomics/ms2rescore/issues/
[discussions]: https://github.com/compomics/ms2rescore/discussions/
[pr]: https://github.com/compomics/ms2rescore/pulls/
-[desktop]: https://ms2rescore.readthedocs.io/gui.html
+[desktop]: https://ms2rescore.readthedocs.io/en/stable/gui/
[desktop-installer]: https://github.com/compomics/ms2rescore/releases/latest
-[cli]: https://ms2rescore.readthedocs.io/cli/cli.html
-[python-package]: https://ms2rescore.readthedocs.io/api/ms2rescore.html
-[docker]: https://ms2rescore.readthedocs.io/installation.html#docker-container
+[cli]: https://ms2rescore.readthedocs.io/en/stable/cli/
+[python-package]: https://ms2rescore.readthedocs.io/en/stable/api/ms2rescore/
+[docker]: https://ms2rescore.readthedocs.io/en/stable/installation#docker-container
[publication-branch]: https://github.com/compomics/ms2rescore/tree/pub
[ms2pip]: https://github.com/compomics/ms2pip
[deeplc]: https://github.com/compomics/deeplc
@@ -116,3 +110,4 @@ make a [pull request][pr]!
[mokapot]: https://mokapot.readthedocs.io/
[psm_utils]: https://github.com/compomics/psm_utils
[file-formats]: https://psm-utils.readthedocs.io/en/stable/#supported-file-formats
+[tims2rescore]: https://ms2rescore.readthedocs.io/en/stable/userguide/tims2Rescore
diff --git a/docs/source/_static/img/ms2rescore-overview.png b/docs/source/_static/img/ms2rescore-overview.png
index d4a4001d..2e7eb061 100644
Binary files a/docs/source/_static/img/ms2rescore-overview.png and b/docs/source/_static/img/ms2rescore-overview.png differ
diff --git a/docs/source/config_schema.md b/docs/source/config_schema.md
index 209bc009..5d8422b2 100644
--- a/docs/source/config_schema.md
+++ b/docs/source/config_schema.md
@@ -67,6 +67,7 @@
- **One of**
- *string*
- *null*
+ - **`write_flashlfq`** *(boolean)*: Write results to a FlashLFQ-compatible file. Default: `false`.
- **`write_report`** *(boolean)*: Write an HTML report with various QC metrics and charts. Default: `false`.
- **`profile`** *(boolean)*: Write a txt report using cProfile for profiling. Default: `false`.
## Definitions
@@ -93,7 +94,6 @@
- **`train_fdr`** *(number)*: FDR threshold for training Mokapot. Minimum: `0`. Maximum: `1`. Default: `0.01`.
- **`write_weights`** *(boolean)*: Write Mokapot weights to a text file. Default: `false`.
- **`write_txt`** *(boolean)*: Write Mokapot results to a text file. Default: `false`.
- - **`write_flashlfq`** *(boolean)*: Write Mokapot results to a FlashLFQ-compatible file. Default: `false`.
- **`percolator`** *(object)*: Percolator rescoring engine configuration. Can contain additional properties. Refer to *[#/definitions/rescoring_engine](#definitions/rescoring_engine)*.
- **`init-weights`**: Weights file for scoring function. Default: `false`.
- **One of**
diff --git a/docs/source/tutorials/in-depth-python-api.ipynb b/docs/source/tutorials/in-depth-python-api.ipynb
index 502c7d06..0aa96d82 100644
--- a/docs/source/tutorials/in-depth-python-api.ipynb
+++ b/docs/source/tutorials/in-depth-python-api.ipynb
@@ -7,6 +7,18 @@
"# Using the Python API "
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "This tutorial shows how to use the MS²Rescore Python API for each step of the rescoring process\n",
+ "individually. This is useful if you want to customize rescoring for your own Python\n",
+ "workflow or if you want to understand how MS²Rescore works.\n",
+ "\n",
+ "Note that the full MS²Rescore workflow is also available from Python with the single function call\n",
+ "`ms2rescore.rescore()`."
+ ]
+ },
{
"cell_type": "code",
"execution_count": 1,
@@ -37342,7 +37354,7 @@
"from pyteomics.mgf import read as read_mgf\n",
"\n",
"for spectrum in read_mgf(\"../../../examples/mgf/20161213_NGHF_DBJ_SA_Exp3A_HeLa_1ug_7min_15000_02.mgf\"):\n",
- " print(spectrum[\"params\"][\"title\"])\n",
+ " print(spectrum[\"params\"][\"title\"]) # noqa T201\n",
" break"
]
},
@@ -37362,7 +37374,7 @@
"source": [
"import re\n",
"spectrum_id = re.match(r\".*scan=(\\d+)$\", spectrum[\"params\"][\"title\"]).group(1)\n",
- "print(spectrum_id)"
+ "print(spectrum_id) # noqa T201"
]
},
{
@@ -38120,7 +38132,6 @@
"metadata": {},
"outputs": [],
"source": [
- "import plotly.express as px\n",
"from ms2rescore.report.charts import (\n",
" calculate_feature_qvalues,\n",
" feature_ecdf_auc_bar,\n",
diff --git a/docs/source/userguide/configuration.rst b/docs/source/userguide/configuration.rst
index a692f774..28cba910 100644
--- a/docs/source/userguide/configuration.rst
+++ b/docs/source/userguide/configuration.rst
@@ -244,13 +244,13 @@ expression pattern that extracts the decoy status from the protein name:
.. code-block:: json
- "decoy_pattern": "DECOY_"
+ "id_decoy_pattern": "DECOY_"
.. tab:: TOML
.. code-block:: toml
- decoy_pattern = "DECOY_"
+ id_decoy_pattern = "DECOY_"
Multi-rank rescoring
diff --git a/examples/msgfplus-ms2rescore.json b/examples/msgfplus-ms2rescore.json
index ee6ec2a9..8088b723 100644
--- a/examples/msgfplus-ms2rescore.json
+++ b/examples/msgfplus-ms2rescore.json
@@ -7,9 +7,6 @@
},
"log_level": "debug",
"processes": 16,
- "feature_generators": {
- "basic": {}
- },
"rescoring_engine": {
"mokapot": {
"fasta_file": "examples/proteins/uniprot-proteome-human-contaminants.fasta",
diff --git a/examples/msgfplus-ms2rescore.toml b/examples/msgfplus-ms2rescore.toml
index 805a3617..533d9732 100644
--- a/examples/msgfplus-ms2rescore.toml
+++ b/examples/msgfplus-ms2rescore.toml
@@ -5,25 +5,7 @@ psm_reader_kwargs = { "score_column" = "PSMScore" }
log_level = "debug"
processes = 16
-# [ms2rescore.modification_mapping]
-
-# [ms2rescore.fixed_modifications]
-
-[ms2rescore.feature_generators.basic]
-# No options, but setting heading enables feature generator
-
-# [ms2rescore.feature_generators.ms2pip]
-# model = "HCD"
-# ms2_tolerance = 0.02
-
-# [ms2rescore.feature_generators.deeplc]
-# deeplc_retrain = false
-
-# [ms2rescore.feature_generators.maxquant]
-# No options, but setting heading enables feature generator
-
[ms2rescore.rescoring_engine.mokapot]
fasta_file = "examples/proteins/uniprot-proteome-human-contaminants.fasta"
write_weights = true
write_txt = true
-# write_flashlfq = true
diff --git a/ms2rescore/__init__.py b/ms2rescore/__init__.py
index 8b4a1eef..73535fac 100644
--- a/ms2rescore/__init__.py
+++ b/ms2rescore/__init__.py
@@ -1,6 +1,6 @@
"""MS²Rescore: Sensitive PSM rescoring with predicted MS² peak intensities and RTs."""
-__version__ = "3.1.1"
+__version__ = "3.1.2"
from warnings import filterwarnings
diff --git a/ms2rescore/__main__.py b/ms2rescore/__main__.py
index 400a1fad..a3b4ae94 100644
--- a/ms2rescore/__main__.py
+++ b/ms2rescore/__main__.py
@@ -209,12 +209,12 @@ def main(tims=False):
cli_args = parser.parse_args()
configurations = []
- if cli_args.config_file:
- configurations.append(cli_args.config_file)
if tims:
configurations.append(
json.load(importlib.resources.open_text(package_data, "config_default_tims.json"))
)
+ if cli_args.config_file:
+ configurations.append(cli_args.config_file)
configurations.append(cli_args)
try:
diff --git a/ms2rescore/core.py b/ms2rescore/core.py
index 170f1038..a02ab4f6 100644
--- a/ms2rescore/core.py
+++ b/ms2rescore/core.py
@@ -163,6 +163,16 @@ def rescore(configuration: Dict, psm_list: Optional[PSMList] = None) -> None:
logger.info(f"Writing output to {output_file_root}.psms.tsv...")
psm_utils.io.write_file(psm_list, output_file_root + ".psms.tsv", filetype="tsv")
+ if config["write_flashlfq"]:
+ logger.info(f"Writing output to {output_file_root}.flashlfq.tsv...")
+ psm_utils.io.write_file(
+ psm_list,
+ output_file_root + ".flashlfq.tsv",
+ filetype="flashlfq",
+ fdr_threshold=0.01,
+ only_target=True, # TODO: Make FDR threshold configurable
+ )
+
# Write report
if config["write_report"]:
try:
diff --git a/ms2rescore/feature_generators/deeplc.py b/ms2rescore/feature_generators/deeplc.py
index 207f8ba1..206d6a21 100644
--- a/ms2rescore/feature_generators/deeplc.py
+++ b/ms2rescore/feature_generators/deeplc.py
@@ -143,11 +143,9 @@ def add_features(self, psm_list: PSMList) -> None:
)
# Disable wild logging to stdout by Tensorflow, unless in debug mode
- with (
- contextlib.redirect_stdout(open(os.devnull, "w"))
- if not self._verbose
- else contextlib.nullcontext()
- ):
+ with contextlib.redirect_stdout(
+ open(os.devnull, "w", encoding="utf-8")
+ ) if not self._verbose else contextlib.nullcontext():
# Make new PSM list for this run (chain PSMs per spectrum to flat list)
psm_list_run = PSMList(psm_list=list(chain.from_iterable(psms.values())))
if self.calibration_set:
diff --git a/ms2rescore/gui/app.py b/ms2rescore/gui/app.py
index 50f8451e..29eefa2b 100644
--- a/ms2rescore/gui/app.py
+++ b/ms2rescore/gui/app.py
@@ -4,11 +4,11 @@
import logging
import multiprocessing
import os
+import platform
import sys
import webbrowser
from pathlib import Path
from typing import Dict, List, Tuple
-import platform
import customtkinter as ctk
from joblib import parallel_backend
@@ -17,7 +17,6 @@
from psm_utils.io import FILETYPES
import ms2rescore.gui.widgets as widgets
-import ms2rescore.package_data as pkg_data
import ms2rescore.package_data.img as pkg_data_img
from ms2rescore import __version__ as ms2rescore_version
from ms2rescore.config_parser import parse_configurations
@@ -28,9 +27,6 @@
with importlib.resources.path(pkg_data_img, "config_icon.png") as resource:
_IMG_DIR = Path(resource).parent
-with importlib.resources.path(pkg_data, "ms2rescore-gui-theme.json") as resource:
- _THEME_FILE = Path(resource).as_posix()
-
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
@@ -41,11 +37,54 @@
except ImportError:
pass
-ctk.set_default_color_theme(_THEME_FILE)
-
# TODO Does this disable multiprocessing everywhere?
parallel_backend("threading")
+CONFIG_WIDTH = 600
+CITATIONS = [
+ (
+ "MS²Rescore: Declercq et al. JPR (2024)",
+ "https://doi.org/10.1021/acs.jproteome.3c00785",
+ ),
+ (
+ "MS²PIP: Declercq et al. NAR (2023)",
+ "https://doi.org/10.1093/nar/gkad335",
+ ),
+ (
+ "DeepLC: Bouwmeester et al. Nat Methods (2021)",
+ "https://doi.org/10.1038/s41592-021-01301-5",
+ ),
+ (
+ "ionmob: Teschner et al. Bioinformatics (2023)",
+ "https://doi.org/10.1093/bioinformatics/btad486",
+ ),
+ (
+ "Mokapot: Fondrie et al. JPR (2021)",
+ "https://doi.org/10.1021/acs.jproteome.0c01010",
+ ),
+ (
+ "Percolator: Käll et al. Nat Methods (2007)",
+ "https://doi.org/10.1038/nmeth1113",
+ ),
+]
+LINKS = [
+ (
+ "User guide",
+ "https://ms2rescore.readthedocs.io/en/stable/userguide/configuration/",
+ "docs",
+ ),
+ (
+ "Discussion forum",
+ "https://github.com/compomics/ms2rescore/discussions/categories/q-a",
+ "comments",
+ ),
+ (
+ "CompOmics/ms2rescore",
+ "https://github.com/compomics/ms2rescore",
+ "github",
+ ),
+]
+
class SideBar(ctk.CTkFrame):
def __init__(self, *args, **kwargs):
@@ -54,7 +93,7 @@ def __init__(self, *args, **kwargs):
# Configure layout (three rows, one column)
self.grid_rowconfigure(0, weight=1)
- # self.rowconfigure((1,2,3,4,5), weight=1)
+ row_count = 0
# Top row: logo
self.logo = ctk.CTkImage(
@@ -62,64 +101,65 @@ def __init__(self, *args, **kwargs):
size=(130, 130),
)
self.logo_label = ctk.CTkLabel(self, text="", image=self.logo)
- self.logo_label.grid(row=0, column=0, padx=0, pady=(20, 50), sticky="n")
+ self.logo_label.grid(row=row_count, column=0, padx=0, pady=(20, 50), sticky="n")
+ row_count += 1
+
+ # Links
+ self.links = LinkFrame(self, LINKS)
+ self.links.configure(fg_color="transparent")
+ self.links.grid(row=row_count, column=0, padx=20, pady=(0, 10), sticky="nsew")
+ row_count += 1
# Citations
- self.citations = CitationFrame(
- self,
- [
- (
- "MS²Rescore: Declercq et al. 2022 MCP",
- "https://doi.org/10.1016/j.mcpro.2022.100266",
- ),
- ("MS²PIP: Declercq et al. 2023 NAR", "https://doi.org/10.1093/nar/gkad335"),
- (
- "DeepLC: Bouwmeester et al. 2021 Nat Methods",
- "https://doi.org/10.1038/s41592-021-01301-5",
- ),
- (
- "Mokapot: Fondrie et al. 2021 JPR",
- "https://doi.org/10.1021/acs.jproteome.0c01010",
- ),
- ("Percolator: Käll et al. 2007 Nat Methods", "https://doi.org/10.1038/nmeth1113"),
- ],
- )
+ self.citations = CitationFrame(self, CITATIONS)
self.citations.configure(fg_color="transparent")
- self.citations.grid(row=1, column=0, padx=20, pady=10, sticky="nsew")
+ self.citations.grid(row=row_count, column=0, padx=20, pady=(0, 10), sticky="nsew")
+ row_count += 1
# Bottom row: Appearance and UI scaling
self.ui_control = widgets.UIControl(self)
self.ui_control.configure(fg_color="transparent")
- self.ui_control.grid(row=2, column=0, padx=20, pady=(0, 10), sticky="ew")
+ self.ui_control.grid(row=row_count, column=0, padx=20, pady=(0, 10), sticky="nsew")
+ row_count += 1
- # Bottom row: GH URL
- self.github_button = ctk.CTkButton(
- self,
- text="compomics/ms2rescore",
- anchor="w",
- fg_color="transparent",
- text_color=("#000000", "#fefdff"),
- image=ctk.CTkImage(
- dark_image=Image.open(os.path.join(str(_IMG_DIR), "github-mark-white.png")),
- light_image=Image.open(os.path.join(str(_IMG_DIR), "github-mark.png")),
- size=(25, 25),
- ),
- )
- self.github_button.bind(
- "",
- lambda e: self.web_callback("https://github.com/compomics/ms2rescore"),
+ # Bottom row: version
+ self.version_label = ctk.CTkLabel(self, text=f"v{ms2rescore_version}")
+ self.version_label.grid(row=row_count, column=0, padx=20, pady=(10, 10))
+ row_count += 1
+
+
+class LinkFrame(ctk.CTkFrame):
+ def __init__(self, master, links: List[Tuple[str, str, str]], *args, **kwargs):
+ super().__init__(master, *args, **kwargs)
+ self.heading = ctk.CTkLabel(
+ self, text="Useful links", font=ctk.CTkFont(weight="bold"), anchor="w"
)
- self.github_button.grid(row=4, column=0, padx=20, pady=(10, 10))
+ self.heading.grid(row=0, column=0, padx=0, pady=0, sticky="ew")
- # Bottom row: version
- self.version_label = ctk.CTkLabel(self, text=ms2rescore_version)
- self.version_label.grid(row=5, column=0, padx=20, pady=(10, 10))
+ for i, (ref, url, icon) in enumerate(links):
+ button = ctk.CTkButton(
+ self,
+ text=ref,
+ text_color=("#000000", "#fefdff"),
+ hover_color=("#3a7ebf", "#1f538d"),
+ fg_color="transparent",
+ anchor="w",
+ image=ctk.CTkImage(
+ dark_image=Image.open(os.path.join(str(_IMG_DIR), f"{icon}_icon_white.png")),
+ light_image=Image.open(os.path.join(str(_IMG_DIR), f"{icon}_icon_black.png")),
+ size=(20, 20),
+ ),
+ command=lambda x=url: webbrowser.open_new(x),
+ )
+ button.grid(row=i + 1, column=0, padx=0, pady=(0, 5), sticky="ew")
class CitationFrame(ctk.CTkFrame):
def __init__(self, master, citations: List[Tuple[str]], *args, **kwargs):
super().__init__(master, *args, **kwargs)
- self.heading = ctk.CTkLabel(self, text="Please cite", anchor="w")
+ self.heading = ctk.CTkLabel(
+ self, text="Please cite", font=ctk.CTkFont(weight="bold"), anchor="w"
+ )
self.heading.grid(row=0, column=0, padx=0, pady=0, sticky="ew")
self.buttons = []
@@ -143,6 +183,8 @@ def __init__(self, *args, **kwargs):
"""MS²Rescore configuration frame."""
super().__init__(*args, **kwargs)
+ self.configure(width=CONFIG_WIDTH)
+
for tab in ["Main", "Advanced", "Feature generators", "Rescoring engine"]:
self.add(tab)
self.tab(tab).grid_columnconfigure(0, weight=1)
@@ -150,16 +192,16 @@ def __init__(self, *args, **kwargs):
self.set("Main")
self.main_config = MainConfiguration(self.tab("Main"))
- self.main_config.grid(row=0, column=0, sticky="nsew")
+ self.main_config.grid(row=0, column=0, padx=5, sticky="nsew")
self.advanced_config = AdvancedConfiguration(self.tab("Advanced"))
- self.advanced_config.grid(row=0, column=0, sticky="nsew")
+ self.advanced_config.grid(row=0, column=0, padx=5, sticky="nsew")
self.fgen_config = FeatureGeneratorConfig(self.tab("Feature generators"))
- self.fgen_config.grid(row=0, column=0, sticky="nsew")
+ self.fgen_config.grid(row=0, column=0, padx=5, sticky="nsew")
self.rescoring_engine_config = RescoringEngineConfig(self.tab("Rescoring engine"))
- self.rescoring_engine_config.grid(row=0, column=0, sticky="nsew")
+ self.rescoring_engine_config.grid(row=0, column=0, padx=5, sticky="nsew")
def get(self):
"""Create MS²Rescore config file"""
@@ -184,52 +226,95 @@ def __init__(self, *args, **kwargs):
self.configure(fg_color="transparent")
self.grid_columnconfigure(0, weight=1)
+ row_n = 0
- self.psm_file_config = PSMFileConfigFrame(self)
- self.psm_file_config.grid(row=0, column=0, pady=(0, 10), sticky="nsew")
-
- self.spectrum_path = widgets.LabeledFileSelect(
- self, label="Select MGF/mzML file or directory", file_option="file/dir"
+ self.psm_file = widgets.LabeledFileSelect(
+ self,
+ label="Identification file",
+ description=(
+ "Select the PSM file generated by the search engine. This file should contain "
+ "all unfiltered target and decoy identifications. Multiple files can be selected."
+ ),
+ wraplength=CONFIG_WIDTH - 20,
+ file_option="openfiles",
)
- self.spectrum_path.grid(row=1, column=0, pady=(0, 10), sticky="nsew")
+ self.psm_file.grid(row=row_n, column=0, pady=(0, 10), sticky="nsew")
+ row_n += 1
- self.fasta_file = widgets.LabeledFileSelect(
+ self.psm_file_type = widgets.LabeledOptionMenu(
self,
- label="Select FASTA file (optional, required for protein inference)",
- file_option="openfile",
+ vertical=False,
+ label="Identification file type",
+ description=(
+ "Select the file type of the PSM file. The 'infer' option will attempt to infer "
+ "the file type from the file extension. If your file type is not listed, do not "
+ "hesitate to open a feature request on the discussion forum."
+ ),
+ wraplength=CONFIG_WIDTH - 150,
+ values=["infer"] + list(FILETYPES.keys()),
)
- self.fasta_file.grid(row=2, column=0, pady=(0, 10), sticky="nsew")
+ self.psm_file_type.grid(row=row_n, column=0, pady=(0, 10), sticky="nsew")
+ row_n += 1
- self.processes = widgets.LabeledOptionMenu(
+ self.spectrum_path = widgets.LabeledFileSelect(
self,
- label="Number of processes",
- values=[str(x) for x in list(range(-1, multiprocessing.cpu_count() + 1))],
+ label="Spectrum file or directory",
+ description=(
+ "Select the MGF, mzML, or Bruker raw file(s) containing the spectra. If the "
+ "search engine wrote the file names to the PSM file, select the directory "
+ "containing the files. The file path should not contain spaces. "
+ ),
+ wraplength=CONFIG_WIDTH - 20,
+ file_option="file/dir",
)
- self.processes.grid(row=3, column=0, pady=(0, 10), sticky="nsew")
+ self.spectrum_path.grid(row=row_n, column=0, pady=(0, 10), sticky="nsew")
+ row_n += 1
self.modification_mapping = widgets.TableInput(
self,
label="Modification mapping",
+ description=(
+ "Map search engine modification labels to ProForma labels (PSI-MOD, UniMod, "
+ "formula, or mass shift). This is required for correct modification parsing. "
+ "If this field is left empty, the search engine labels will be used as is, "
+ "which may lead to incorrect feature generation for modified peptides. "
+ "Check out the user guide for more information."
+ ),
columns=2,
header_labels=["Search engine label", "ProForma label"],
+ wraplength=CONFIG_WIDTH - 20, # width of the frame minus padding; hardcoded for now
)
- self.modification_mapping.grid(row=4, column=0, sticky="new")
+ self.modification_mapping.grid(row=row_n, column=0, pady=(0, 10), sticky="new")
+ row_n += 1
self.fixed_modifications = widgets.TableInput(
self,
- label="Fixed modifications",
+ label="Fixed modifications (MaxQuant only)",
+ description=(
+ "Add fixed modifications that are not included in the PSM file by the search "
+ "engine. If the search engine writes fixed modifications to the PSM file (as most "
+ "do), leave this field empty. However, if you are using MaxQuant, which does not "
+ "write fixed modifications to the PSM file, you should add them here."
+ ),
columns=2,
header_labels=["ProForma label", "Amino acids (comma-separated)"],
+ wraplength=CONFIG_WIDTH - 20, # width of the frame minus padding; hardcoded for now
)
- self.fixed_modifications.grid(row=5, column=0, pady=(0, 10), sticky="nsew")
+ self.fixed_modifications.grid(row=row_n, column=0, pady=(0, 10), sticky="nsew")
+ row_n += 1
def get(self) -> Dict:
"""Get the configured values as a dictionary."""
+ try:
+ # there cannot be spaces in the file path
+ # TODO: Fix this in widgets.LabeledFileSelect
+ psm_files = self.psm_file.get().split(" ")
+ except AttributeError:
+ raise MS2RescoreConfigurationError("No PSM file provided. Please select a file.")
return {
- **self.psm_file_config.get(),
+ "psm_file": psm_files,
+ "psm_file_type": self.psm_file_type.get(),
"spectrum_path": self.spectrum_path.get(),
- "fasta_file": self.fasta_file.get(),
- "processes": int(self.processes.get()),
"modification_mapping": self._parse_modification_mapping(
self.modification_mapping.get()
),
@@ -256,41 +341,6 @@ def _parse_fixed_modifications(table_output):
return fixed_modifications or None
-class PSMFileConfigFrame(ctk.CTkFrame):
- def __init__(self, *args, **kwargs):
- """PSM file configuration frame with labeled file select and option menu."""
- super().__init__(*args, **kwargs)
-
- self.configure(fg_color="transparent")
- self.grid_columnconfigure(0, weight=1)
-
- self.psm_file = widgets.LabeledFileSelect(
- self, label="Select identification file", file_option="openfiles"
- )
- self.psm_file.grid(row=0, column=0, pady=0, padx=(0, 5), sticky="nsew")
-
- self.psm_file_type = widgets.LabeledOptionMenu(
- self,
- vertical=True,
- label="PSM file type",
- values=["infer"] + list(FILETYPES.keys()),
- )
- self.psm_file_type.grid(row=0, column=1, pady=(5, 0), sticky="nsew")
-
- def get(self) -> Dict:
- """Get the configured values as a dictionary."""
- try:
- # there cannot be spaces in the file path
- # TODO: Fix this in widgets.LabeledFileSelect
- psm_files = self.psm_file.get().split(" ")
- except AttributeError:
- raise MS2RescoreConfigurationError("No PSM file provided. Please select a file.")
- return {
- "psm_file": psm_files,
- "psm_file_type": self.psm_file_type.get(),
- }
-
-
class AdvancedConfiguration(ctk.CTkFrame):
def __init__(self, *args, **kwargs):
"""Advanced MS²Rescore configuration frame."""
@@ -299,47 +349,136 @@ def __init__(self, *args, **kwargs):
self.configure(fg_color="transparent")
self.grid_columnconfigure(0, weight=1)
- self.lower_score = widgets.LabeledSwitch(self, label="Lower score is better")
+ self.lower_score = widgets.LabeledSwitch(
+ self,
+ label="Lower score is better",
+ description=(
+ "When enabled, a lower search engine score is considered to denote a better PSM."
+ ),
+ wraplength=CONFIG_WIDTH - 180,
+ )
self.lower_score.grid(row=0, column=0, pady=(0, 10), sticky="nsew")
- self.usi = widgets.LabeledSwitch(self, label="Rename PSM IDs to their USI")
+ self.usi = widgets.LabeledSwitch(
+ self,
+ label="Rename spectrum IDs to USIs",
+ description=(
+ "Rename the spectrum identifiers to Universal Spectrum Identifiers "
+ "(USIs) for full provenance tracking."
+ ),
+ wraplength=CONFIG_WIDTH - 180,
+ )
self.usi.grid(row=1, column=0, pady=(0, 10), sticky="nsew")
+ self.write_flashlfq = widgets.LabeledSwitch(
+ self,
+ label="Write FlashLFQ input file",
+ description=(
+ "Write a file that can be used as input for FlashLFQ. This file only contains "
+ "target PSMs that pass the FDR threshold."
+ ),
+ wraplength=CONFIG_WIDTH - 180,
+ )
+ self.write_flashlfq.grid(row=2, column=0, pady=(0, 10), sticky="nsew")
+
self.generate_report = widgets.LabeledSwitch(
- self, label="Generate MS²Rescore report", default=True
+ self,
+ label="Generate interactive report",
+ description=(
+ "Generate an interactive report with quality control charts about the MS²Rescore "
+ "run. This report can be viewed in a web browser."
+ ),
+ wraplength=CONFIG_WIDTH - 180,
+ default=True,
)
- self.generate_report.grid(row=2, column=0, pady=(0, 10), sticky="nsew")
+ self.generate_report.grid(row=3, column=0, pady=(0, 10), sticky="nsew")
- self.id_decoy_pattern = widgets.LabeledEntry(self, label="Decoy protein regex pattern")
- self.id_decoy_pattern.grid(row=3, column=0, pady=(0, 10), sticky="nsew")
+ self.id_decoy_pattern = widgets.LabeledEntry(
+ self,
+ label="Decoy protein regex pattern",
+ description=(
+ "A regular expression pattern to identify decoy PSMs by the associated protein "
+ "names. Most PSM file types contain a dedicated field indicating decoy PSMs, in "
+ "which case this field can be left empty."
+ ),
+ wraplength=CONFIG_WIDTH - 180,
+ )
+ self.id_decoy_pattern.grid(row=4, column=0, pady=(0, 10), sticky="nsew")
+
+ self.psm_id_pattern = widgets.LabeledEntry(
+ self,
+ label="PSM ID regex pattern",
+ description=(
+ "A regular expression pattern to extract the spectrum ID from the IDs in the PSM "
+ "file. In most cases, this field can be left empty. Check the user guide for more "
+ "information."
+ ),
+ wraplength=CONFIG_WIDTH - 180,
+ )
+ self.psm_id_pattern.grid(row=5, column=0, pady=(0, 10), sticky="nsew")
- self.psm_id_pattern = widgets.LabeledEntry(self, label="PSM ID regex pattern")
- self.psm_id_pattern.grid(row=4, column=0, pady=(0, 10), sticky="nsew")
+ self.spectrum_id_pattern = widgets.LabeledEntry(
+ self,
+ label="Spectrum ID regex pattern",
+ description=(
+ "Similar to the PSM ID regex pattern, but for the IDs in the spectrum file."
+ ),
+ wraplength=CONFIG_WIDTH - 180,
+ )
+ self.spectrum_id_pattern.grid(row=6, column=0, pady=(0, 10), sticky="nsew")
- self.spectrum_id_pattern = widgets.LabeledEntry(self, label="Spectrum ID regex pattern")
- self.spectrum_id_pattern.grid(row=5, column=0, pady=(0, 10), sticky="nsew")
+ self.processes = widgets.LabeledOptionMenu(
+ self,
+ label="Number of parallel processes",
+ description=(
+ "Choose higher values for faster processing, and lower values for less memory "
+ "usage."
+ ),
+ wraplength=CONFIG_WIDTH - 180,
+ # Limit to 16 processes to avoid memory overhead
+ values=[str(x) for x in list(range(1, min(16, multiprocessing.cpu_count()) + 1))],
+ default_value=str(min(16, multiprocessing.cpu_count())),
+ )
+ self.processes.grid(row=7, column=0, pady=(0, 10), sticky="nsew")
self.file_prefix = widgets.LabeledFileSelect(
- self, label="Filename for output files", file_option="savefile"
+ self,
+ label="Filename for output files",
+ file_option="savefile",
+ description=(
+ "Select the output file prefix. The output files will be saved in the selected "
+ "directory with the selected filename plus a suffix denoting the file type. If "
+ "left empty, this will be based on the PSM file."
+ ),
+ wraplength=CONFIG_WIDTH - 20,
)
- self.file_prefix.grid(row=7, column=0, columnspan=2, sticky="nsew")
+ self.file_prefix.grid(row=8, column=0, columnspan=2, sticky="nsew")
self.config_file = widgets.LabeledFileSelect(
- self, label="Configuration file", file_option="openfile"
+ self,
+ label="Configuration file",
+ file_option="openfile",
+ description=(
+ "Select a configuration file. Any options that are left empty in the application "
+ "will be filled in with values from this file."
+ ),
+ wraplength=CONFIG_WIDTH - 20,
)
- self.config_file.grid(row=8, column=0, columnspan=2, sticky="nsew")
+ self.config_file.grid(row=9, column=0, columnspan=2, sticky="nsew")
def get(self) -> Dict:
"""Get the configured values as a dictionary."""
return {
"lower_score_is_better": bool(int(self.lower_score.get())), # str repr of 0 or 1
"rename_to_usi": self.usi.get(),
+ "write_flashlfq": self.write_flashlfq.get(),
+ "write_report": self.generate_report.get(),
"id_decoy_pattern": self.id_decoy_pattern.get(),
"psm_id_pattern": self.psm_id_pattern.get(),
"spectrum_id_pattern": self.spectrum_id_pattern.get(),
+ "processes": int(self.processes.get()),
"output_path": self.file_prefix.get(),
"config_file": self.config_file.get(),
- "write_report": self.generate_report.get(),
}
@@ -397,7 +536,7 @@ def __init__(self, *args, **kwargs):
self.configure(fg_color="transparent")
self.grid_columnconfigure(0, weight=1)
- self.title = widgets.Heading(self, text="Basic features")
+ self.title = widgets._Heading(self, text="Basic features")
self.title.grid(row=0, column=0, columnspan=2, pady=(0, 5), sticky="ew")
self.enabled = widgets.LabeledSwitch(self, label="Enable Basic features", default=True)
@@ -418,7 +557,7 @@ def __init__(self, *args, **kwargs):
self.configure(fg_color="transparent")
self.grid_columnconfigure(0, weight=1)
- self.title = widgets.Heading(self, text="MS²PIP")
+ self.title = widgets._Heading(self, text="MS²PIP (spectrum intensity prediction)")
self.title.grid(row=0, column=0, columnspan=2, pady=(0, 5), sticky="ew")
self.enabled = widgets.LabeledSwitch(self, label="Enable MS²PIP", default=True)
@@ -455,7 +594,7 @@ def __init__(self, *args, **kwargs):
self.configure(fg_color="transparent")
self.grid_columnconfigure(0, weight=1)
- self.title = widgets.Heading(self, text="DeepLC")
+ self.title = widgets._Heading(self, text="DeepLC (retention time prediction)")
self.title.grid(row=0, column=0, columnspan=2, pady=(0, 5), sticky="ew")
self.enabled = widgets.LabeledSwitch(self, label="Enable DeepLC", default=True)
@@ -510,7 +649,7 @@ def __init__(self, *args, **kwargs):
self.configure(fg_color="transparent")
self.grid_columnconfigure(0, weight=1)
- self.title = widgets.Heading(self, text="Ionmob")
+ self.title = widgets._Heading(self, text="Ionmob (ion mobility prediction)")
self.title.grid(row=0, column=0, columnspan=2, pady=(0, 5), sticky="ew")
self.enabled = widgets.LabeledSwitch(self, label="Enable Ionmob", default=False)
@@ -539,7 +678,7 @@ def __init__(self, *args, **kwargs):
self.configure(fg_color="transparent")
self.grid_columnconfigure(0, weight=1)
- self.title = widgets.Heading(self, text="im2deep")
+ self.title = widgets._Heading(self, text="IM2Deep (ion mobility prediction)")
self.title.grid(row=0, column=0, columnspan=2, pady=(0, 5), sticky="ew")
self.enabled = widgets.LabeledSwitch(self, label="Enable im2deep", default=False)
@@ -579,7 +718,7 @@ def get(self) -> Dict:
if self.radio_button.get().lower() == "mokapot":
return {self.radio_button.get().lower(): self.mokapot_config.get()}
elif self.radio_button.get().lower() == "percolator":
- return {self.radio_button.get().lower(): self.mokapot_config.get()}
+ return {self.radio_button.get().lower(): self.percolator_config.get()}
class MokapotRescoringConfiguration(ctk.CTkFrame):
@@ -589,22 +728,29 @@ def __init__(self, *args, **kwargs):
self.configure(fg_color="transparent")
self.grid_columnconfigure(0, weight=1)
+ row_n = 0
- self.title = widgets.Heading(self, text="Mokapot coffeeguration")
- self.title.grid(row=0, column=0, columnspan=2, pady=(0, 5), sticky="ew")
+ self.title = widgets._Heading(self, text="Mokapot coffeeguration")
+ self.title.grid(row=row_n, column=0, columnspan=2, pady=(0, 5), sticky="ew")
+ row_n += 1
self.write_weights = widgets.LabeledSwitch(
self, label="Write model weights to file", default=True
)
- self.write_weights.grid(row=1, column=0, pady=(0, 10), sticky="nsew")
+ self.write_weights.grid(row=row_n, column=0, pady=(0, 10), sticky="nsew")
+ row_n += 1
self.write_txt = widgets.LabeledSwitch(self, label="Write TXT output files", default=True)
- self.write_txt.grid(row=2, column=0, pady=(0, 10), sticky="nsew")
+ self.write_txt.grid(row=row_n, column=0, pady=(0, 10), sticky="nsew")
+ row_n += 1
- self.write_flashlfq = widgets.LabeledSwitch(
- self, label="Write file for FlashLFQ", default=False
+ self.fasta_file = widgets.LabeledFileSelect(
+ self,
+ label="Select FASTA file (optional, required for protein inference)",
+ file_option="openfile",
)
- self.write_flashlfq.grid(row=3, column=0, pady=(0, 10), sticky="nsew")
+ self.fasta_file.grid(row=row_n, column=0, pady=(0, 10), sticky="nsew")
+ row_n += 1
self.protein_kwargs = widgets.TableInput(
self,
@@ -612,14 +758,15 @@ def __init__(self, *args, **kwargs):
columns=2,
header_labels=["Parameter", "Value"],
)
- self.protein_kwargs.grid(row=4, column=0, sticky="nsew")
+ self.protein_kwargs.grid(row=row_n, column=0, sticky="nsew")
+ row_n += 1
def get(self) -> Dict:
"""Return the configuration as a dictionary."""
config = {
"write_weights": self.write_weights.get(),
"write_txt": self.write_txt.get(),
- "write_flashlfq": self.write_flashlfq.get(),
+ "fasta_file": self.fasta_file.get(),
"protein_kwargs": self._parse_protein_kwargs(self.protein_kwargs.get()),
}
return config
@@ -642,7 +789,7 @@ def __init__(self, *args, **kwargs):
self.configure(fg_color="transparent")
self.grid_columnconfigure(0, weight=1)
- self.title = widgets.Heading(self, text="Percolator coffeeguration")
+ self.title = widgets._Heading(self, text="Percolator coffeeguration")
self.title.grid(row=0, column=0, columnspan=2, pady=(0, 5), sticky="ew")
self.weights_file = widgets.LabeledFileSelect(
@@ -665,6 +812,8 @@ def function(config):
config_list = [config]
config = parse_configurations(config_list)
rescore(configuration=config)
+ if config["ms2rescore"]["write_report"]:
+ webbrowser.open_new_tab(config["ms2rescore"]["output_path"] + ".report.html")
def app():
@@ -677,6 +826,7 @@ def app():
root.protocol("WM_DELETE_WINDOW", sys.exit)
dpi = root.winfo_fpixels("1i")
root.geometry(f"{int(15*dpi)}x{int(10*dpi)}")
+ root.minsize(int(13 * dpi), int(9 * dpi))
root.title("MS²Rescore")
if platform.system() != "Linux":
root.wm_iconbitmap(os.path.join(str(_IMG_DIR), "program_icon.ico"))
diff --git a/ms2rescore/gui/function2ctk.py b/ms2rescore/gui/function2ctk.py
index 60bad120..b9c565de 100644
--- a/ms2rescore/gui/function2ctk.py
+++ b/ms2rescore/gui/function2ctk.py
@@ -52,7 +52,7 @@ def __init__(
# 2x3 grid, only logging column expands with window
self.grid_columnconfigure(0, weight=0) # Left: Sidebar
- self.grid_columnconfigure(1, weight=2) # Middle: Configuration
+ self.grid_columnconfigure(1, weight=0) # Middle: Configuration
self.grid_columnconfigure(2, weight=1) # Right: Logging
self.grid_rowconfigure(0, weight=1)
diff --git a/ms2rescore/gui/widgets.py b/ms2rescore/gui/widgets.py
index ca3d03f1..382a6ea8 100644
--- a/ms2rescore/gui/widgets.py
+++ b/ms2rescore/gui/widgets.py
@@ -7,33 +7,61 @@
import customtkinter as ctk
-class Heading(ctk.CTkLabel):
+class _Heading(ctk.CTkLabel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.configure(
+ font=ctk.CTkFont(weight="bold"),
fg_color=("gray80", "gray30"),
text_color=("black", "white"),
corner_radius=6,
)
-class LabeledEntry(ctk.CTkFrame):
+class _LabeledWidget(ctk.CTkFrame):
+ def __init__(self, *args, label="Label", description=None, wraplength=0, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.grid_columnconfigure(0, weight=1)
+
+ self.row_n = 0
+
+ self._label_frame = ctk.CTkFrame(self)
+ self._label_frame.grid_columnconfigure(0, weight=1)
+ self._label_frame.configure(fg_color="transparent")
+ self._label_frame.grid(row=self.row_n, column=0, padx=0, pady=(0, 5), sticky="nsew")
+ self.row_n += 1
+
+ self._label = ctk.CTkLabel(
+ self._label_frame,
+ text=label,
+ font=ctk.CTkFont(weight="bold"),
+ wraplength=wraplength,
+ justify="left",
+ anchor="w",
+ )
+ self._label.grid(row=0, column=0, padx=0, pady=0, sticky="w")
+
+ if description:
+ self._description = ctk.CTkLabel(
+ self._label_frame,
+ text=description,
+ wraplength=wraplength,
+ justify="left",
+ anchor="w",
+ )
+ self._description.grid(row=1, column=0, padx=0, pady=0, sticky="ew")
+
+
+class LabeledEntry(_LabeledWidget):
def __init__(
self,
*args,
- label="Enter text",
placeholder_text="Enter text...",
default_value="",
**kwargs,
):
super().__init__(*args, **kwargs)
- self.grid_columnconfigure(0, weight=1)
-
self._variable = ctk.StringVar(value=default_value)
-
- self._label = ctk.CTkLabel(self, text=label)
- self._label.grid(row=0, column=0, padx=0, pady=5, sticky="w")
-
self._entry = ctk.CTkEntry(
self, placeholder_text=placeholder_text, textvariable=self._variable
)
@@ -43,11 +71,10 @@ def get(self):
return self._variable.get()
-class LabeledEntryTextbox(ctk.CTkFrame):
+class LabeledEntryTextbox(_LabeledWidget):
def __init__(
self,
*args,
- label="Enter text",
initial_contents="Enter text here...",
box_height=100,
**kwargs,
@@ -56,49 +83,40 @@ def __init__(
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(1, weight=1)
- self._label = ctk.CTkLabel(self, text=label, anchor="w")
- self._label.grid(row=0, column=0, padx=0, pady=5, sticky="ew")
-
self.text_box = ctk.CTkTextbox(self, height=box_height)
self.text_box.insert("1.0", initial_contents)
- self.text_box.grid(row=1, column=0, padx=0, pady=5, sticky="nsew")
+ self.text_box.grid(row=self.row_n, column=0, padx=0, pady=5, sticky="nsew")
+ self.row_n += 1
def get(self):
return self.text_box.get("0.0", tk.END)
-class LabeledRadioButtons(ctk.CTkFrame):
- def __init__(self, *args, label="Select option", options=[], default_value=None, **kwargs):
+class LabeledRadioButtons(_LabeledWidget):
+ def __init__(
+ self,
+ *args,
+ options=[],
+ default_value=None,
+ **kwargs,
+ ):
super().__init__(*args, **kwargs)
- self.grid_columnconfigure(0, weight=1)
-
self.value = ctk.StringVar(value=default_value or options[0])
-
- self._label = ctk.CTkLabel(self, text=label)
- self._label.grid(row=0, column=0, padx=0, pady=(0, 5), sticky="w")
-
self._radio_buttons = []
for i, option in enumerate(options):
radio_button = ctk.CTkRadioButton(self, text=option, variable=self.value, value=option)
- radio_button.grid(row=i + 1, column=0, padx=0, pady=(0, 5), sticky="w")
+ radio_button.grid(row=self.row_n, column=0, padx=0, pady=(0, 5), sticky="w")
self._radio_buttons.append(radio_button)
+ self.row_n += 1
def get(self):
return self.value.get()
-class LabeledOptionMenu(ctk.CTkFrame):
- def __init__(
- self, *args, vertical=False, label="Select option", values=[], default_value=None, **kwargs
- ):
+class LabeledOptionMenu(_LabeledWidget):
+ def __init__(self, *args, vertical=False, values=[], default_value=None, **kwargs):
super().__init__(*args, **kwargs)
- self.grid_columnconfigure(0, weight=1)
-
self.value = ctk.StringVar(value=default_value or values[0])
-
- self._label = ctk.CTkLabel(self, text=label)
- self._label.grid(row=0, column=0, padx=0, pady=0 if vertical else 5, sticky="w")
-
self._option_menu = ctk.CTkOptionMenu(self, variable=self.value, values=values)
self._option_menu.grid(
row=1 if vertical else 0,
@@ -112,16 +130,10 @@ def get(self):
return self.value.get()
-class LabeledSwitch(ctk.CTkFrame):
- def __init__(self, *args, label="Enable/disable", default=False, **kwargs):
+class LabeledSwitch(_LabeledWidget):
+ def __init__(self, *args, default=False, **kwargs):
super().__init__(*args, **kwargs)
- self.grid_columnconfigure(0, weight=1)
-
self.value = ctk.StringVar(value="0")
-
- self._label = ctk.CTkLabel(self, text=label)
- self._label.grid(row=0, column=0, padx=0, pady=5, sticky="w")
-
self._switch = ctk.CTkSwitch(self, variable=self.value, text="", onvalue="1", offvalue="0")
self._switch.grid(row=0, column=1, padx=0, pady=5, sticky="e")
if default:
@@ -202,11 +214,10 @@ def set(self, value: float):
self.entry.insert(0, format(value, self.str_format))
-class LabeledFloatSpinbox(ctk.CTkFrame):
+class LabeledFloatSpinbox(_LabeledWidget):
def __init__(
self,
*args,
- label="Enter value",
initial_value=0.0,
step_size=1,
**kwargs,
@@ -214,9 +225,6 @@ def __init__(
super().__init__(*args, **kwargs)
self.grid_columnconfigure(0, weight=1)
- self._label = ctk.CTkLabel(self, text=label)
- self._label.grid(row=0, column=0, padx=0, pady=5, sticky="w")
-
self._spinbox = FloatSpinbox(
self,
initial_value=initial_value,
@@ -228,15 +236,20 @@ def get(self):
return self._spinbox.get()
-class LabeledFileSelect(ctk.CTkFrame):
- def __init__(self, *args, label="Select file", file_option="openfile", **kwargs):
+class LabeledFileSelect(_LabeledWidget):
+ def __init__(
+ self,
+ *args,
+ file_option="openfile",
+ **kwargs,
+ ):
"""
Advanced file selection widget with entry and file, directory, or save button.
Parameters
----------
- label : str
- Label text.
+ wraplength : int
+ Maximum line length before wrapping.
file_option : str
One of "openfile", "directory", "file/dir", or "savefile. Determines the type of file
selection dialog that is shown when the button is pressed.
@@ -253,14 +266,12 @@ def __init__(self, *args, label="Select file", file_option="openfile", **kwargs)
self._button_1 = None
self._button_2 = None
- self.grid_columnconfigure(0, weight=1)
-
- # Subwidgets
- self._label = ctk.CTkLabel(self, text=label)
- self._label.grid(row=0, column=0, columnspan=2, padx=0, pady=(5, 0), sticky="w")
+ self._label_frame.grid(
+ row=self.row_n - 1, column=0, columnspan=3, padx=0, pady=(0, 5), sticky="nsew"
+ ) # Override grid placement of _LabeledWidget label frame to span all columns
self._entry = ctk.CTkEntry(self, placeholder_text="Select a file or directory")
- self._entry.grid(row=1, column=0, padx=0, pady=0, sticky="ew")
+ self._entry.grid(row=self.row_n, column=0, padx=0, pady=5, sticky="ew")
if file_option == "directory":
self._button_1 = ctk.CTkButton(self, text="Browse directories", command=self._pick_dir)
@@ -279,9 +290,10 @@ def __init__(self, *args, label="Select file", file_option="openfile", **kwargs)
self, text="Path to save file(s)", command=self._save_file
)
- self._button_1.grid(row=1, column=1, padx=(5, 0), pady=0, sticky="e")
+ self._button_1.grid(row=self.row_n, column=1, padx=(5, 0), pady=5, sticky="e")
if self._button_2:
- self._button_2.grid(row=1, column=2, padx=(5, 0), pady=0, sticky="e")
+ self._button_2.grid(row=self.row_n, column=2, padx=(5, 0), pady=5, sticky="e")
+ self.row_n += 1
def get(self):
"""Returns the selected file or directory."""
@@ -312,15 +324,19 @@ def _save_file(self):
self._update_entry()
-class TableInput(ctk.CTkFrame):
- def __init__(self, *args, label=None, columns=2, header_labels=["A", "B"], **kwargs):
+class TableInput(_LabeledWidget):
+ def __init__(
+ self,
+ *args,
+ columns=2,
+ header_labels=["A", "B"],
+ **kwargs,
+ ):
"""
Table input widget with user-configurable number of rows.
Parameters
----------
- label : str
- Label text.
columns : int
Number of columns in the table.
header_labels : list of str
@@ -328,25 +344,16 @@ def __init__(self, *args, label=None, columns=2, header_labels=["A", "B"], **kwa
"""
super().__init__(*args, **kwargs)
- self.label = label
self.columns = columns
self.header_labels = header_labels
self.uniform_hash = str(random.getrandbits(128))
- self.grid_columnconfigure(0, weight=1)
-
- # Label
- if label:
- self.label = ctk.CTkLabel(self, text=label)
- self.label.grid(row=0, column=0, pady=(5, 0), sticky="w")
- label_row = 1
- else:
- label_row = 0
-
# Header row
header_row = ctk.CTkFrame(self)
- header_row.grid(row=0 + label_row, column=0, padx=(33, 0), pady=(0, 5), sticky="ew")
+ header_row.grid(row=self.row_n, column=0, padx=(33, 0), pady=(5, 5), sticky="ew")
+ self.row_n += 1
+
for i, header in enumerate(self.header_labels):
header_row.grid_columnconfigure(i, weight=1, uniform=self.uniform_hash)
padx = (0, 5) if i < len(self.header_labels) - 1 else (0, 0)
@@ -364,7 +371,8 @@ def __init__(self, *args, label=None, columns=2, header_labels=["A", "B"], **kwa
self.input_frame = ctk.CTkFrame(self)
self.input_frame.uniform_hash = self.uniform_hash
self.input_frame.grid_columnconfigure(0, weight=1)
- self.input_frame.grid(row=1 + label_row, column=0, sticky="new")
+ self.input_frame.grid(row=self.row_n, column=0, sticky="new")
+ self.row_n += 1
# Add first row that cannot be removed
self.add_row()
@@ -372,7 +380,8 @@ def __init__(self, *args, label=None, columns=2, header_labels=["A", "B"], **kwa
# Button to add more rows
self.add_button = ctk.CTkButton(self, text="+", width=28, command=self.add_row)
- self.add_button.grid(row=2 + label_row, column=0, pady=(0, 5), sticky="w")
+ self.add_button.grid(row=self.row_n, column=0, pady=(0, 5), sticky="w")
+ self.row_n += 1
def add_row(self):
row = _TableInputRow(self.input_frame, columns=self.columns)
diff --git a/ms2rescore/package_data/config_default.json b/ms2rescore/package_data/config_default.json
index 126fde8b..29042e91 100644
--- a/ms2rescore/package_data/config_default.json
+++ b/ms2rescore/package_data/config_default.json
@@ -17,8 +17,7 @@
"mokapot": {
"train_fdr": 0.01,
"write_weights": true,
- "write_txt": true,
- "write_flashlfq": true
+ "write_txt": true
}
},
"config_file": null,
@@ -41,6 +40,7 @@
"processes": -1,
"rename_to_usi": false,
"fasta_file": null,
+ "write_flashlfq": false,
"write_report": false
}
}
diff --git a/ms2rescore/package_data/config_default_tims.json b/ms2rescore/package_data/config_default_tims.json
index 2a77adf1..89913ccf 100644
--- a/ms2rescore/package_data/config_default_tims.json
+++ b/ms2rescore/package_data/config_default_tims.json
@@ -16,8 +16,7 @@
"rescoring_engine": {
"mokapot": {
"write_weights": true,
- "write_txt": true,
- "write_flashlfq": true
+ "write_txt": true
}
},
"psm_file": null
diff --git a/ms2rescore/package_data/config_schema.json b/ms2rescore/package_data/config_schema.json
index 40471714..3a2286ce 100644
--- a/ms2rescore/package_data/config_schema.json
+++ b/ms2rescore/package_data/config_schema.json
@@ -181,6 +181,11 @@
"description": "Path to FASTA file with protein sequences to use for protein inference",
"oneOf": [{ "type": "string" }, { "type": "null" }]
},
+ "write_flashlfq": {
+ "description": "Write results to a FlashLFQ-compatible file",
+ "type": "boolean",
+ "default": false
+ },
"write_report": {
"description": "Write an HTML report with various QC metrics and charts",
"type": "boolean",
@@ -337,11 +342,6 @@
"description": "Write Mokapot results to a text file",
"type": "boolean",
"default": false
- },
- "write_flashlfq": {
- "description": "Write Mokapot results to a FlashLFQ-compatible file",
- "type": "boolean",
- "default": false
}
}
},
diff --git a/ms2rescore/package_data/img/comments_icon_black.png b/ms2rescore/package_data/img/comments_icon_black.png
new file mode 100644
index 00000000..602b110a
Binary files /dev/null and b/ms2rescore/package_data/img/comments_icon_black.png differ
diff --git a/ms2rescore/package_data/img/comments_icon_white.png b/ms2rescore/package_data/img/comments_icon_white.png
new file mode 100644
index 00000000..12953450
Binary files /dev/null and b/ms2rescore/package_data/img/comments_icon_white.png differ
diff --git a/ms2rescore/package_data/img/docs_icon_black.png b/ms2rescore/package_data/img/docs_icon_black.png
new file mode 100644
index 00000000..4305c324
Binary files /dev/null and b/ms2rescore/package_data/img/docs_icon_black.png differ
diff --git a/ms2rescore/package_data/img/docs_icon_white.png b/ms2rescore/package_data/img/docs_icon_white.png
new file mode 100644
index 00000000..fb51127f
Binary files /dev/null and b/ms2rescore/package_data/img/docs_icon_white.png differ
diff --git a/ms2rescore/package_data/img/github-mark.png b/ms2rescore/package_data/img/github_icon_black.png
similarity index 100%
rename from ms2rescore/package_data/img/github-mark.png
rename to ms2rescore/package_data/img/github_icon_black.png
diff --git a/ms2rescore/package_data/img/github-mark-white.png b/ms2rescore/package_data/img/github_icon_white.png
similarity index 100%
rename from ms2rescore/package_data/img/github-mark-white.png
rename to ms2rescore/package_data/img/github_icon_white.png
diff --git a/ms2rescore/package_data/ms2rescore-gui-theme.json b/ms2rescore/package_data/ms2rescore-gui-theme.json
deleted file mode 100644
index 92c89cf1..00000000
--- a/ms2rescore/package_data/ms2rescore-gui-theme.json
+++ /dev/null
@@ -1,155 +0,0 @@
-{
- "CTk": {
- "fg_color": ["gray95", "gray10"]
- },
- "CTkToplevel": {
- "fg_color": ["gray95", "gray10"]
- },
- "CTkFrame": {
- "corner_radius": 6,
- "border_width": 0,
- "fg_color": ["gray90", "gray13"],
- "top_fg_color": ["gray85", "gray16"],
- "border_color": ["gray65", "gray28"]
- },
- "CTkButton": {
- "corner_radius": 6,
- "border_width": 0,
- "fg_color": ["#3a7ebf", "#1f538d"],
- "hover_color": ["#325882", "#14375e"],
- "border_color": ["#3E454A", "#949A9F"],
- "text_color": ["#DCE4EE", "#DCE4EE"],
- "text_color_disabled": ["gray74", "gray60"]
- },
- "CTkLabel": {
- "corner_radius": 0,
- "fg_color": "transparent",
- "text_color": ["gray14", "gray84"]
- },
- "CTkEntry": {
- "corner_radius": 6,
- "border_width": 2,
- "fg_color": ["#F9F9FA", "#343638"],
- "border_color": ["#979DA2", "#565B5E"],
- "text_color": ["gray14", "gray84"],
- "placeholder_text_color": ["gray52", "gray62"]
- },
- "CTkCheckbox": {
- "corner_radius": 6,
- "border_width": 3,
- "fg_color": ["#3a7ebf", "#1f538d"],
- "border_color": ["#3E454A", "#949A9F"],
- "hover_color": ["#325882", "#14375e"],
- "checkmark_color": ["#DCE4EE", "gray90"],
- "text_color": ["gray14", "gray84"],
- "text_color_disabled": ["gray60", "gray45"]
- },
- "CTkSwitch": {
- "corner_radius": 1000,
- "border_width": 3,
- "button_length": 0,
- "fg_color": ["#939BA2", "#4A4D50"],
- "progress_color": ["#3a7ebf", "#1f538d"],
- "button_color": ["gray36", "#D5D9DE"],
- "button_hover_color": ["gray20", "gray100"],
- "text_color": ["gray14", "gray84"],
- "text_color_disabled": ["gray60", "gray45"]
- },
- "CTkRadiobutton": {
- "corner_radius": 1000,
- "border_width_checked": 6,
- "border_width_unchecked": 3,
- "fg_color": ["#3a7ebf", "#1f538d"],
- "border_color": ["#3E454A", "#949A9F"],
- "hover_color": ["#325882", "#14375e"],
- "text_color": ["gray14", "gray84"],
- "text_color_disabled": ["gray60", "gray45"]
- },
- "CTkProgressBar": {
- "corner_radius": 1000,
- "border_width": 0,
- "fg_color": ["#939BA2", "#4A4D50"],
- "progress_color": ["#3a7ebf", "#1f538d"],
- "border_color": ["gray", "gray"]
- },
- "CTkSlider": {
- "corner_radius": 1000,
- "button_corner_radius": 1000,
- "border_width": 6,
- "button_length": 0,
- "fg_color": ["#939BA2", "#4A4D50"],
- "progress_color": ["gray40", "#AAB0B5"],
- "button_color": ["#3a7ebf", "#1f538d"],
- "button_hover_color": ["#325882", "#14375e"]
- },
- "CTkOptionMenu": {
- "corner_radius": 6,
- "fg_color": ["#3a7ebf", "#1f538d"],
- "button_color": ["#325882", "#14375e"],
- "button_hover_color": ["#234567", "#1e2c40"],
- "text_color": ["#DCE4EE", "#DCE4EE"],
- "text_color_disabled": ["gray74", "gray60"]
- },
- "CTkComboBox": {
- "corner_radius": 6,
- "border_width": 2,
- "fg_color": ["#F9F9FA", "#343638"],
- "border_color": ["#979DA2", "#565B5E"],
- "button_color": ["#979DA2", "#565B5E"],
- "button_hover_color": ["#6E7174", "#7A848D"],
- "text_color": ["gray14", "gray84"],
- "text_color_disabled": ["gray50", "gray45"]
- },
- "CTkScrollbar": {
- "corner_radius": 1000,
- "border_spacing": 4,
- "fg_color": "transparent",
- "button_color": ["gray55", "gray41"],
- "button_hover_color": ["gray40", "gray53"]
- },
- "CTkSegmentedButton": {
- "corner_radius": 6,
- "border_width": 2,
- "fg_color": ["#979DA2", "gray29"],
- "selected_color": ["#3a7ebf", "#1f538d"],
- "selected_hover_color": ["#325882", "#14375e"],
- "unselected_color": ["#979DA2", "gray29"],
- "unselected_hover_color": ["gray70", "gray41"],
- "text_color": ["#DCE4EE", "#DCE4EE"],
- "text_color_disabled": ["gray74", "gray60"]
- },
- "CTkTextbox": {
- "corner_radius": 6,
- "border_width": 0,
- "fg_color": ["gray100", "gray20"],
- "border_color": ["#979DA2", "#565B5E"],
- "text_color": ["gray14", "gray84"],
- "scrollbar_button_color": ["gray55", "gray41"],
- "scrollbar_button_hover_color": ["gray40", "gray53"]
- },
- "CTkScrollableFrame": {
- "label_fg_color": ["gray80", "gray21"]
- },
- "DropdownMenu": {
- "fg_color": ["gray90", "gray20"],
- "hover_color": ["gray75", "gray28"],
- "text_color": ["gray14", "gray84"]
- },
- "CTkFont": {
- "macOS": {
- "family": "SF Display",
- "size": 13,
- "weight": "normal"
- },
- "Windows": {
- "family": "Roboto",
- "size": 13,
- "weight": "normal"
- },
- "Linux": {
- "family": "Roboto",
- "size": 13,
- "weight": "normal"
- }
- }
-}
diff --git a/ms2rescore/report/generate.py b/ms2rescore/report/generate.py
index 1f399219..0b976c48 100644
--- a/ms2rescore/report/generate.py
+++ b/ms2rescore/report/generate.py
@@ -56,8 +56,8 @@ def generate_report(
Parameters
----------
output_path_prefix
- Prefix of the MS²Rescore output file names. For example, if the PSM file is
- ``/path/to/file.psms.tsv``, the prefix is ``/path/to/file.ms2rescore``.
+ Prefix of the MS²Rescore output file names. For example, if the output PSM file is
+ ``/path/to/file.ms2rescore.psms.tsv``, the prefix is ``/path/to/file.ms2rescore``.
psm_list
PSMs to be used for the report. If not provided, the PSMs will be read from the
PSM file that matches the ``output_path_prefix``.
diff --git a/ms2rescore/report/utils.py b/ms2rescore/report/utils.py
index 069a641f..d5fbfb86 100644
--- a/ms2rescore/report/utils.py
+++ b/ms2rescore/report/utils.py
@@ -70,7 +70,8 @@ def get_confidence_estimates(
for when, scores in [("before", score_before), ("after", score_after)]:
try:
confidence[when] = lin_psm_dataset.assign_confidence(scores=scores)
- except RuntimeError:
+ except (RuntimeError, IndexError):
confidence[when] = None
+ logger.warning("Could not assign confidence estimates for %s rescoring.", when)
return confidence["before"], confidence["after"]
diff --git a/ms2rescore/rescoring_engines/mokapot.py b/ms2rescore/rescoring_engines/mokapot.py
index f02d877f..967c40b3 100644
--- a/ms2rescore/rescoring_engines/mokapot.py
+++ b/ms2rescore/rescoring_engines/mokapot.py
@@ -45,7 +45,6 @@ def rescore(
train_fdr: float = 0.01,
write_weights: bool = False,
write_txt: bool = False,
- write_flashlfq: bool = False,
protein_kwargs: Optional[Dict[str, Any]] = None,
**kwargs: Any,
) -> None:
@@ -57,8 +56,7 @@ def rescore(
:py:class:`~mokapot.dataset.LinearPsmDataset`, and then optionally adds protein information
from a FASTA file. The dataset is then passed to the :py:func:`~mokapot.brew` function, which
returns the new scores, q-values, and PEPs. These are then written back to the original
- :py:class:`~psm_utils.psm_list.PSMList`. Optionally, results can be written to a Mokapot text
- file, a FlashLFQ-compatible file, or the model weights can be saved.
+ :py:class:`~psm_utils.psm_list.PSMList`.
Parameters
----------
@@ -75,8 +73,6 @@ def rescore(
Write model weights to a text file. Defaults to ``False``.
write_txt
Write Mokapot results to a text file. Defaults to ``False``.
- write_flashlfq
- Write Mokapot results to a FlashLFQ-compatible file. Defaults to ``False``.
protein_kwargs
Keyword arguments to pass to the :py:meth:`~mokapot.dataset.LinearPsmDataset.add_proteins`
method.
@@ -86,6 +82,13 @@ def rescore(
"""
_set_log_levels()
+ if "write_flashlfq" in kwargs:
+ _ = kwargs.pop("write_flashlfq")
+ logger.warning(
+ "The `write_flashlfq` argument has moved. To write FlashLFQ generic TSV, use the "
+ "MS²Rescore-level `write_flashlfq` option instead."
+ )
+
# Convert PSMList to Mokapot dataset
lin_psm_data = convert_psm_list(psm_list)
feature_names = list(lin_psm_data.features.columns)
@@ -119,10 +122,6 @@ def rescore(
)
if write_txt:
confidence_results.to_txt(file_root=output_file_root, decoys=True)
- if write_flashlfq:
- # TODO: How do we validate that the RTs are in minutes?
- confidence_results.psms["retention_time"] = confidence_results.psms["retention_time"] * 60
- confidence_results.to_flashlfq(output_file_root + ".mokapot.flashlfq.txt")
def convert_psm_list(
@@ -167,10 +166,6 @@ def convert_psm_list(
feature_df.columns = [f"feature:{f}" for f in feature_df.columns]
combined_df = pd.concat([psm_df[required_columns], feature_df], axis=1)
- # Ensure filename for FlashLFQ txt output
- if not combined_df["run"].notnull().all():
- combined_df["run"] = "na"
-
feature_names = [f"feature:{f}" for f in feature_names] if feature_names else None
lin_psm_data = LinearPsmDataset(
diff --git a/pyproject.toml b/pyproject.toml
index 7c1b0a9c..4285f1fa 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -30,23 +30,23 @@ classifiers = [
"Development Status :: 5 - Production/Stable",
]
dynamic = ["version"]
-requires-python = ">=3.8"
+requires-python = ">=3.9"
dependencies = [
"cascade-config>=0.4.0",
"click>=7",
"customtkinter>=5,<6",
- "deeplc>=2.2",
- "deeplcretrainer>=0.2",
+ "deeplc>=3.1",
+ "deeplcretrainer",
"im2deep>=0.1.3",
"jinja2>=3",
"lxml>=4.5",
- "mokapot>=0.9",
+ "mokapot>=0.10",
"ms2pip>=4.0.0",
"ms2rescore_rs>=0.3.0",
- "numpy>=1.16.0",
- "pandas>=1.0",
+ "numpy>=1.25",
+ "pandas>=1",
"plotly>=5",
- "psm_utils>=0.9",
+ "psm_utils>=1.1",
"pyteomics>=4.7.2",
"rich>=12",
"tomli>=2; python_version < '3.11'",