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'",