diff --git a/docs/source/userguide/input-files.rst b/docs/source/userguide/input-files.rst index b556b52..770d12a 100644 --- a/docs/source/userguide/input-files.rst +++ b/docs/source/userguide/input-files.rst @@ -29,6 +29,13 @@ Check the list of :py:mod:`psm_utils` tags in the file extension, the file type can also be inferred from the file name. In that case, ``psm_file_type`` option can be set to ``infer``. +.. note:: + If a previous MS²Rescore run crashed during feature generation or rescoring, an intermediate + file (``.intermediate.psms.tsv``) is automatically saved. This file contains all PSMs + with features that were successfully added up to that point. You can resume processing by + providing this file as the PSM file (``-p .intermediate.psms.tsv -t tsv``) to skip + already completed feature generation steps. + Spectrum file(s) ================ diff --git a/docs/source/userguide/output-files.rst b/docs/source/userguide/output-files.rst index 89c0363..5499e04 100644 --- a/docs/source/userguide/output-files.rst +++ b/docs/source/userguide/output-files.rst @@ -31,6 +31,10 @@ Log and configuration files: +--------------------------------------+--------------------------------------------------------------------------------------+ | ``.feature_names.tsv`` | List of the features and their descriptions | +--------------------------------------+--------------------------------------------------------------------------------------+ +| ``.intermediate.psms.tsv`` | Created automatically if the process crashes during feature generation or rescoring. | +| | Contains all PSMs with successfully added features up to the crash point. Can be | +| | used to resume processing with ``-p .intermediate.psms.tsv -t tsv``. | ++--------------------------------------+--------------------------------------------------------------------------------------+ Rescoring engine files: diff --git a/ms2rescore.spec b/ms2rescore.spec index 8ecd4eb..36e6a24 100644 --- a/ms2rescore.spec +++ b/ms2rescore.spec @@ -16,7 +16,7 @@ project = "ms2rescore" bundle_name = "ms2rescore" bundle_identifier = f"{bundle_name}.{__version__}" -extra_requirements = {"ionmob"} +extra_requirements = {} # Requirements config skip_requirements_regex = r"^(?:.*\..*)" diff --git a/ms2rescore/__main__.py b/ms2rescore/__main__.py index 8a27811..80cefa5 100644 --- a/ms2rescore/__main__.py +++ b/ms2rescore/__main__.py @@ -6,6 +6,7 @@ import json import logging import sys +from datetime import datetime from pathlib import Path from typing import Union @@ -196,7 +197,13 @@ def profile(fnc, filepath): def inner(*args, **kwargs): with cProfile.Profile() as profiler: return_value = fnc(*args, **kwargs) - profiler.dump_stats(filepath + ".profile.prof") + + # Add timestamp to profiler output filename + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + profile_filename = f"{filepath}.profile_{timestamp}.prof" + profiler.dump_stats(profile_filename) + LOGGER.info(f"Profile data written to: {profile_filename}") + return return_value return inner @@ -248,6 +255,7 @@ def main(tims=False): # Run MS²Rescore try: if config["ms2rescore"]["profile"]: + LOGGER.info("Profiling enabled") profiled_rescore = profile(rescore, config["ms2rescore"]["output_path"]) profiled_rescore(configuration=config) else: diff --git a/ms2rescore/core.py b/ms2rescore/core.py index 4ea8103..eb3b4cc 100644 --- a/ms2rescore/core.py +++ b/ms2rescore/core.py @@ -13,7 +13,10 @@ from ms2rescore.parse_spectra import add_precursor_values from ms2rescore.report import generate from ms2rescore.rescoring_engines import mokapot, percolator -from ms2rescore.rescoring_engines.mokapot import add_peptide_confidence, add_psm_confidence +from ms2rescore.rescoring_engines.mokapot import ( + add_peptide_confidence, + add_psm_confidence, +) logger = logging.getLogger(__name__) @@ -34,7 +37,9 @@ def rescore(configuration: Dict, psm_list: Optional[PSMList] = None) -> None: f"Running MS²Rescore with following configuration: {json.dumps(configuration, indent=4)}" ) config = configuration["ms2rescore"] - output_file_root = config["output_path"] + output_file_root = config["output_path"].split(".intermidiate.")[ + 0 + ] # if no intermediate, takes full name # Write full configuration including defaults to file with open(output_file_root + ".full-config.json", "w") as f: @@ -59,6 +64,17 @@ def rescore(configuration: Dict, psm_list: Optional[PSMList] = None) -> None: logger.debug( f"PSMs already contain the following rescoring features: {psm_list_feature_names}" ) + # ckeck if all features are already present + for fgen_name, fgen_config in list(config["feature_generators"].items()): + fgen_features = FEATURE_GENERATORS[fgen_name]().feature_names + if set(fgen_features).issubset(psm_list_feature_names): + logger.debug( + f"Skipping feature generator {fgen_name} because all features are already " + "present in the PSM file." + ) + feature_names[fgen_name] = set(fgen_features) + feature_names["psm_file"] = psm_list_feature_names - set(fgen_features) + del config["feature_generators"][fgen_name] # Add missing precursor info from spectrum file if needed required_ms_data = { @@ -89,12 +105,26 @@ def rescore(configuration: Dict, psm_list: Optional[PSMList] = None) -> None: "files or disable the feature generator." ) continue - - # Add features - fgen.add_features(psm_list) + try: + fgen.add_features(psm_list) + except (Exception, KeyboardInterrupt) as e: + logger.error( + f"Error while adding features from {fgen_name}: {e}, writing intermediary output..." + ) + # Write intermediate TSV + psm_utils.io.write_file( + psm_list, output_file_root + ".intermediate.psms.tsv", filetype="tsv" + ) + raise e logger.debug(f"Adding features from {fgen_name}: {set(fgen.feature_names)}") feature_names[fgen_name] = set(fgen.feature_names) + # Remove overlapping features from psm_file to avoid duplicates + # (e.g., hyperscore can be in both psm_file and ms2pip) + overlap = feature_names.get("psm_file", set()) & feature_names[fgen_name] + if overlap: + feature_names["psm_file"] = feature_names["psm_file"] - overlap + # Filter out psms that do not have all added features all_feature_names = {f for fgen in feature_names.values() for f in fgen} psms_with_features = [ @@ -114,6 +144,12 @@ def rescore(configuration: Dict, psm_list: Optional[PSMList] = None) -> None: ) psm_list = psm_list[psms_with_features] + if "mumble" in config["psm_generator"]: + from ms2rescore.utils import filter_mumble_psms + + # Remove PSMs where matched_ions_pct drops 25% below the original hit + psm_list = filter_mumble_psms(psm_list, threshold=0.75) + # Write feature names to file _write_feature_names(feature_names, output_file_root) @@ -160,10 +196,12 @@ def rescore(configuration: Dict, psm_list: Optional[PSMList] = None) -> None: protein_kwargs=protein_kwargs, **config["rescoring_engine"]["mokapot"], ) - except exceptions.RescoringError as e: + except (Exception, KeyboardInterrupt) as e: # Write output - logger.info(f"Writing intermediary output to {output_file_root}.psms.tsv...") - psm_utils.io.write_file(psm_list, output_file_root + ".psms.tsv", filetype="tsv") + logger.info(f"Writing intermediary output to {output_file_root}.intermediate.psms.tsv...") + psm_utils.io.write_file( + psm_list, output_file_root + ".intermediate.psms.tsv", filetype="tsv" + ) # Reraise exception raise e @@ -219,7 +257,10 @@ def _write_feature_names(feature_names, output_file_root): def _log_id_psms_before(psm_list: PSMList, fdr: float = 0.01, max_rank: int = 1) -> int: """Log #PSMs identified before rescoring.""" id_psms_before = ( - (psm_list["qvalue"] <= 0.01) & (psm_list["rank"] <= max_rank) & (~psm_list["is_decoy"]) + (psm_list["qvalue"] <= 0.01) + & (psm_list["rank"] <= max_rank) + & (~psm_list["is_decoy"]) + & ([metadata.get("original_psm", True) for metadata in psm_list["metadata"]]) ).sum() logger.info( f"Found {id_psms_before} identified PSMs with rank <= {max_rank} at {fdr} FDR before " @@ -285,7 +326,9 @@ def _calculate_confidence(psm_list: PSMList) -> PSMList: ) # Recalculate confidence - new_confidence = lin_psm_data.assign_confidence(scores=psm_list["score"]) + new_confidence = lin_psm_data.assign_confidence( + scores=list(psm_list["score"]) + ) # explicity make it a list to avoid TypingError: Failed in nopython mode pipeline (step: nopython frontend) in mokapot # Add new confidence estimations to PSMList add_psm_confidence(psm_list, new_confidence) diff --git a/ms2rescore/exceptions.py b/ms2rescore/exceptions.py index ba5492a..68b1b43 100644 --- a/ms2rescore/exceptions.py +++ b/ms2rescore/exceptions.py @@ -41,3 +41,9 @@ class RescoringError(MS2RescoreError): """Error while rescoring PSMs.""" pass + + +class ParseSpectrumError(MS2RescoreError): + """Error while rescoring PSMs.""" + + pass diff --git a/ms2rescore/feature_generators/__init__.py b/ms2rescore/feature_generators/__init__.py index d89f087..e597e33 100644 --- a/ms2rescore/feature_generators/__init__.py +++ b/ms2rescore/feature_generators/__init__.py @@ -6,15 +6,13 @@ from ms2rescore.feature_generators.basic import BasicFeatureGenerator from ms2rescore.feature_generators.deeplc import DeepLCFeatureGenerator from ms2rescore.feature_generators.im2deep import IM2DeepFeatureGenerator -from ms2rescore.feature_generators.ionmob import IonMobFeatureGenerator -from ms2rescore.feature_generators.maxquant import MaxQuantFeatureGenerator +from ms2rescore.feature_generators.ms2 import MS2FeatureGenerator from ms2rescore.feature_generators.ms2pip import MS2PIPFeatureGenerator FEATURE_GENERATORS: dict[str, type[FeatureGeneratorBase]] = { "basic": BasicFeatureGenerator, "ms2pip": MS2PIPFeatureGenerator, "deeplc": DeepLCFeatureGenerator, - "maxquant": MaxQuantFeatureGenerator, - "ionmob": IonMobFeatureGenerator, "im2deep": IM2DeepFeatureGenerator, + "ms2": MS2FeatureGenerator, } diff --git a/ms2rescore/feature_generators/base.py b/ms2rescore/feature_generators/base.py index 1e96d86..6ab4fb5 100644 --- a/ms2rescore/feature_generators/base.py +++ b/ms2rescore/feature_generators/base.py @@ -17,11 +17,11 @@ def __init__(self, *args, **kwargs) -> None: @property @abstractmethod - def feature_names(self): + def feature_names(self) -> list[str]: pass @abstractmethod - def add_features(self, psm_list: PSMList): + def add_features(self, psm_list: PSMList) -> None: pass diff --git a/ms2rescore/feature_generators/basic.py b/ms2rescore/feature_generators/basic.py index bc2222e..3e9aa37 100644 --- a/ms2rescore/feature_generators/basic.py +++ b/ms2rescore/feature_generators/basic.py @@ -31,13 +31,24 @@ def __init__(self, *args, **kwargs) -> None: """ super().__init__(*args, **kwargs) - self._feature_names = None @property def feature_names(self) -> List[str]: - if self._feature_names is None: - raise ValueError("Feature names have not been set yet. First run `add_features`.") - return self._feature_names + return [ + "charge_n", + "charge_1", + "charge_2", + "charge_3", + "charge_4", + "charge_5", + "charge_6", + "abs_ms1_error_ppm", + "search_engine_score", + "theoretical_mass", + "experimental_mass", + "mass_error", + "pep_len", + ] def add_features(self, psm_list: PSMList) -> None: """ @@ -51,11 +62,10 @@ def add_features(self, psm_list: PSMList) -> None: """ logger.info("Adding basic features to PSMs.") - self._feature_names = [] # Reset feature names - charge_states = np.array([psm.peptidoform.precursor_charge for psm in psm_list]) precursor_mzs = psm_list["precursor_mz"] scores = psm_list["score"] + peptide_lengths = np.array([len(psm.peptidoform.sequence) for psm in psm_list]) has_charge = None not in charge_states has_mz = None not in precursor_mzs and has_charge @@ -63,16 +73,16 @@ def add_features(self, psm_list: PSMList) -> None: if has_charge: charge_n = charge_states - charge_one_hot, one_hot_names = _one_hot_encode_charge(charge_states) - self._feature_names.extend(["charge_n"] + one_hot_names) + charge_one_hot, _ = _one_hot_encode_charge(charge_states) if has_mz: # Charge also required for theoretical m/z theo_mz = np.array([psm.peptidoform.theoretical_mz for psm in psm_list]) abs_ms1_error_ppm = np.abs((precursor_mzs - theo_mz) / theo_mz * 10**6) - self._feature_names.append("abs_ms1_error_ppm") - if has_score: - self._feature_names.append("search_engine_score") + if has_mz and has_charge: + experimental_mass = (precursor_mzs * charge_n) - (charge_n * 1.007276466812) + theoretical_mass = (theo_mz * charge_n) - (charge_n * 1.007276466812) + mass_error = experimental_mass - theoretical_mass for i, psm in enumerate(psm_list): psm.rescoring_features.update( @@ -81,6 +91,10 @@ def add_features(self, psm_list: PSMList) -> None: **charge_one_hot[i] if has_charge else {}, **{"abs_ms1_error_ppm": abs_ms1_error_ppm[i]} if has_mz else {}, **{"search_engine_score": scores[i]} if has_score else {}, + **{"theoretical_mass": theoretical_mass[i]} if has_mz and has_charge else {}, + **{"experimental_mass": experimental_mass[i]} if has_mz and has_charge else {}, + **{"mass_error": mass_error[i]} if has_mz and has_charge else {}, + **{"pep_len": peptide_lengths[i]}, ) ) @@ -88,15 +102,18 @@ def add_features(self, psm_list: PSMList) -> None: def _one_hot_encode_charge( charge_states: np.ndarray, ) -> Tuple[Iterable[Dict[str, int]], List[str]]: - """One-hot encode charge states.""" + """One-hot encode charge states with fixed range 1-6.""" n_entries = len(charge_states) - min_charge = np.min(charge_states) - max_charge = np.max(charge_states) + heading = [f"charge_{i}" for i in range(1, 7)] - mask = np.zeros((n_entries, max_charge - min_charge + 1), dtype=bool) - mask[np.arange(n_entries), charge_states - min_charge] = 1 - one_hot = mask.view("i1") + # Create mask for charges 1-6 + mask = np.zeros((n_entries, 6), dtype=bool) - heading = [f"charge_{i}" for i in range(min_charge, max_charge + 1)] + # Set the appropriate charge position to 1 for each entry + for i, charge in enumerate(charge_states): + if charge is not None and 1 <= charge <= 6: + mask[i, int(charge) - 1] = 1 + + one_hot = mask.view("i1") return [dict(zip(heading, row)) for row in one_hot], heading diff --git a/ms2rescore/feature_generators/deeplc.py b/ms2rescore/feature_generators/deeplc.py index f372815..b1010c8 100644 --- a/ms2rescore/feature_generators/deeplc.py +++ b/ms2rescore/feature_generators/deeplc.py @@ -15,23 +15,22 @@ """ -import contextlib import logging -import os -from collections import defaultdict -from inspect import getfullargspec -from itertools import chain from typing import List, Union import numpy as np from psm_utils import PSMList +from deeplc.core import predict, finetune +from deeplc.calibration import SplineTransformerCalibration from ms2rescore.feature_generators.base import FeatureGeneratorBase from ms2rescore.parse_spectra import MSDataType -os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" logger = logging.getLogger(__name__) +# Suppress verbose onnx2torch logging +logging.getLogger("onnx2torch").setLevel(logging.WARNING) + class DeepLCFeatureGenerator(FeatureGeneratorBase): """DeepLC retention time-based feature generator.""" @@ -41,7 +40,6 @@ class DeepLCFeatureGenerator(FeatureGeneratorBase): def __init__( self, *args, - lower_score_is_better: bool = False, calibration_set_size: Union[int, float, None] = None, processes: int = 1, **kwargs, @@ -54,8 +52,7 @@ def __init__( Parameters ---------- - lower_score_is_better - Whether a lower PSM score denotes a better matching PSM. Default: False + calibration_set_size: int or float Amount of best PSMs to use for DeepLC calibration. If this value is lower than the number of available PSMs, all PSMs will be used. (default: 0.15) @@ -72,34 +69,42 @@ def __init__( """ super().__init__(*args, **kwargs) - self.lower_psm_score_better = lower_score_is_better self.calibration_set_size = calibration_set_size - self.processes = processes self.deeplc_kwargs = kwargs or {} self._verbose = logger.getEffectiveLevel() <= logging.DEBUG - # Lazy-load DeepLC - from deeplc import DeepLC + self.model = self.deeplc_kwargs.get("model", None) - self.DeepLC = DeepLC + self.calibration = None - # Remove any kwargs that are not DeepLC arguments - self.deeplc_kwargs = { - k: v for k, v in self.deeplc_kwargs.items() if k in getfullargspec(DeepLC).args - } - self.deeplc_kwargs.update({"config_file": None}) + # Prepare DeepLC predict kwargs + self.predict_kwargs = { + k: v for k, v in self.deeplc_kwargs.items() if k in ["device", "batch_size"] + } # getfullargspec(predict).args does not work on this outer predict function + # self.predict_kwargs["num_workers"] = processes - # Set default DeepLC arguments + # Prepare DeepLC finetune kwargs if "deeplc_retrain" not in self.deeplc_kwargs: self.deeplc_kwargs["deeplc_retrain"] = False - - self.deeplc_predictor = None - if "path_model" in self.deeplc_kwargs: - self.user_model = self.deeplc_kwargs.pop("path_model") - logging.debug(f"Using user-provided DeepLC model {self.user_model}.") - else: - self.user_model = None + return # skip the rest of the init if no retraining + + if self.deeplc_kwargs["deeplc_retrain"]: + self.finetune_kwargs = { + k: v + for k, v in self.deeplc_kwargs.items() + if k + in [ + "epochs", + "device", + "batch_size", + "learning_rate", + "patience", + "trainable_layers", + "validation_split", + ] + } + self.finetune_kwargs["threads"] = processes @property def feature_names(self) -> List[str]: @@ -116,131 +121,234 @@ def add_features(self, psm_list: PSMList) -> None: """Add DeepLC-derived features to PSMs.""" logger.info("Adding DeepLC-derived features to PSMs.") + psm_list_df = psm_list.to_dataframe() + psm_list_df = psm_list_df[ + [ + "peptidoform", + "retention_time", + "run", + "qvalue", + "is_decoy", + ] + ] + psm_list_df["sequence"] = psm_list_df["peptidoform"].apply(lambda x: x.modified_sequence) - # Get easy-access nested version of PSMList - psm_dict = psm_list.get_psm_dict() - - # Run DeepLC for each spectrum file - current_run = 1 - total_runs = sum(len(runs) for runs in psm_dict.values()) - - for runs in psm_dict.values(): - # Reset DeepLC predictor for each collection of runs - self.deeplc_predictor = None - self.selected_model = None - for run, psms in runs.items(): - peptide_rt_diff_dict = defaultdict( - lambda: { - "observed_retention_time_best": np.inf, - "predicted_retention_time_best": np.inf, - "rt_diff_best": np.inf, - } - ) - logger.info( - f"Running DeepLC for PSMs from run ({current_run}/{total_runs}): `{run}`..." - ) + if self.deeplc_kwargs["deeplc_retrain"]: + # Filter high-confidence target PSMs once for transfer learning + target_mask = (psm_list["qvalue"] <= 0.01) & (~psm_list["is_decoy"]) + target_psms = psm_list[target_mask] - # Disable wild logging to stdout by Tensorflow, unless in debug mode - - 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()))) - - psm_list_calibration = self._get_calibration_psms(psm_list_run) - logger.debug(f"Calibrating DeepLC with {len(psm_list_calibration)} PSMs...") - self.deeplc_predictor = self.DeepLC( - n_jobs=self.processes, - verbose=self._verbose, - path_model=self.selected_model or self.user_model, - **self.deeplc_kwargs, - ) - self.deeplc_predictor.calibrate_preds(psm_list_calibration) - # Still calibrate for each run, but do not try out all model options. - # Just use model that was selected based on first run - if not self.selected_model: - self.selected_model = list(self.deeplc_predictor.model.keys()) - self.deeplc_kwargs["deeplc_retrain"] = False - logger.debug( - f"Selected DeepLC model {self.selected_model} based on " - "calibration of first run. Using this model (after new " - "calibrations) for the remaining runs." - ) - - logger.debug("Predicting retention times...") - predictions = np.array(self.deeplc_predictor.make_preds(psm_list_run)) - observations = psm_list_run["retention_time"] - rt_diffs_run = np.abs(predictions - observations) - - logger.debug("Adding features to PSMs...") - for i, psm in enumerate(psm_list_run): - psm["rescoring_features"].update( - { - "observed_retention_time": observations[i], - "predicted_retention_time": predictions[i], - "rt_diff": rt_diffs_run[i], - } - ) - peptide = psm.peptidoform.proforma.split("\\")[0] # remove charge - if peptide_rt_diff_dict[peptide]["rt_diff_best"] > rt_diffs_run[i]: - peptide_rt_diff_dict[peptide] = { - "observed_retention_time_best": observations[i], - "predicted_retention_time_best": predictions[i], - "rt_diff_best": rt_diffs_run[i], - } - for psm in psm_list_run: - psm["rescoring_features"].update( - peptide_rt_diff_dict[psm.peptidoform.proforma.split("\\")[0]] - ) - current_run += 1 - - def _get_calibration_psms(self, psm_list: PSMList): - """Get N best scoring target PSMs for calibration.""" - psm_list_targets = psm_list[~psm_list["is_decoy"]] - if self.calibration_set_size: - n_psms = self._get_number_of_calibration_psms(psm_list_targets) - indices = np.argsort(psm_list_targets["score"]) - indices = indices[:n_psms] if self.lower_psm_score_better else indices[-n_psms:] - return psm_list_targets[indices] - else: - identified_psms = psm_list_targets[psm_list_targets["qvalue"] <= 0.01] - if len(identified_psms) == 0: - raise ValueError( - "No target PSMs with q-value <= 0.01 found. Please set calibration set size for calibrating deeplc." - ) - elif (len(identified_psms) < 500) & (self.deeplc_kwargs["deeplc_retrain"]): - logger.warning( - " Less than 500 target PSMs with q-value <= 0.01 found for retraining. Consider turning of deeplc_retrain, as this is likely not enough data for retraining." - ) - return identified_psms + # Determine best run for transfer learning + best_run = self._best_run_by_shared_proteoforms( + target_psms["run"], + target_psms["peptidoform"], + ) + + # Fine-tune on best run + best_run_psms = target_psms[target_psms["run"] == best_run] + logger.debug( + f"Fine-tuning DeepLC on run '{best_run}'... with {len(best_run_psms)} PSMs" + ) + self.model = finetune( + best_run_psms, + model=self.model, + train_kwargs=self.finetune_kwargs, + ) + + # Predict retention times for all PSMs + logger.info("Predicting retention times with DeepLC...") + psm_list_df["predicted_retention_time"] = predict( + psm_list, model=self.model, predict_kwargs=self.predict_kwargs + ) + + # Calibrate predictions per run + logger.info("Calibrating predicted retention times per run...") + psm_list_df = psm_list_df.sort_values("qvalue") + for run in psm_list_df["run"].unique(): + run_df = psm_list_df[psm_list_df["run"] == run] + + # Get calibration data (target PSMs only) + observed_rt_calibration, predicted_rt_calibration = self._get_calibration_data(run_df) + if len(observed_rt_calibration) == 0: + raise ValueError(f"Run '{run}' has no target PSMs available for calibration.") + + # Fit calibration and transform all predictions for this run + calibration = SplineTransformerCalibration() + calibration.fit( + target=observed_rt_calibration, + source=predicted_rt_calibration, + ) + + calibrated_rt = calibration.transform(run_df["predicted_retention_time"].values) + + # Update predictions with calibrated values + psm_list_df.loc[psm_list_df["run"] == run, "predicted_retention_time"] = calibrated_rt + + psm_list_df["observed_retention_time"] = psm_list_df["retention_time"] + psm_list_df["rt_diff"] = ( + psm_list_df["observed_retention_time"] - psm_list_df["predicted_retention_time"] + ).abs() + psm_list_df_best = psm_list_df.loc[ + psm_list_df.groupby(["run", "sequence"])["rt_diff"].idxmin() + ] + + psm_list_df = psm_list_df.merge( + psm_list_df_best[ + [ + "run", + "sequence", + "observed_retention_time", + "predicted_retention_time", + "rt_diff", + ] + ], + on=["run", "sequence"], + how="left", + suffixes=("", "_best"), + ) + + psm_list_feature_dicts = psm_list_df[self.feature_names].to_dict(orient="records") + # Add features to PSMs + logger.debug("Adding features to PSMs...") + for psm, features in zip(psm_list, psm_list_feature_dicts): + psm.rescoring_features.update(features) + + def _get_calibration_data(self, run_df) -> tuple[np.ndarray, np.ndarray]: + """Get calibration data (observed and predicted RTs) from run dataframe. + + Only target (non-decoy) PSMs are used for calibration. - def _get_number_of_calibration_psms(self, psm_list): - """Get number of calibration PSMs given `calibration_set_size` and total number of PSMs.""" + Parameters + ---------- + run_df : pd.DataFrame + Dataframe containing PSMs for a single run, pre-sorted by qvalue ascending, with + columns: 'retention_time', 'predicted_retention_time', 'qvalue', 'is_decoy' + + Returns + ------- + tuple[np.ndarray, np.ndarray] + Observed and predicted retention times for calibration + """ + # Filter to target PSMs only (run_df is pre-sorted by qvalue) + target_df = run_df[~run_df["is_decoy"]] + + # Determine number of calibration PSMs if isinstance(self.calibration_set_size, float): if not 0 < self.calibration_set_size <= 1: raise ValueError( "If `calibration_set_size` is a float, it cannot be smaller than " "or equal to 0 or larger than 1." ) - else: - num_calibration_psms = round(len(psm_list) * self.calibration_set_size) + num_calibration_psms = round(len(target_df) * self.calibration_set_size) elif isinstance(self.calibration_set_size, int): - if self.calibration_set_size > len(psm_list): + if self.calibration_set_size > len(target_df): logger.warning( - f"Requested number of calibration PSMs ({self.calibration_set_size}" - f") is larger than total number of PSMs ({len(psm_list)}). Using " - "all PSMs for calibration." + f"Requested number of calibration PSMs ({self.calibration_set_size}) " + f"is larger than total number of target PSMs ({len(target_df)}). Using " + "all target PSMs for calibration." ) - num_calibration_psms = len(psm_list) + num_calibration_psms = len(target_df) else: num_calibration_psms = self.calibration_set_size else: - raise TypeError( - "Expected float or int for `calibration_set_size`. Got " - f"{type(self.calibration_set_size)} instead. " + # Use PSMs with q-value <= 0.01 + num_calibration_psms = (target_df["qvalue"] <= 0.01).sum() + + logger.debug(f"Using {num_calibration_psms} target PSMs for calibration") + + calibration_df = target_df.head(num_calibration_psms) + + return ( + calibration_df["retention_time"].values, + calibration_df["predicted_retention_time"].values, + ) + + @staticmethod + def _best_run_by_shared_proteoforms(runs, proteoforms): + """ + Return the run whose proteoform set has the largest total overlap with all other runs: + score(run_i) = sum_{j != i} |P_i ∩ P_j| + + Tie / degenerate handling (per request): + - If no run shares anything (all scores == 0): warn and return the first run (by first appearance). + - If multiple runs are tied for best score: return the first among them (by first appearance). + """ + logger.debug("Determining best run for transfer learning based on shared proteoforms.") + runs = np.asarray(runs) + proteoforms = np.asarray(proteoforms) + if runs.shape[0] != proteoforms.shape[0]: + raise ValueError("runs and proteoforms must have the same length.") + if runs.size == 0: + raise ValueError("Empty input: runs/proteoforms must contain at least one entry.") + + # Preserve run order by first appearance + run_to_idx = {} + run_order = [] + run_idx = np.empty(runs.shape[0], dtype=np.int64) + for i, r in enumerate(runs): + if r not in run_to_idx: + run_to_idx[r] = len(run_order) + run_order.append(r) + run_idx[i] = run_to_idx[r] + + # Fast path: sparse incidence matrix + try: + from scipy.sparse import coo_matrix + except ImportError: + # Fallback (slower): set-based + run_sets = {} + for r, p in zip(runs, proteoforms): + run_sets.setdefault(r, set()).add(p) + + scores = [] + for r in run_order: + Pi = run_sets[r] + s = 0 + for r2 in run_order: + if r2 is r: + continue + s += len(Pi & run_sets[r2]) + scores.append(s) + + scores = np.asarray(scores, dtype=np.int64) + max_score = scores.max() + best_candidates = np.flatnonzero(scores == max_score) + + if max_score == 0: + logger.warning( + "No runs share any identified proteoforms with other runs; transfer learning " + "might not be as effective." + ) + return run_order[0] + + return run_order[int(best_candidates[0])] + + # Encode proteoforms (order does not matter for correctness) + _, prot_idx = np.unique(proteoforms, return_inverse=True) + + # De-duplicate (run, proteoform) pairs + pairs = np.unique(np.stack([run_idx, prot_idx], axis=1), axis=0) + r = pairs[:, 0] + p = pairs[:, 1] + + M = coo_matrix( + (np.ones(len(r), dtype=np.int8), (r, p)), + shape=(len(run_order), int(prot_idx.max()) + 1), + ).tocsr() + + # Overlap matrix O[i,j] = |P_i ∩ P_j| + overlap = (M @ M.T).toarray() + np.fill_diagonal(overlap, 0) + + scores = overlap.sum(axis=1).astype(np.int64) + max_score = int(scores.max()) + best_candidates = np.flatnonzero(scores == max_score) + + if max_score == 0: + logger.warning( + "No runs share any identified proteoforms with other runs; transfer learning " + "might not be as effective." ) - logger.debug(f"Using {num_calibration_psms} PSMs for calibration") - return num_calibration_psms + return run_order[0] + + return run_order[int(best_candidates[0])] diff --git a/ms2rescore/feature_generators/im2deep.py b/ms2rescore/feature_generators/im2deep.py index 3ddbaff..2380060 100644 --- a/ms2rescore/feature_generators/im2deep.py +++ b/ms2rescore/feature_generators/im2deep.py @@ -8,23 +8,18 @@ """ -import contextlib import logging -import os -from inspect import getfullargspec -from itertools import chain -from typing import List +from typing import List, Union import numpy as np -import pandas as pd -from im2deep.utils import im2ccs -from im2deep.im2deep import predict_ccs from psm_utils import PSMList +from im2deep.core import predict +from im2deep.calibration import LinearCCSCalibration, get_default_reference +from im2deep.utils import im2ccs from ms2rescore.feature_generators.base import FeatureGeneratorBase from ms2rescore.parse_spectra import MSDataType -os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" logger = logging.getLogger(__name__) @@ -35,6 +30,8 @@ class IM2DeepFeatureGenerator(FeatureGeneratorBase): def __init__( self, + multi: bool = False, + calibration_set_size: Union[int, float] = None, *args, processes: int = 1, **kwargs, @@ -52,14 +49,24 @@ def __init__( """ super().__init__(*args, **kwargs) - self._verbose = logger.getEffectiveLevel() <= logging.DEBUG + self.multi = multi + if self.multi: + raise NotImplementedError( + "Multi-IM mode is not yet implemented for IM2DeepFeatureGenerator." + ) + + self.calibration_set_size = calibration_set_size - # Remove any kwargs that are not IM2Deep arguments self.im2deep_kwargs = kwargs or {} - self.im2deep_kwargs = { - k: v for k, v in self.im2deep_kwargs.items() if k in getfullargspec(predict_ccs).args + self._verbose = logger.getEffectiveLevel() <= logging.DEBUG + + self.model = self.im2deep_kwargs.get("model", None) + + # Prepare IM2Deep predict kwargs + self.predict_kwargs = { + k: v for k, v in self.im2deep_kwargs.items() if k in ["device", "batch_size"] } - self.im2deep_kwargs["n_jobs"] = processes + self.predict_kwargs["num_workers"] = processes @property def feature_names(self) -> List[str]: @@ -73,100 +80,125 @@ def feature_names(self) -> List[str]: def add_features(self, psm_list: PSMList) -> None: """Add IM2Deep-derived features to PSMs""" + logger.info("Adding IM2Deep-derived features to PSMs") - # Get easy-access nested version of PSMlist - psm_dict = psm_list.get_psm_dict() + psm_list_df = psm_list.to_dataframe() + psm_list_df = psm_list_df[ + [ + "peptidoform", + "ion_mobility", + "precursor_mz", + "run", + "qvalue", + "is_decoy", + "metadata", + ] + ] - # Run IM2Deep for each spectrum file - current_run = 1 - total_runs = sum(len(runs) for runs in psm_dict.values()) + psm_list_df["sequence"] = psm_list_df["peptidoform"].apply(lambda x: x.modified_sequence) + psm_list_df["charge"] = [pep.precursor_charge for pep in psm_list_df["peptidoform"]] + psm_list_df["ccs_observed_im2deep"] = im2ccs( + psm_list_df["ion_mobility"], + psm_list_df["precursor_mz"], + psm_list_df["charge"], + ) - for runs in psm_dict.values(): - # Reset IM2Deep predictor for each collection of runs - for run, psms in runs.items(): - logger.info( - f"Running IM2Deep for PSMs from run ({current_run}/{total_runs}): `{run}`..." - ) + # Make predictions with IM2Deep + logger.info("Predicting CCS values with IM2Deep...") + psm_list_df["predicted_CCS_uncalibrated"] = predict( + psm_list, model=self.model, predict_kwargs=self.predict_kwargs + ) - # Disable wild logging to stdout by TensorFlow, unless in debug mode - 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()))) - - logger.debug("Calibrating IM2Deep...") - - # Convert ion mobility to CCS and calibrate CCS values - psm_list_run_df = psm_list_run.to_dataframe() - psm_list_run_df["charge"] = [ - pep.precursor_charge for pep in psm_list_run_df["peptidoform"] - ] - psm_list_run_df["ccs_observed"] = im2ccs( - psm_list_run_df["ion_mobility"], - psm_list_run_df["precursor_mz"], - psm_list_run_df["charge"], - ) - - # Create dataframe with high confidence hits for calibration - cal_psm_df = self.make_calibration_df(psm_list_run_df) - - # Make predictions with IM2Deep - logger.debug("Predicting CCS values...") - predictions = predict_ccs( - psm_list_run, cal_psm_df, write_output=False, **self.im2deep_kwargs - ) - - # Add features to PSMs - logger.debug("Adding features to PSMs...") - observations = psm_list_run_df["ccs_observed"] - ccs_diffs_run = np.abs(predictions - observations) - for i, psm in enumerate(psm_list_run): - psm["rescoring_features"].update( - { - "ccs_observed_im2deep": observations[i], - "ccs_predicted_im2deep": predictions[i], - "ccs_error_im2deep": ccs_diffs_run[i], - "abs_ccs_error_im2deep": np.abs(ccs_diffs_run[i]), - "perc_ccs_error_im2deep": np.abs(ccs_diffs_run[i]) - / observations[i] - * 100, - } - ) - - current_run += 1 - - @staticmethod - def make_calibration_df(psm_list_df: pd.DataFrame, threshold: float = 0.25) -> pd.DataFrame: - """ - Make dataframe for calibration of IM2Deep predictions. + # getting reference CCS values for calibration + source_dataframe = get_default_reference(multi=self.multi) + + # Create dataframe with high confidence hits for calibration + logger.info("Calibrating predicted CCS values per run...") + for run in psm_list_df["run"].unique(): + run_df = psm_list_df[psm_list_df["run"] == run].copy() + + calibration_df = self._get_im_calibration_data(run_df) + + calibration = LinearCCSCalibration() + calibration.fit( + psm_df_target=calibration_df, + psm_df_source=source_dataframe, + ) + + calibrated_im = calibration.transform( + run_df[["peptidoform", "predicted_CCS_uncalibrated"]] + ) + + # Update predictions with calibrated values + psm_list_df.loc[psm_list_df["run"] == run, "predicted_CCS_uncalibrated"] = ( + calibrated_im + ) + + # Apply calibration shifts + psm_list_df.rename( + columns={"predicted_CCS_uncalibrated": "ccs_predicted_im2deep"}, inplace=True + ) + psm_list_df["ccs_error_im2deep"] = ( + psm_list_df["ccs_predicted_im2deep"] - psm_list_df["ccs_observed_im2deep"] + ) + psm_list_df["abs_ccs_error_im2deep"] = np.abs(psm_list_df["ccs_error_im2deep"]) + psm_list_df["perc_ccs_error_im2deep"] = ( + np.abs(psm_list_df["ccs_error_im2deep"]) / psm_list_df["ccs_observed_im2deep"] * 100 + ) + + psm_list_feature_dicts = psm_list_df[self.feature_names].to_dict(orient="records") + # Add features to PSMs + logger.debug("Adding features to PSMs...") + for psm, features in zip(psm_list, psm_list_feature_dicts): + psm.rescoring_features.update(features) + + def _get_im_calibration_data(self, run_df) -> tuple[np.ndarray, np.ndarray]: + """Get calibration data (observed and predicted CCS values) from run dataframe. + + Only target (non-decoy) PSMs are used for calibration. Parameters ---------- - psm_list_df - DataFrame with PSMs. - threshold - Percentage of highest scoring identified target PSMs to use for calibration, - default 0.25. - + run_df : pd.DataFrame + Dataframe containing PSMs for a single run, with columns: + 'ccs_observed_im2deep', 'ccs_predicted_im2deep', 'qvalue', 'is_decoy' Returns ------- - pd.DataFrame - DataFrame with high confidence hits for calibration. - + tuple[np.ndarray, np.ndarray] + Observed and predicted CCS values for calibration """ - identified_psms = psm_list_df[ - (psm_list_df["qvalue"] < 0.01) - & (~psm_list_df["is_decoy"]) - & (psm_list_df["charge"] < 5) # predictions do not go higher for IM2Deep - ] - calibration_psms = identified_psms[ - identified_psms["qvalue"] < identified_psms["qvalue"].quantile(1 - threshold) - ] - logger.debug( - f"Number of high confidence hits for calculating shift: {len(calibration_psms)}" + # Filter to target PSMs only + target_df = run_df[~run_df["is_decoy"]].copy() + target_df = target_df.sort_values("qvalue", ascending=True) + + # Determine number of calibration PSMs + if isinstance(self.calibration_set_size, float): + if not 0 < self.calibration_set_size <= 1: + raise ValueError( + "If `calibration_set_size` is a float, it cannot be smaller than " + "or equal to 0 or larger than 1." + ) + num_calibration_psms = round(len(target_df) * self.calibration_set_size) + elif isinstance(self.calibration_set_size, int): + if self.calibration_set_size > len(target_df): + logger.warning( + f"Requested number of calibration PSMs ({self.calibration_set_size}) " + f"is larger than total number of target PSMs ({len(target_df)}). Using " + "all target PSMs for calibration." + ) + num_calibration_psms = len(target_df) + else: + num_calibration_psms = self.calibration_set_size + else: + # Use PSMs with q-value <= 0.01 + num_calibration_psms = (target_df["qvalue"] <= 0.01).sum() + + logger.debug(f"Using {num_calibration_psms} target PSMs for calibration") + + # Select calibration PSMs (assuming they are sorted by q-value) + calibration_df = target_df.head(num_calibration_psms) + + return calibration_df[["peptidoform", "ccs_observed_im2deep"]].rename( + columns={"ccs_observed_im2deep": "CCS"} ) - return calibration_psms diff --git a/ms2rescore/feature_generators/ionmob.py b/ms2rescore/feature_generators/ionmob.py deleted file mode 100644 index d6b4a88..0000000 --- a/ms2rescore/feature_generators/ionmob.py +++ /dev/null @@ -1,296 +0,0 @@ -""" -``ionmob`` collisional cross section (CCS)-based feature generator. - -``ionmob`` is a predictor for peptide collisional cross sections (CCS), as measured in ion mobility -devices, such as the Bruker timsTOF instruments. More info can be found on the -`ionmob GitHub page `_. - -If you use ``ionmob`` in your work, please cite the following publication: - -.. epigraph:: - Teschner, D. et al. Ionmob: a Python package for prediction of peptide collisional - cross-section values. *Bioinformatics* 39, btad486 (2023). - `doi:10.1093/bioinformatics/btad486 `_ - -""" - -import logging -from itertools import chain -from pathlib import Path -from typing import Dict, Optional - -import pandas as pd -import tensorflow as tf -from psm_utils import Peptidoform, PSMList - -from ms2rescore.feature_generators.base import FeatureGeneratorBase, FeatureGeneratorException -from ms2rescore.parse_spectra import MSDataType - -try: - from ionmob import __file__ as ionmob_file - from ionmob.preprocess.data import to_tf_dataset_inference - from ionmob.utilities.chemistry import VARIANT_DICT, calculate_mz, reduced_mobility_to_ccs - from ionmob.utilities.tokenization import tokenizer_from_json - from ionmob.utilities.utility import get_ccs_shift -except ImportError: - IONMOB_INSTALLED = False -else: - IONMOB_INSTALLED = True - -logger = logging.getLogger(__name__) - -if IONMOB_INSTALLED: - IONMOB_DIR = Path(ionmob_file).parent - DEFAULT_MODELS_IONMOB = { - Path("pretrained_models/DeepTwoMerModel"), - Path("pretrained_models/GRUPredictor"), - Path("pretrained_models/SqrtModel"), - } - DEFAULT_MODELS_DICT = { - mod_path.stem: IONMOB_DIR / mod_path for mod_path in DEFAULT_MODELS_IONMOB - } - DEFAULT_TOKENIZER = IONMOB_DIR / "pretrained_models/tokenizers/tokenizer.json" - DEFAULT_REFERENCE_DATASET = IONMOB_DIR / "example_data/Tenzer_unimod.parquet" - - -class IonMobFeatureGenerator(FeatureGeneratorBase): - """Ionmob collisional cross section (CCS)-based feature generator.""" - - required_ms_data = {MSDataType.ion_mobility} - - def __init__( - self, - *args, - ionmob_model: str = "GRUPredictor", - reference_dataset: Optional[str] = None, - tokenizer: Optional[str] = None, - **kwargs, - ) -> None: - """ - Ionmob collisional cross section (CCS)-based feature generator. - - Parameters - ---------- - *args - Additional arguments passed to the base class. - ionmob_model - Path to a trained Ionmob model or one of the default models (``DeepTwoMerModel``, - ``GRUPredictor``, or ``SqrtModel``). Default: ``GRUPredictor``. - reference_dataset - Path to a reference dataset for CCS shift calculation. Uses the default reference - dataset if not specified. - tokenizer - Path to a tokenizer or one of the default tokenizers. Uses the default tokenizer if - not specified. - **kwargs - Additional keyword arguments passed to the base class. - - """ - super().__init__(*args, **kwargs) - - # Check if Ionmob could be imported - if not IONMOB_INSTALLED: - raise ImportError( - "Ionmob not installed. Please install Ionmob to use this feature generator." - ) - - # Get model from file or one of the default models - if Path(ionmob_model).is_file(): - self.ionmob_model = tf.keras.models.load_model(ionmob_model) - elif ionmob_model in DEFAULT_MODELS_DICT: - self.ionmob_model = tf.keras.models.load_model( - DEFAULT_MODELS_DICT[ionmob_model].as_posix() - ) - else: - raise IonmobException( - f"Invalid Ionmob model: {ionmob_model}. Should be path to a model file or one of " - f"the default models: {DEFAULT_MODELS_DICT.keys()}." - ) - self.reference_dataset = pd.read_parquet(reference_dataset or DEFAULT_REFERENCE_DATASET) - self.tokenizer = tokenizer_from_json(tokenizer or DEFAULT_TOKENIZER) - - self._verbose = logger.getEffectiveLevel() <= logging.DEBUG - - @property - def feature_names(self): - return [ - "ccs_predicted", - "ccs_observed", - "ccs_error", - "abs_ccs_error", - "perc_ccs_error", - ] - - @property - def allowed_modifications(self): - """Return a list of modifications that are allowed in ionmob.""" - return [token for aa_tokens in VARIANT_DICT.values() for token in aa_tokens] - - def add_features(self, psm_list: PSMList) -> None: - """ - Add Ionmob-derived features to PSMs. - - Parameters - ---------- - psm_list - PSMs to add features to. - - """ - logger.info("Adding Ionmob-derived features to PSMs.") - psm_dict = psm_list.get_psm_dict() - current_run = 1 - total_runs = sum(len(runs) for runs in psm_dict.values()) - - for runs in psm_dict.values(): - for run, psms in runs.items(): - logger.info( - f"Running Ionmob for PSMs from run ({current_run}/{total_runs}): `{run}`..." - ) - - psm_list_run = PSMList(psm_list=list(chain.from_iterable(psms.values()))) - psm_list_run_df = psm_list_run.to_dataframe() - - # prepare data frames for CCS prediction - psm_list_run_df["charge"] = [ - peptidoform.precursor_charge for peptidoform in psm_list_run_df["peptidoform"] - ] - psm_list_run_df = psm_list_run_df[ - psm_list_run_df["charge"] < 5 - ] # predictions do not go higher for ionmob - - psm_list_run_df["sequence-tokenized"] = psm_list_run_df.apply( - lambda x: self.tokenize_peptidoform(x["peptidoform"]), axis=1 - ) - psm_list_run_df = psm_list_run_df[ - psm_list_run_df.apply( - lambda x: self._is_valid_tokenized_sequence(x["sequence-tokenized"]), - axis=1, - ) - ] - - # TODO: Use observed m/z? - psm_list_run_df["mz"] = psm_list_run_df.apply( - lambda x: calculate_mz(x["sequence-tokenized"], x["charge"]), axis=1 - ) # use precursor m/z from PSMs? - - psm_list_run_df["ccs_observed"] = psm_list_run_df.apply( - lambda x: reduced_mobility_to_ccs(x["ion_mobility"], x["mz"], x["charge"]), - axis=1, - ) - # calibrate CCS values - shift_factor = self.calculate_ccs_shift(psm_list_run_df) - psm_list_run_df["ccs_observed"] + shift_factor - - # predict CCS values - tf_ds = to_tf_dataset_inference( - psm_list_run_df["mz"], - psm_list_run_df["charge"], - psm_list_run_df["sequence-tokenized"], - self.tokenizer, - ) - - psm_list_run_df["ccs_predicted"], _ = self.ionmob_model.predict(tf_ds) - - # calculate CCS features - ccs_features = self._calculate_features(psm_list_run_df) - - # add CCS features to PSMs - for psm in psm_list_run: - try: - psm["rescoring_features"].update(ccs_features[psm.spectrum_id]) - except KeyError: - psm["rescoring_features"].update({}) - current_run += 1 - - def _calculate_features(self, feature_df: pd.DataFrame) -> Dict[str, Dict[str, float]]: - """Get CCS features for PSMs.""" - ccs_features = {} - for row in feature_df.itertuples(): - ccs_features[row.spectrum_id] = { - "ccs_predicted": row.ccs_predicted, - "ccs_observed": row.ccs_observed, - "ccs_error": row.ccs_observed - row.ccs_predicted, - "abs_ccs_error": abs(row.ccs_observed - row.ccs_predicted), - "perc_ccs_error": ((abs(row.ccs_observed - row.ccs_predicted)) / row.ccs_observed) - * 100, - } - return ccs_features - - @staticmethod - def tokenize_peptidoform(peptidoform: Peptidoform) -> list: - """Tokenize proforma sequence and add modifications.""" - tokenized_seq = [] - - if peptidoform.properties["n_term"]: - tokenized_seq.append( - f"[UNIMOD:{peptidoform.properties['n_term'][0].definition['id']}]" - ) - else: - tokenized_seq.append("") - - for amino_acid, modification in peptidoform.parsed_sequence: - tokenized_seq.append(amino_acid) - if modification: - tokenized_seq[-1] = ( - tokenized_seq[-1] + f"[UNIMOD:{modification[0].definition['id']}]" - ) - - if peptidoform.properties["c_term"]: - pass # provide if c-term mods are supported - else: - tokenized_seq.append("") - - return tokenized_seq - - def calculate_ccs_shift(self, psm_dataframe: pd.DataFrame) -> float: - """ - Apply CCS shift to CCS values. - - Parameters - ---------- - psm_dataframe - Dataframe with PSMs as returned by :py:meth:`psm_utils.PSMList.to_dataframe`. - - """ - df = psm_dataframe.copy() - df.rename({"ccs_observed": "ccs"}, axis=1, inplace=True) - high_conf_hits = list(df["spectrum_id"][df["score"].rank(pct=True) > 0.95]) - logger.debug( - f"Number of high confidence hits for calculating shift: {len(high_conf_hits)}" - ) - - shift_factor = get_ccs_shift( - df[["charge", "sequence-tokenized", "ccs"]][df["spectrum_id"].isin(high_conf_hits)], - self.reference_dataset, - ) - - logger.debug(f"CCS shift factor: {shift_factor}") - - return shift_factor - - def _is_valid_tokenized_sequence(self, tokenized_seq): - """ - Check if peptide sequence contains invalid tokens. - - Parameters - ---------- - tokenized_seq - Tokenized peptide sequence. - - Returns - ------- - bool - False if invalid tokens are present, True otherwise. - - """ - for token in tokenized_seq: - if token not in self.allowed_modifications: - logger.debug(f"Invalid modification found: {token}") - return False - return True - - -class IonmobException(FeatureGeneratorException): - """Exception raised by Ionmob feature generator.""" - - pass diff --git a/ms2rescore/feature_generators/maxquant.py b/ms2rescore/feature_generators/maxquant.py deleted file mode 100644 index 45205c2..0000000 --- a/ms2rescore/feature_generators/maxquant.py +++ /dev/null @@ -1,208 +0,0 @@ -""" -Feature generator for PSMs from the MaxQuant search engine. - -MaxQuant msms.txt files contain various metrics from peptide-spectrum matching that can be used -to generate rescoring features. These include features related to the mass errors of the seven -fragment ions with the highest intensities, and features related to the ion current of the -identified fragment ions. - -""" - -import logging -from typing import List, Tuple - -import numpy as np -from psm_utils import PSMList - -from ms2rescore.exceptions import MS2RescoreError -from ms2rescore.feature_generators.base import FeatureGeneratorBase - -logger = logging.getLogger(__name__) - - -class MaxQuantFeatureGenerator(FeatureGeneratorBase): - """Generate MaxQuant-derived features.""" - - available_features = [ - "mean_error_top7", - "sq_mean_error_top7", - "stdev_error_top7", - "ln_explained_ion_current", - "ln_nterm_ion_current_ratio", - "ln_cterm_ion_current_ratio", - "ln_ms2_ion_current", - ] - - def __init__(self, *args, **kwargs) -> None: - """ - Generate MaxQuant-derived features. - - Attributes - ---------- - feature_names: list[str] - Names of the features that will be added to the PSMs. - - Raises - ------ - MissingMetadataError - If the required metadata entries are not present in the PSMs. - - """ - super().__init__(*args, **kwargs) - self._feature_names = self.available_features.copy() - - @property - def feature_names(self) -> List[str]: - return self._feature_names - - def add_features(self, psm_list: PSMList): - """ - Add MaxQuant-derived features to PSMs. - - Parameters - ---------- - psm_list - PSMs to add features to. - - """ - # Check if all PSMs are from MaxQuant - if not self._all_psms_from_maxquant(psm_list): - self._feature_names = [] # Set feature names to empty list to indicate none added - logger.warning("Not all PSMs are from MaxQuant. Skipping MaxQuant feature generation.") - return - else: - self._feature_names = self.available_features # Reset feature names - logger.info("Adding MaxQuant-derived features to PSMs.") - - # Infer mass deviations column name - for column_name in [ - "Mass deviations [Da]", - "Mass Deviations [Da]", - "Mass deviations [ppm]", - "Mass Deviations [ppm]", - ]: - if column_name in psm_list[0]["metadata"].keys(): - self._mass_deviations_key = column_name - break - else: - raise MissingMetadataError( - "No mass deviations entry in PSM metadata. Cannot compute MaxQuant features." - ) - - # Check other columns - for column_name in ["Intensities", "Matches", "Intensity coverage"]: - if column_name not in psm_list[0]["metadata"].keys(): - raise MissingMetadataError( - f"Missing {column_name} entry in PSM metadata. Cannot compute MaxQuant features." - ) - - # Add features to PSMs - for psm in psm_list: - psm["rescoring_features"].update(self._compute_features(psm["metadata"])) - - @staticmethod - def _all_psms_from_maxquant(psm_list): - """Check if the PSMs are from MaxQuant.""" - return (psm_list["source"] == "msms").all() - - def _compute_features(self, psm_metadata): - """Compute features from derived from intensities and mass errors.""" - features = {} - if all(k in psm_metadata.keys() for k in ["Intensities", self._mass_deviations_key]): - ( - features["mean_error_top7"], - features["sq_mean_error_top7"], - features["stdev_error_top7"], - ) = self._calculate_top7_peak_features( - psm_metadata["Intensities"], psm_metadata[self._mass_deviations_key] - ) - - if all(k in psm_metadata.keys() for k in ["Intensities", "Matches", "Intensity coverage"]): - ( - features["ln_explained_ion_current"], - features["ln_nterm_ion_current_ratio"], - features["ln_cterm_ion_current_ratio"], - features["ln_ms2_ion_current"], - ) = self._calculate_ion_current_features( - psm_metadata["Matches"], - psm_metadata["Intensities"], - psm_metadata["Intensity coverage"], - ) - - return features - - @staticmethod - def _calculate_top7_peak_features(intensities: str, mass_errors: str) -> Tuple[np.ndarray]: - """ - Calculate "top 7 peak"-related search engine features. - The following features are calculated: - - mean_error_top7: Mean of mass errors of the seven fragment ion peaks with the - highest intensities - - sq_mean_error_top7: Squared MeanErrorTop7 - - stdev_error_top7: Standard deviation of mass errors of the seven fragment ion - peaks with the highest intensities - """ - try: - intensities = [float(i) for i in intensities.split(";")] - mass_errors = [float(i) for i in mass_errors.split(";")] - except ValueError: - return 0.0, 0.0, 0.0 - - indices_most_intens = np.array(intensities).argsort()[-1:-8:-1] - mass_errors_top7 = [(mass_errors[i]) for i in indices_most_intens] - mean_error_top7 = np.mean(mass_errors_top7) - sq_mean_error_top7 = mean_error_top7**2 - stdev_error_top7 = np.std(mass_errors_top7) - - return mean_error_top7, sq_mean_error_top7, stdev_error_top7 - - @staticmethod - def _calculate_ion_current_features( - matches: str, intensities: str, intensity_coverage: str - ) -> Tuple[np.ndarray]: - """ - Calculate ion current related search engine features. - The following features are calculated: - - ln_explained_ion_current: Summed intensity of identified fragment ions, - divided by that of all fragment ions, logged - - ln_nterm_ion_current_ratio: Summed intensity of identified N-terminal - fragments, divided by that of all identified fragments, logged - - ln_cterm_ion_current_ratio: Summed intensity of identified N-terminal - fragments, divided by that of all identified fragments, logged - - ln_ms2_ion_current: Summed intensity of all observed fragment ions, logged - """ - pseudo_count = 0.00001 - try: - ln_explained_ion_current = float(intensity_coverage) + pseudo_count - summed_intensities = sum([float(i) for i in intensities.split(";")]) - except ValueError: - return 0.0, 0.0, 0.0, 0.0 - - # Calculate ratio between matched b- and y-ion intensities - y_ion_int = sum( - [ - float(intensities.split(";")[i]) - for i, m in enumerate(matches.split(";")) - if m.startswith("y") - ] - ) - y_int_ratio = y_ion_int / summed_intensities - - ln_nterm_ion_current_ratio = (y_int_ratio + pseudo_count) * ln_explained_ion_current - ln_cterm_ion_current_ratio = (1 - y_int_ratio + pseudo_count) * ln_explained_ion_current - ln_ms2_ion_current = summed_intensities / ln_explained_ion_current - - out = [ - ln_explained_ion_current, - ln_nterm_ion_current_ratio, - ln_cterm_ion_current_ratio, - ln_ms2_ion_current, - ] - - return tuple([np.log(x) for x in out]) - - -class MissingMetadataError(MS2RescoreError): - """Exception raised when a required metadata entry is missing.""" - - pass diff --git a/ms2rescore/feature_generators/ms2.py b/ms2rescore/feature_generators/ms2.py new file mode 100644 index 0000000..39ddbec --- /dev/null +++ b/ms2rescore/feature_generators/ms2.py @@ -0,0 +1,104 @@ +""" +MS2-based feature generator. + +""" + +import logging + +from typing import List, Optional + +from psm_utils import PSMList +from ms2rescore_rs import ms2_features_from_ms2spectra +from ms2rescore.feature_generators.base import FeatureGeneratorBase + +logger = logging.getLogger(__name__) + + +class MS2FeatureGenerator(FeatureGeneratorBase): + """DeepLC retention time-based feature generator.""" + + def __init__( + self, + *args, + spectrum_path: Optional[str] = None, + spectrum_id_pattern: str = "(.*)", + fragmentation_model: str = "cidhcd", + mass_mode: str = "monoisotopic", + processes: int = 1, + calculate_hyperscore: bool = True, + **kwargs, + ) -> None: + """ + Generate MS2-based features for rescoring. + + Parameters + ---------- + spectrum_path + Path to spectrum file or directory with spectrum files. If None, inferred from ``run`` + field in PSMs. Defaults to :py:const:`None`. + spectrum_id_pattern : str, optional + Regular expression pattern to extract spectrum ID from spectrum file. Defaults to + :py:const:`.*`. + fragmentation_model + Fragmentation model to use for theoretical spectrum generation. Defaults to + :py:const:`cidhcd` (b and y ions). + mass_mode + Mass mode to use for theoretical spectrum generation. Defaults to + :py:const:`monoisotopic`. + processes : int, optional + Number of processes to use for feature generation. Defaults to 1. + calculate_hyperscore : bool, optional + Whether to calculate hyperscore feature. Defaults to True. + + Attributes + ---------- + feature_names: list[str] + Names of the features that will be added to the PSMs. + + """ + super().__init__(*args, **kwargs) + + self.spectrum_path = spectrum_path + self.spectrum_id_pattern = spectrum_id_pattern + self.fragmentation_model = fragmentation_model.lower() + self.mass_mode = mass_mode.lower() + self.processes = processes + self.calculate_hyperscore = calculate_hyperscore + + @property + def feature_names(self) -> List[str]: + return [ + "ln_explained_intensity", + "ln_total_intensity", + "ln_explained_intensity_ratio", + "ln_explained_b_ion_ratio", + "ln_explained_y_ion_ratio", + "longest_b_ion_sequence", + "longest_y_ion_sequence", + "matched_b_ions", + "matched_b_ions_pct", + "matched_y_ions", + "matched_y_ions_pct", + "matched_ions_pct", + "hyperscore", + ] + + def add_features(self, psm_list: PSMList) -> None: + logger.info("Adding MS2-derived features to PSMs.") + + spectra = psm_list["spectrum"] + # Keep parity with your current behavior: + proformas = [psm.peptidoform.proforma.split("/")[0] for psm in psm_list] + seq_lens = [len(psm.peptidoform.sequence) for psm in psm_list] + + feature_dicts = ms2_features_from_ms2spectra( + spectra=spectra, + proformas=proformas, + seq_lens=seq_lens, + fragmentation_model=self.fragmentation_model, + mass_mode=self.mass_mode, + calculate_hyperscore=self.calculate_hyperscore, + ) + + for psm, feats in zip(psm_list, feature_dicts): + psm.rescoring_features.update(feats) diff --git a/ms2rescore/feature_generators/ms2pip.py b/ms2rescore/feature_generators/ms2pip.py index c830a79..b51a9dd 100644 --- a/ms2rescore/feature_generators/ms2pip.py +++ b/ms2rescore/feature_generators/ms2pip.py @@ -24,23 +24,15 @@ """ import logging -import multiprocessing -import os -import warnings -from itertools import chain -from typing import List, Optional, Union +from typing import Optional + +from ms2pip import process_MS2_spectra +from ms2rescore_rs import ms2pip_features_from_prediction_peak_arrays -import numpy as np -import pandas as pd -from ms2pip import correlate -from ms2pip.exceptions import NoMatchingSpectraFound -from ms2pip.result import ProcessingResult from psm_utils import PSMList -from rich.progress import track -from ms2rescore.feature_generators.base import FeatureGeneratorBase, FeatureGeneratorException +from ms2rescore.feature_generators.base import FeatureGeneratorBase from ms2rescore.parse_spectra import MSDataType -from ms2rescore.utils import infer_spectrum_path logger = logging.getLogger(__name__) @@ -58,7 +50,7 @@ def __init__( spectrum_path: Optional[str] = None, spectrum_id_pattern: str = "(.*)", model_dir: Optional[str] = None, - processes: 1, + processes: int = 1, **kwargs, ) -> None: """ @@ -182,213 +174,35 @@ def add_features(self, psm_list: PSMList) -> None: """ logger.info("Adding MS²PIP-derived features to PSMs.") - psm_dict = psm_list.get_psm_dict() - current_run = 1 - total_runs = sum(len(runs) for runs in psm_dict.values()) - - for runs in psm_dict.values(): - for run, psms in runs.items(): - logger.info( - f"Running MS²PIP for PSMs from run ({current_run}/{total_runs}) `{run}`..." - ) - psm_list_run = PSMList(psm_list=list(chain.from_iterable(psms.values()))) - spectrum_filename = infer_spectrum_path(self.spectrum_path, run) - logger.debug(f"Using spectrum file `{spectrum_filename}`") - try: - os.environ.pop("CUDA_VISIBLE_DEVICES", None) - ms2pip_results = correlate( - psms=psm_list_run, - spectrum_file=str(spectrum_filename), - spectrum_id_pattern=self.spectrum_id_pattern, - model=self.model, - ms2_tolerance=self.ms2_tolerance, - compute_correlations=False, - model_dir=self.model_dir, - processes=self.processes, - ) - except NoMatchingSpectraFound as e: - raise FeatureGeneratorException( - f"Could not find any matching spectra for PSMs from run `{run}`. " - "Please check that the `spectrum_id_pattern` and `psm_id_pattern` " - "options are configured correctly. See " - "https://ms2rescore.readthedocs.io/en/latest/userguide/configuration/#mapping-psms-to-spectra" - " for more information." - ) from e - self._calculate_features(psm_list_run, ms2pip_results) - current_run += 1 - - def _calculate_features( - self, psm_list: PSMList, ms2pip_results: List[ProcessingResult] - ) -> None: - """Calculate features from all MS²PIP results and add to PSMs.""" - logger.debug("Calculating features from predicted spectra") - with multiprocessing.Pool(int(self.processes)) as pool: - # Use imap, so we can use a progress bar - counts_failed = 0 - for result, features in zip( - ms2pip_results, - track( - pool.imap(self._calculate_features_single, ms2pip_results, chunksize=1000), - total=len(ms2pip_results), - description="Calculating features...", - transient=True, - ), - ): - if features: - # Cannot use result.psm directly, as it is a copy from MS²PIP multiprocessing - try: - psm_list[result.psm_index]["rescoring_features"].update(features) - except (AttributeError, TypeError): - psm_list[result.psm_index]["rescoring_features"] = features - else: - counts_failed += 1 - - if counts_failed > 0: - logger.warning(f"Failed to calculate features for {counts_failed} PSMs") - - def _calculate_features_single(self, processing_result: ProcessingResult) -> Union[dict, None]: - """Calculate MS²PIP-based features for single PSM.""" - if ( - processing_result.observed_intensity is None - or processing_result.predicted_intensity is None - ): - return None - - # Suppress RuntimeWarnings about invalid values - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Convert intensities to arrays - target_b = processing_result.predicted_intensity["b"].clip(np.log2(0.001)) - target_y = processing_result.predicted_intensity["y"].clip(np.log2(0.001)) - target_all = np.concatenate([target_b, target_y]) - prediction_b = processing_result.observed_intensity["b"].clip(np.log2(0.001)) - prediction_y = processing_result.observed_intensity["y"].clip(np.log2(0.001)) - prediction_all = np.concatenate([prediction_b, prediction_y]) - - # Prepare 'unlogged' intensity arrays - target_b_unlog = 2**target_b - 0.001 - target_y_unlog = 2**target_y - 0.001 - target_all_unlog = 2**target_all - 0.001 - prediction_b_unlog = 2**prediction_b - 0.001 - prediction_y_unlog = 2**prediction_y - 0.001 - prediction_all_unlog = 2**prediction_all - 0.001 - - # Calculate absolute differences - abs_diff_b = np.abs(target_b - prediction_b) - abs_diff_y = np.abs(target_y - prediction_y) - abs_diff_all = np.abs(target_all - prediction_all) - abs_diff_b_unlog = np.abs(target_b_unlog - prediction_b_unlog) - abs_diff_y_unlog = np.abs(target_y_unlog - prediction_y_unlog) - abs_diff_all_unlog = np.abs(target_all_unlog - prediction_all_unlog) - - # Compute features - feature_values = [ - # Features between spectra in log space - np.corrcoef(target_all, prediction_all)[0][1], # Pearson all ions - np.corrcoef(target_b, prediction_b)[0][1], # Pearson b ions - np.corrcoef(target_y, prediction_y)[0][1], # Pearson y ions - _mse(target_all, prediction_all), # MSE all ions - _mse(target_b, prediction_b), # MSE b ions - _mse(target_y, prediction_y), # MSE y ions - np.min(abs_diff_all), # min_abs_diff_norm - np.max(abs_diff_all), # max_abs_diff_norm - np.quantile(abs_diff_all, 0.25), # abs_diff_Q1_norm - np.quantile(abs_diff_all, 0.5), # abs_diff_Q2_norm - np.quantile(abs_diff_all, 0.75), # abs_diff_Q3_norm - np.mean(abs_diff_all), # mean_abs_diff_norm - np.std(abs_diff_all), # std_abs_diff_norm - np.min(abs_diff_b), # ionb_min_abs_diff_norm - np.max(abs_diff_b), # ionb_max_abs_diff_norm - np.quantile(abs_diff_b, 0.25), # ionb_abs_diff_Q1_norm - np.quantile(abs_diff_b, 0.5), # ionb_abs_diff_Q2_norm - np.quantile(abs_diff_b, 0.75), # ionb_abs_diff_Q3_norm - np.mean(abs_diff_b), # ionb_mean_abs_diff_norm - np.std(abs_diff_b), # ionb_std_abs_diff_norm - np.min(abs_diff_y), # iony_min_abs_diff_norm - np.max(abs_diff_y), # iony_max_abs_diff_norm - np.quantile(abs_diff_y, 0.25), # iony_abs_diff_Q1_norm - np.quantile(abs_diff_y, 0.5), # iony_abs_diff_Q2_norm - np.quantile(abs_diff_y, 0.75), # iony_abs_diff_Q3_norm - np.mean(abs_diff_y), # iony_mean_abs_diff_norm - np.std(abs_diff_y), # iony_std_abs_diff_norm - np.dot(target_all, prediction_all), # Dot product all ions - np.dot(target_b, prediction_b), # Dot product b ions - np.dot(target_y, prediction_y), # Dot product y ions - _cosine_similarity(target_all, prediction_all), # Cos similarity all ions - _cosine_similarity(target_b, prediction_b), # Cos similarity b ions - _cosine_similarity(target_y, prediction_y), # Cos similarity y ions - # Same features in normal space - np.corrcoef(target_all_unlog, prediction_all_unlog)[0][1], # Pearson all - np.corrcoef(target_b_unlog, prediction_b_unlog)[0][1], # Pearson b - np.corrcoef(target_y_unlog, prediction_y_unlog)[0][1], # Pearson y - _spearman(target_all_unlog, prediction_all_unlog), # Spearman all ions - _spearman(target_b_unlog, prediction_b_unlog), # Spearman b ions - _spearman(target_y_unlog, prediction_y_unlog), # Spearman y ions - _mse(target_all_unlog, prediction_all_unlog), # MSE all ions - _mse(target_b_unlog, prediction_b_unlog), # MSE b ions - _mse(target_y_unlog, prediction_y_unlog), # MSE y ions, - # Ion type with min absolute difference - 0 if np.min(abs_diff_b_unlog) <= np.min(abs_diff_y_unlog) else 1, - # Ion type with max absolute difference - 0 if np.max(abs_diff_b_unlog) >= np.max(abs_diff_y_unlog) else 1, - np.min(abs_diff_all_unlog), # min_abs_diff - np.max(abs_diff_all_unlog), # max_abs_diff - np.quantile(abs_diff_all_unlog, 0.25), # abs_diff_Q1 - np.quantile(abs_diff_all_unlog, 0.5), # abs_diff_Q2 - np.quantile(abs_diff_all_unlog, 0.75), # abs_diff_Q3 - np.mean(abs_diff_all_unlog), # mean_abs_diff - np.std(abs_diff_all_unlog), # std_abs_diff - np.min(abs_diff_b_unlog), # ionb_min_abs_diff - np.max(abs_diff_b_unlog), # ionb_max_abs_diff_norm - np.quantile(abs_diff_b_unlog, 0.25), # ionb_abs_diff_Q1 - np.quantile(abs_diff_b_unlog, 0.5), # ionb_abs_diff_Q2 - np.quantile(abs_diff_b_unlog, 0.75), # ionb_abs_diff_Q3 - np.mean(abs_diff_b_unlog), # ionb_mean_abs_diff - np.std(abs_diff_b_unlog), # ionb_std_abs_diff - np.min(abs_diff_y_unlog), # iony_min_abs_diff - np.max(abs_diff_y_unlog), # iony_max_abs_diff - np.quantile(abs_diff_y_unlog, 0.25), # iony_abs_diff_Q1 - np.quantile(abs_diff_y_unlog, 0.5), # iony_abs_diff_Q2 - np.quantile(abs_diff_y_unlog, 0.75), # iony_abs_diff_Q3 - np.mean(abs_diff_y_unlog), # iony_mean_abs_diff - np.std(abs_diff_y_unlog), # iony_std_abs_diff - np.dot(target_all_unlog, prediction_all_unlog), # Dot product all ions - np.dot(target_b_unlog, prediction_b_unlog), # Dot product b ions - np.dot(target_y_unlog, prediction_y_unlog), # Dot product y ions - _cosine_similarity(target_all_unlog, prediction_all_unlog), # Cos similarity all - _cosine_similarity(target_b_unlog, prediction_b_unlog), # Cos similarity b ions - _cosine_similarity(target_y_unlog, prediction_y_unlog), # Cos similarity y ions - ] - - features = dict( - zip( - self.feature_names, - [0.0 if np.isnan(ft) else ft for ft in feature_values], - ) + ms2pip_results = process_MS2_spectra( + psms=psm_list, + model=self.model, + model_dir=self.model_dir, + processes=self.processes, ) - - return features - - -def _spearman(x: np.ndarray, y: np.ndarray) -> float: - """Spearman rank correlation.""" - x = np.array(x) - y = np.array(y) - x_rank = pd.Series(x).rank() - y_rank = pd.Series(y).rank() - return np.corrcoef(x_rank, y_rank)[0][1] - - -def _mse(x: np.ndarray, y: np.ndarray) -> float: - """Mean squared error""" - x = np.array(x) - y = np.array(y) - return np.mean((x - y) ** 2) - - -def _cosine_similarity(x: np.ndarray, y: np.ndarray) -> float: - """Cosine similarity""" - x = np.array(x) - y = np.array(y) - return np.dot(x, y) / (np.linalg.norm(x, 2) * np.linalg.norm(y, 2)) + self._calculate_features(psm_list, ms2pip_results) + + def _calculate_features(self, psm_list, ms2pip_results): + idx = [] + pred_b = [] + pred_y = [] + obs_b = [] + obs_y = [] + + for r in ms2pip_results: + if r.observed_intensity is None or r.predicted_intensity is None: + continue + idx.append(r.psm_index) + pred_b.append(r.predicted_intensity["b"]) + pred_y.append(r.predicted_intensity["y"]) + obs_b.append(r.observed_intensity["b"]) + obs_y.append(r.observed_intensity["y"]) + + results = ms2pip_features_from_prediction_peak_arrays(idx, pred_b, pred_y, obs_b, obs_y) + + for psm_index, feats in results: + if feats: + try: + psm_list[psm_index]["rescoring_features"].update(feats) + except (AttributeError, TypeError): + psm_list[psm_index]["rescoring_features"] = feats diff --git a/ms2rescore/gui/__main__.py b/ms2rescore/gui/__main__.py index 583a856..aa0724b 100644 --- a/ms2rescore/gui/__main__.py +++ b/ms2rescore/gui/__main__.py @@ -1,5 +1,6 @@ """Entrypoint for MS²Rescore GUI.""" +import contextlib import multiprocessing import os import sys diff --git a/ms2rescore/gui/app.py b/ms2rescore/gui/app.py index bc781c8..3f4cbcf 100644 --- a/ms2rescore/gui/app.py +++ b/ms2rescore/gui/app.py @@ -66,10 +66,6 @@ "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", @@ -514,16 +510,12 @@ def __init__(self, *args, **kwargs): self.im2deep_config = Im2DeepConfiguration(self) self.im2deep_config.grid(row=3, column=0, pady=(0, 20), sticky="nsew") - self.ionmob_config = IonmobConfiguration(self) - self.ionmob_config.grid(row=4, column=0, pady=(0, 20), sticky="nsew") - def get(self) -> Dict: """Return the configuration as a dictionary.""" basic_enabled, basic_config = self.basic_config.get() ms2pip_enabled, ms2pip_config = self.ms2pip_config.get() deeplc_enabled, deeplc_config = self.deeplc_config.get() im2deep_enabled, im2deep_config = self.im2deep_config.get() - ionmob_enabled, ionmob_config = self.ionmob_config.get() config = {} if basic_enabled: @@ -532,8 +524,6 @@ def get(self) -> Dict: config["ms2pip"] = ms2pip_config if deeplc_enabled: config["deeplc"] = deeplc_config - if ionmob_enabled: - config["ionmob"] = ionmob_config if im2deep_enabled: config["im2deep"] = im2deep_config @@ -653,35 +643,6 @@ def get(self) -> Dict: return enabled, config -class IonmobConfiguration(ctk.CTkFrame): - def __init__(self, *args, **kwargs): - """IonMob configuration frame.""" - super().__init__(*args, **kwargs) - - self.configure(fg_color="transparent") - self.grid_columnconfigure(0, weight=1) - - 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) - self.enabled.grid(row=1, column=0, pady=(0, 10), sticky="nsew") - - self.model = widgets.LabeledEntry( - self, - label="Name of built-in model or path to custom model", - placeholder_text="GRUPredictor", - default_value="GRUPredictor", - ) - self.model.grid(row=3, column=0, pady=(0, 10), sticky="nsew") - - def get(self) -> Tuple[bool, Dict[str, Any]]: - """Return the configuration as a dictionary.""" - enabled = self.enabled.get() - config = {"ionmob_model": self.model.get()} - return enabled, config - - class Im2DeepConfiguration(ctk.CTkFrame): def __init__(self, *args, **kwargs): """IM2Deep configuration frame.""" @@ -904,7 +865,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.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": diff --git a/ms2rescore/package_data/config_default.json b/ms2rescore/package_data/config_default.json index 805be2c..29042e9 100644 --- a/ms2rescore/package_data/config_default.json +++ b/ms2rescore/package_data/config_default.json @@ -12,6 +12,7 @@ }, "maxquant": {} }, + "psm_generator": {}, "rescoring_engine": { "mokapot": { "train_fdr": 0.01, diff --git a/ms2rescore/package_data/config_schema.json b/ms2rescore/package_data/config_schema.json index 3840a9e..c56edbc 100644 --- a/ms2rescore/package_data/config_schema.json +++ b/ms2rescore/package_data/config_schema.json @@ -62,6 +62,18 @@ "mokapot": {} } }, + "psm_generator": { + "description": "PSM generator and their configuration.", + "type": "object", + "minProperties": 0, + "maxProperties": 1, + "patternProperties": { + ".*": { "$ref": "#/definitions/psm_generator" }, + "mumble": { + "$ref": "#/definitions/mumble" + } + } + }, "config_file": { "description": "Path to configuration file", "oneOf": [{ "type": "string" }, { "type": "null" }] @@ -189,6 +201,7 @@ "oneOf": [{ "type": "boolean" }, { "type": "null" }], "default": false } + } } }, @@ -203,6 +216,11 @@ "type": "object", "additionalProperties": true }, + "psm_generator": { + "description": "Additional PSM feature generator configuration", + "type": "object", + "additionalProperties": true + }, "basic": { "$ref": "#/definitions/feature_generator", "description": "Basic feature generator configuration", @@ -283,6 +301,30 @@ } } }, + "mumble": { + "$ref": "#/definitions/psm_generator", + "description": "Mumble PSM generator configuration using Mubmle", + "type": "object", + "additionalProperties": true, + "properties": { + "aa_combinations": { + "description": "Additional amino acid combinations to consider as mass shift", + "type": "integer", + "default": 0 + }, + "fasta_file": { + "description": "Maximum number of modifications per peptide", + "oneOf": [{ "type": "string" }, { "type": "null" }], + "default": false + }, + "mass_error": { + "description": "mass error in Da", + "type": "number", + "default": 0.2 + } + } + }, + "mokapot": { "$ref": "#/definitions/rescoring_engine", "description": "Mokapot rescoring engine configuration. Additional properties are passed to the Mokapot brew function.", diff --git a/ms2rescore/parse_psms.py b/ms2rescore/parse_psms.py index 65b3dcd..0f5df3c 100644 --- a/ms2rescore/parse_psms.py +++ b/ms2rescore/parse_psms.py @@ -84,7 +84,9 @@ def parse_psms(config: Dict, psm_list: Union[PSMList, None]) -> PSMList: { "before_rescoring_score": psm.score, "before_rescoring_qvalue": psm.qvalue, - "before_rescoring_pep": psm.pep, + "before_rescoring_pep": psm.pep + if psm.pep is not None + else float("nan"), # until fixed in psm_utils "before_rescoring_rank": psm.rank, } ) @@ -109,6 +111,41 @@ def parse_psms(config: Dict, psm_list: Union[PSMList, None]) -> PSMList: psm_list.add_fixed_modifications(config["fixed_modifications"]) psm_list.apply_fixed_modifications() + if config["psm_id_pattern"]: + pattern = re.compile(config["psm_id_pattern"]) + logger.debug("Applying 'psm_id_pattern'...") + logger.debug( + f"Parsing '{psm_list[0].spectrum_id}' to '{_match_psm_ids(psm_list[0].spectrum_id, pattern)}'" + ) + new_ids = [_match_psm_ids(old_id, pattern) for old_id in psm_list["spectrum_id"]] + psm_list["spectrum_id"] = new_ids + + # Addition of Modifications for mass shifts in the PSMs with Mumble + if "mumble" in config["psm_generator"]: + try: + from mumble import PSMHandler + except ImportError: + raise MS2RescoreConfigurationError( + "mumble is not installed. Please install it with: pip install ms2rescore[mumble]" + ) + logger.debug("Applying modifications for mass shifts using Mumble...") + # set inlcude original psm to True and include decoy psm to true + config["psm_generator"]["mumble"]["include_original_psm"] = True + config["psm_generator"]["mumble"]["include_decoy_psm"] = True + mumble_config = config["psm_generator"]["mumble"] + + # Check if psm_list[0].rescoring_features is empty or not + if psm_list[0].rescoring_features: + logger.debug("Removing psm_file rescoring features from PSMs...") + # psm_list.remove_rescoring_features() # TODO add this to psm_utils + for psm in psm_list: + psm.rescoring_features = {} + + psm_handler = PSMHandler( + **mumble_config, + ) + psm_list = psm_handler.add_modified_psms(psm_list) + return psm_list @@ -120,7 +157,7 @@ def _read_psms(config, psm_list): psm_list = [] for current_file, psm_file in enumerate(config["psm_file"]): logger.info( - f"Reading PSMs from PSM file ({current_file+1}/{total_files}): '{psm_file}'..." + f"Reading PSMs from PSM file ({current_file + 1}/{total_files}): '{psm_file}'..." ) psm_list.extend( psm_utils.io.read_file( @@ -186,7 +223,7 @@ def _parse_values_from_spectrum_id( ["retention_time", "ion_mobility"], ): if pattern: - logger.debug(f"Parsing {label} from spectrum_id with regex pattern " f"{pattern}") + logger.debug(f"Parsing {label} from spectrum_id with regex pattern {pattern}") try: pattern = re.compile(pattern) psm_list[key] = [ diff --git a/ms2rescore/parse_spectra.py b/ms2rescore/parse_spectra.py index 103619c..bedbe7d 100644 --- a/ms2rescore/parse_spectra.py +++ b/ms2rescore/parse_spectra.py @@ -3,11 +3,12 @@ import logging import re from enum import Enum -from itertools import chain -from typing import Optional, Set, Tuple +from typing import Optional, Set import numpy as np -from ms2rescore_rs import Precursor, get_precursor_info +from ms2rescore_rs import Precursor, get_ms2_spectra, MS2Spectrum +from rich.progress import track + from psm_utils import PSMList from ms2rescore.exceptions import MS2RescoreConfigurationError, MS2RescoreError @@ -24,7 +25,6 @@ class MSDataType(str, Enum): precursor_mz = "precursor m/z" ms2_spectra = "MS2 spectra" - # Mimic behavior of StrEnum (Python >=3.11) def __str__(self): return self.value @@ -69,7 +69,7 @@ def add_precursor_values( # Check which data types are missing # Missing if: all values are 0, OR any values are None/NaN missing_data_types = set() - if spectrum_path is None: + if MSDataType.ms2_spectra in required_data_types: missing_data_types.add(MSDataType.ms2_spectra) rt_values = np.asarray(psm_list["retention_time"]) @@ -101,39 +101,67 @@ def add_precursor_values( "Spectrum path must be provided to parse precursor values that are not present in the" " PSM list." ) + else: + LOGGER.debug( + "Missing required data types: %s. Parsing from spectrum files.", + ", ".join(str(dt) for dt in data_types_to_parse), + ) # Get precursor values from spectrum files LOGGER.info("Parsing precursor info from spectrum files...") - mz, rt, im = _get_precursor_values(psm_list, spectrum_path, spectrum_id_pattern) + _add_precursor_values(psm_list, spectrum_path, spectrum_id_pattern) + + # Extract precursor values from MS2 spectrum objects in a single pass + precursor_data = [ + (ms2.precursor.rt, ms2.precursor.im, ms2.precursor.mz) for ms2 in psm_list["spectrum"] + ] + rts, ims, mzs = map(np.array, zip(*precursor_data)) # Determine which data types were successfully found in spectrum files - # ms2rescore_rs always returns 0.0 for missing values - found_data_types = {MSDataType.ms2_spectra} # MS2 spectra available when processing files - if np.all(rt != 0.0): + found_data_types = {MSDataType.ms2_spectra} + + # Add found data types: if missing and all zeros, raise error + if not np.all(rts == 0.0): found_data_types.add(MSDataType.retention_time) - if np.all(im != 0.0): + if MSDataType.retention_time in data_types_to_parse: + LOGGER.debug( + "Missing retention time values in PSM list. Updating from spectrum files." + ) + psm_list["retention_time"] = rts + elif MSDataType.retention_time in data_types_to_parse: + raise SpectrumParsingError( + "Retention time values are required but not available in spectrum files " + "(all values are zero)." + ) + + if not np.all(ims == 0.0): found_data_types.add(MSDataType.ion_mobility) - if np.all(mz != 0.0): + if MSDataType.ion_mobility in data_types_to_parse: + LOGGER.debug("Missing ion mobility values in PSM list. Updating from spectrum files.") + psm_list["ion_mobility"] = ims + elif MSDataType.ion_mobility in data_types_to_parse: + raise SpectrumParsingError( + "Ion mobility values are required but not available in spectrum files " + "(all values are zero)." + ) + + if np.all(mzs == 0.0): found_data_types.add(MSDataType.precursor_mz) + if MSDataType.precursor_mz in data_types_to_parse: + LOGGER.debug("Missing precursor m/z values in PSM list. Updating from spectrum files.") + psm_list["precursor_mz"] = mzs + elif MSDataType.precursor_mz in data_types_to_parse: + raise SpectrumParsingError( + "Precursor m/z values are required but not available in spectrum files " + "(all values are zero)." + ) - # Update PSM list with missing precursor values that were found - update_types = data_types_to_parse & found_data_types - - if MSDataType.retention_time in update_types: - LOGGER.debug("Missing retention time values in PSM list. Updating from spectrum files.") - psm_list["retention_time"] = rt - if MSDataType.ion_mobility in update_types: - LOGGER.debug("Missing ion mobility values in PSM list. Updating from spectrum files.") - psm_list["ion_mobility"] = im - if MSDataType.precursor_mz in update_types: - LOGGER.debug("Missing precursor m/z values in PSM list. Updating from spectrum files.") - psm_list["precursor_mz"] = mz - elif ( + # Check if precursor m/z values are consistent between PSMs and spectrum files + if ( MSDataType.precursor_mz not in missing_data_types and MSDataType.precursor_mz in found_data_types ): - # Check if precursor m/z values are consistent between PSMs and spectrum files - mz_diff = np.abs(psm_list["precursor_mz"] - mz) + mz_diff = np.abs(psm_list["precursor_mz"] - mzs) if np.mean(mz_diff) > 1e-2: LOGGER.warning( "Mismatch between precursor m/z values in PSM list and spectrum files (mean " @@ -149,20 +177,23 @@ def add_precursor_values( return available_data_types -def _apply_spectrum_id_pattern( - precursors: dict[str, Precursor], pattern: str +def _acquire_observed_spectra_dict( + ms2: list[MS2Spectrum], pattern: str, spectrum_ids: list[str] ) -> dict[str, Precursor]: """Apply spectrum ID pattern to precursor IDs.""" # Map precursor IDs using regex pattern compiled_pattern = re.compile(pattern) - id_mapping = { - match.group(1): spectrum_id - for spectrum_id in precursors.keys() - if (match := compiled_pattern.search(spectrum_id)) is not None + spectrum_ids_set = set(spectrum_ids) # For faster lookup + + ms2_observed_spectra_mapping = { + match.group(1): ms2_spectrum + for ms2_spectrum in ms2 + if (match := compiled_pattern.search(str(ms2_spectrum.identifier))) is not None + and match.group(1) in spectrum_ids_set } # Validate that any IDs were matched - if not id_mapping: + if not ms2_observed_spectra_mapping: raise MS2RescoreConfigurationError( "'spectrum_id_pattern' did not match any spectrum-file IDs. Please check and try " "again. See " @@ -170,64 +201,33 @@ def _apply_spectrum_id_pattern( "for more information." ) - # Validate that the same number of unique IDs were matched - elif len(id_mapping) != len(precursors): - new_id, old_id = next(iter(id_mapping.items())) - raise MS2RescoreConfigurationError( - "'spectrum_id_pattern' resulted in a different number of unique spectrum IDs. This " - "indicates issues with the regex pattern. Please check and try again. " - f"Example old ID: '{old_id}' -> new ID: '{new_id}'. " - "See https://ms2rescore.readthedocs.io/en/stable/userguide/configuration/#mapping-psms-to-spectra " - "for more information." - ) - - precursors = {new_id: precursors[orig_id] for new_id, orig_id in id_mapping.items()} + return ms2_observed_spectra_mapping - return precursors - -def _get_precursor_values( +def _add_precursor_values( psm_list: PSMList, spectrum_path: str, spectrum_id_pattern: Optional[str] = None -) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: +) -> None: """Get precursor m/z, RT, and IM from spectrum files.""" # Iterate over different runs in PSM list - precursor_dict = dict() - psm_dict = psm_list.get_psm_dict() - for runs in psm_dict.values(): - for run_name, psms in runs.items(): - psm_list_run = PSMList(psm_list=list(chain.from_iterable(psms.values()))) - spectrum_file = infer_spectrum_path(spectrum_path, run_name) - - LOGGER.debug("Reading spectrum file: '%s'", spectrum_file) - precursors: dict[str, Precursor] = get_precursor_info(str(spectrum_file)) - - # Parse spectrum IDs with regex pattern if provided - if spectrum_id_pattern: - precursors = _apply_spectrum_id_pattern(precursors, spectrum_id_pattern) - - # Ensure all PSMs have precursor values - for psm in psm_list_run: - if psm.spectrum_id not in precursors: - raise MS2RescoreConfigurationError( - "Mismatch between PSM and spectrum file IDs. Could not find precursor " - f"values for PSM with ID {psm.spectrum_id} in run {run_name}.\n" - "Please check that the `spectrum_id_pattern` and `psm_id_pattern` options " - "are configured correctly. See " - "https://ms2rescore.readthedocs.io/en/stable/userguide/configuration/#mapping-psms-to-spectra" - " for more information.\n" - f"Example ID from PSM file: {psm.spectrum_id}\n" - f"Example ID from spectrum file: {list(precursors.keys())[0]}" - ) - - # Store precursor values in dictionary - precursor_dict[run_name] = precursors - - # Reshape precursor values into arrays matching PSM list - mzs = np.fromiter((precursor_dict[psm.run][psm.spectrum_id].mz for psm in psm_list), float) - rts = np.fromiter((precursor_dict[psm.run][psm.spectrum_id].rt for psm in psm_list), float) - ims = np.fromiter((precursor_dict[psm.run][psm.spectrum_id].im for psm in psm_list), float) - - return mzs, rts, ims + if spectrum_id_pattern is None: + spectrum_id_pattern = r"^(.*)$" # Match entire identifier if no pattern provided + + for run_name in track(set(psm_list["run"])): + run_mask = psm_list["run"] == run_name + psm_list_run = psm_list[run_mask] + spectrum_file = infer_spectrum_path(spectrum_path, run_name) + + LOGGER.debug("Reading spectrum file: '%s'", spectrum_file) + ms2_spectra: list[MS2Spectrum] = get_ms2_spectra(str(spectrum_file)) + + # Parse spectrum IDs with regex pattern if provided + ms2_spectra_dict = _acquire_observed_spectra_dict( + ms2_spectra, spectrum_id_pattern, psm_list_run["spectrum_id"] + ) + + psm_list_run["spectrum"] = [ + ms2_spectra_dict[spec_id] for spec_id in psm_list_run["spectrum_id"] + ] class SpectrumParsingError(MS2RescoreError): diff --git a/ms2rescore/report/__main__.py b/ms2rescore/report/__main__.py index 86fd5f2..6ab5e6d 100644 --- a/ms2rescore/report/__main__.py +++ b/ms2rescore/report/__main__.py @@ -1,6 +1,8 @@ import logging +from pathlib import Path import click +import psm_utils.io from rich.logging import RichHandler from ms2rescore.report.generate import generate_report @@ -9,8 +11,19 @@ @click.command() -@click.argument("output_prefix", type=str) -def main(**kwargs): +@click.argument("psm_file", type=click.Path(exists=True)) +@click.option( + "--output", + "-o", + type=click.Path(), + default=None, + help="Output path for the report HTML file. If not provided, will be based on PSM file name.", +) +def main(psm_file, output): + """Generate MS²Rescore report from a PSM TSV file. + + PSM_FILE: Path to the PSM TSV file (e.g., output.psms.tsv) + """ logging.getLogger("mokapot").setLevel(logging.WARNING) logging.basicConfig( level=logging.INFO, @@ -19,7 +32,38 @@ def main(**kwargs): ) try: - generate_report(kwargs["output_prefix"]) + psm_file_path = Path(psm_file) + + # Determine output path + if output: + output_path = Path(output) + else: + # Try to infer from ms2rescore naming convention + if ".ms2rescore.psms.tsv" in psm_file_path.name: + output_prefix = str(psm_file_path).replace(".psms.tsv", "") + else: + # Use the PSM file name without extension + output_prefix = str(psm_file_path.with_suffix("")) + output_path = Path(output_prefix + ".report.html") + + logger.info(f"Reading PSMs from {psm_file_path}...") + psm_list = psm_utils.io.read_file(psm_file_path, filetype="tsv", show_progressbar=True) + + logger.info("Generating report...") + # Try to infer output prefix for finding other files + if ".ms2rescore.psms.tsv" in psm_file_path.name: + output_prefix = str(psm_file_path).replace(".psms.tsv", "") + else: + output_prefix = str(psm_file_path.with_suffix("")) + + generate_report( + output_path_prefix=output_prefix, + psm_list=psm_list, + output_file=output_path, + ) + + logger.info(f"✓ Report generated: {output_path}") + except Exception as e: logger.exception(e) exit(1) diff --git a/ms2rescore/report/charts.py b/ms2rescore/report/charts.py index 1a24690..e1384bc 100644 --- a/ms2rescore/report/charts.py +++ b/ms2rescore/report/charts.py @@ -1,7 +1,9 @@ """Collection of Plotly-based charts for reporting results of MS²Rescore.""" +import importlib.resources import warnings from collections import defaultdict +from pathlib import Path from typing import Dict, List, Optional, Tuple, Union import mokapot @@ -639,3 +641,369 @@ def feature_ecdf_auc_bar( }, color_discrete_map=color_discrete_map, ) + + +def rt_scatter( + df: pd.DataFrame, + predicted_column: str = "Predicted retention time", + observed_column: str = "Observed retention time", + xaxis_label: str = "Observed retention time", + yaxis_label: str = "Predicted retention time", + plot_title: str = "Predicted vs. observed retention times", +) -> go.Figure: + """ + Plot a scatter plot of the predicted vs. observed retention times. + + Parameters + ---------- + df : pd.DataFrame + Dataframe containing the predicted and observed retention times. + predicted_column : str, optional + Name of the column containing the predicted retention times, by default + ``Predicted retention time``. + observed_column : str, optional + Name of the column containing the observed retention times, by default + ``Observed retention time``. + xaxis_label : str, optional + X-axis label, by default ``Observed retention time``. + yaxis_label : str, optional + Y-axis label, by default ``Predicted retention time``. + plot_title : str, optional + Scatter plot title, by default ``Predicted vs. observed retention times`` + + """ + # Draw scatter + fig = px.scatter( + df, + x=observed_column, + y=predicted_column, + opacity=0.3, + ) + + # Draw diagonal line + fig.add_scatter( + x=[min(df[observed_column]), max(df[observed_column])], + y=[min(df[observed_column]), max(df[observed_column])], + mode="lines", + line=dict(color="red", width=3, dash="dash"), + ) + + # Hide legend + fig.update_layout( + title=plot_title, + showlegend=False, + xaxis_title=xaxis_label, + yaxis_title=yaxis_label, + ) + + return fig + + +def rt_distribution_baseline( + df: pd.DataFrame, + predicted_column: str = "Predicted retention time", + observed_column: str = "Observed retention time", +) -> go.Figure: + """ + Plot a distribution plot of the relative mean absolute error of the current + DeepLC performance compared to the baseline performance. + + Parameters + ---------- + df : pd.DataFrame + Dataframe containing the predicted and observed retention times. + predicted_column : str, optional + Name of the column containing the predicted retention times, by default + ``Predicted retention time``. + observed_column : str, optional + Name of the column containing the observed retention times, by default + ``Observed retention time``. + + """ + # Get baseline data from deeplc package + try: + import deeplc.package_data + + baseline_path = ( + Path(importlib.resources.files(deeplc.package_data)) + / "baseline_performance" + / "baseline_predictions.csv" + ) + baseline_df = pd.read_csv(baseline_path) + except (ImportError, FileNotFoundError): + # If deeplc is not installed or baseline data not found, return empty figure + fig = go.Figure() + fig.add_annotation( + text="DeepLC baseline data not available. Install DeepLC to view performance comparison.", + showarrow=False, + ) + return fig + + baseline_df["rel_mae_best"] = baseline_df[ + ["rel_mae_transfer_learning", "rel_mae_new_model", "rel_mae_calibrate"] + ].min(axis=1) + baseline_df.fillna(0.0, inplace=True) + + # Calculate current RMAE and percentile compared to baseline + mae = sum(abs(df[observed_column] - df[predicted_column])) / len(df.index) + mae_rel = (mae / max(df[observed_column])) * 100 + percentile = round((baseline_df["rel_mae_transfer_learning"] < mae_rel).mean() * 100, 1) + + # Calculate x-axis range with 5% padding + all_values = np.append(baseline_df["rel_mae_transfer_learning"].values, mae_rel) + padding = (all_values.max() - all_values.min()) / 20 # 5% padding + x_min = all_values.min() - padding + x_max = all_values.max() + padding + + # Make labels human-readable + hover_label_mapping = { + "train_number": "Training dataset size", + "rel_mae_transfer_learning": "RMAE with transfer learning", + "rel_mae_new_model": "RMAE with new model from scratch", + "rel_mae_calibrate": "RMAE with calibrating existing model", + "rel_mae_best": "RMAE with best method", + } + label_mapping = hover_label_mapping.copy() + label_mapping.update({"Unnamed: 0": "Dataset"}) + + # Generate plot + fig = px.histogram( + data_frame=baseline_df, + x="rel_mae_best", + marginal="rug", + hover_data=hover_label_mapping.keys(), + hover_name="Unnamed: 0", + labels=label_mapping, + opacity=0.8, + ) + fig.add_vline( + x=mae_rel, + line_width=3, + line_dash="dash", + line_color="red", + annotation_text=f"Current performance (percentile {percentile}%)", + annotation_position="top left", + name="Current performance", + row=1, + ) + fig.update_xaxes(range=[x_min, x_max]) + fig.update_layout( + title=(f"Current DeepLC performance compared to {len(baseline_df.index)} datasets"), + xaxis_title="Relative mean absolute error (%)", + ) + + return fig + + +def score_scatter_plot_df( + psm_df: pd.DataFrame, + fdr_threshold: float = 0.01, +) -> go.Figure: + """ + Plot PSM scores before and after rescoring from a dataframe. + + Parameters + ---------- + psm_df + Dataframe with PSM information including score_before, score_after, + qvalue_before, qvalue_after, and is_decoy columns. + fdr_threshold + FDR threshold for drawing threshold lines. + + Returns + ------- + go.Figure + Plotly figure with score comparison. + """ + if "score_before" not in psm_df.columns or "score_after" not in psm_df.columns: + figure = go.Figure() + figure.add_annotation( + text="No before/after score data available for comparison.", + showarrow=False, + ) + return figure + + # Prepare data + plot_df = psm_df.copy() + plot_df["PSM type"] = plot_df["is_decoy"].map({True: "decoy", False: "target"}) + + # Get score thresholds + try: + score_threshold_before = ( + plot_df[plot_df["qvalue_before"] <= fdr_threshold] + .sort_values("qvalue_before", ascending=False)["score_before"] + .iloc[0] + ) + except (IndexError, KeyError): + score_threshold_before = None + + try: + score_threshold_after = ( + plot_df[plot_df["qvalue_after"] <= fdr_threshold] + .sort_values("qvalue_after", ascending=False)["score_after"] + .iloc[0] + ) + except (IndexError, KeyError): + score_threshold_after = None + + # Plot + fig = px.scatter( + data_frame=plot_df, + x="score_before", + y="score_after", + color="PSM type", + marginal_x="histogram", + marginal_y="histogram", + opacity=0.1, + labels={ + "score_before": "PSM score (before rescoring)", + "score_after": "PSM score (after rescoring)", + }, + ) + + # Draw FDR thresholds + if score_threshold_before: + fig.add_vline(x=score_threshold_before, line_dash="dash", row=1, col=1) + fig.add_vline(x=score_threshold_before, line_dash="dash", row=2, col=1) + if score_threshold_after: + fig.add_hline(y=score_threshold_after, line_dash="dash", row=1, col=1) + fig.add_hline(y=score_threshold_after, line_dash="dash", row=1, col=2) + + return fig + + +def fdr_plot_comparison_df( + psm_df: pd.DataFrame, +) -> go.Figure: + """ + Plot number of identifications in function of FDR threshold before/after rescoring from dataframe. + + Parameters + ---------- + psm_df + Dataframe with PSM information including qvalue_before, qvalue_after, and is_decoy columns. + + Returns + ------- + go.Figure + Plotly figure with FDR comparison. + """ + if "qvalue_before" not in psm_df.columns or "qvalue_after" not in psm_df.columns: + figure = go.Figure() + figure.add_annotation( + text="No before/after q-value data available for comparison.", + showarrow=False, + ) + return figure + + # Filter targets only + targets = psm_df[~psm_df["is_decoy"]].copy() + + # Prepare data in long format + plot_data = pd.concat( + [ + targets[["qvalue_before"]] + .rename(columns={"qvalue_before": "q-value"}) + .assign(**{"before/after": "before rescoring"}), + targets[["qvalue_after"]] + .rename(columns={"qvalue_after": "q-value"}) + .assign(**{"before/after": "after rescoring"}), + ] + ) + + # Plot + fig = px.ecdf( + data_frame=plot_data, + x="q-value", + color="before/after", + log_x=True, + ecdfnorm=None, + labels={ + "q-value": "FDR threshold", + "before/after": "", + }, + color_discrete_map={ + "before rescoring": "#316395", + "after rescoring": "#319545", + }, + ) + fig.add_vline(x=0.01, line_dash="dash", line_color="black") + fig.update_layout(yaxis_title="Identified PSMs") + return fig + + +def identification_overlap_df( + psm_df: pd.DataFrame, + fdr_threshold: float = 0.01, +) -> go.Figure: + """ + Plot stacked bar charts of removed, retained, and gained PSMs and peptides from dataframe. + + Parameters + ---------- + psm_df + Dataframe with PSM information including qvalue_before, qvalue_after, + is_decoy, and peptidoform columns. + fdr_threshold + FDR threshold for counting identifications. + + Returns + ------- + go.Figure + Plotly figure with identification overlap. + """ + if "qvalue_before" not in psm_df.columns or "qvalue_after" not in psm_df.columns: + figure = go.Figure() + figure.add_annotation( + text="No before/after q-value data available for comparison.", + showarrow=False, + ) + return figure + + overlap_data = defaultdict(dict) + + # PSM level + targets = psm_df[~psm_df["is_decoy"]] + psms_before = set(targets[targets["qvalue_before"] <= fdr_threshold].index) + psms_after = set(targets[targets["qvalue_after"] <= fdr_threshold].index) + + overlap_data["removed"]["psms"] = -len(psms_before - psms_after) + overlap_data["retained"]["psms"] = len(psms_after.intersection(psms_before)) + overlap_data["gained"]["psms"] = len(psms_after - psms_before) + + # Peptide level + if "peptidoform" in psm_df.columns: + peptides_before = set( + targets[targets["qvalue_before"] <= fdr_threshold]["peptidoform"].unique() + ) + peptides_after = set( + targets[targets["qvalue_after"] <= fdr_threshold]["peptidoform"].unique() + ) + + overlap_data["removed"]["peptides"] = -len(peptides_before - peptides_after) + overlap_data["retained"]["peptides"] = len(peptides_after.intersection(peptides_before)) + overlap_data["gained"]["peptides"] = len(peptides_after - peptides_before) + + colors = ["#953331", "#316395", "#319545"] + levels = list(overlap_data["retained"].keys()) + fig = plotly.subplots.make_subplots(rows=len(levels), cols=1) + + for i, level in enumerate(levels): + for (item, data), color in zip(overlap_data.items(), colors): + if level not in data: + continue + fig.add_trace( + go.Bar( + y=[level], + x=[data[level]], + marker={"color": color}, + orientation="h", + name=item, + showlegend=True if i == 0 else False, + ), + row=i + 1, + col=1, + ) + fig.update_layout(barmode="relative") + + return fig diff --git a/ms2rescore/report/generate.py b/ms2rescore/report/generate.py index 4a5d411..f1848e8 100644 --- a/ms2rescore/report/generate.py +++ b/ms2rescore/report/generate.py @@ -3,8 +3,8 @@ import importlib.resources import json import logging +from collections import defaultdict from datetime import datetime -from itertools import cycle from pathlib import Path from typing import Dict, Optional @@ -23,9 +23,13 @@ import ms2rescore import ms2rescore.report.charts as charts import ms2rescore.report.templates as templates +from ms2rescore.feature_generators import FEATURE_GENERATORS from ms2rescore.report.utils import ( + calculate_fdr_stats, + create_psm_dataframe, get_confidence_estimates, get_feature_values, + infer_feature_names_from_psm_list, read_feature_names, ) @@ -50,6 +54,8 @@ def generate_report( psm_list: Optional[psm_utils.PSMList] = None, feature_names: Optional[Dict[str, set]] = None, use_txt_log: bool = False, + output_file: Optional[Path] = None, + use_mokapot: bool = False, ): """ Generate the report. @@ -68,6 +74,11 @@ def generate_report( use_txt_log If True, the log file will be read from ``output_path_prefix + ".log.txt"`` instead of ``output_path_prefix + ".log.html"``. + output_file + Path to the output HTML file. If not provided, will be ``output_path_prefix + ".report.html"``. + use_mokapot + If True, use mokapot LinearConfidence objects for overview charts (legacy mode). + If False (default), use PSM dataframe directly. """ files = _collect_files(output_path_prefix, use_txt_log=use_txt_log) @@ -80,27 +91,70 @@ def generate_report( else: raise FileNotFoundError("PSM file not found and no PSM list provided.") - # Read config - config = json.loads(files["configuration"].read_text()) + # Create comprehensive dataframe from PSM list + logger.debug("Creating PSM dataframe...") + psm_df = create_psm_dataframe(psm_list) - logger.debug("Recalculating confidence estimates...") - fasta_file = config["ms2rescore"]["fasta_file"] - confidence_before, confidence_after = get_confidence_estimates(psm_list, fasta_file) + # Pre-compute commonly used filtered subsets for performance + targets = psm_df[~psm_df["is_decoy"]] + is_decoy = psm_df["is_decoy"] # Extract once for reuse + if "qvalue_before" in psm_df.columns and "qvalue_after" in psm_df.columns: + targets_before_fdr = targets[targets["qvalue_before"] <= 0.01] + targets_after_fdr = targets[targets["qvalue_after"] <= 0.01] + else: + targets_before_fdr = None + targets_after_fdr = None + + # Try to read config, but don't fail if it doesn't exist + config = None + if files["configuration"] and files["configuration"].is_file(): + try: + config = json.loads(files["configuration"].read_text()) + except (json.JSONDecodeError, FileNotFoundError): + logger.warning("Could not read configuration file. Proceeding without it.") + config = {"ms2rescore": {}} + else: + logger.info("No configuration file found. Proceeding without it.") + config = {"ms2rescore": {}} + + # Generate overview context + if use_mokapot and config.get("ms2rescore", {}).get("fasta_file"): + logger.debug("Recalculating confidence estimates with mokapot...") + fasta_file = config["ms2rescore"]["fasta_file"] + confidence_before, confidence_after = get_confidence_estimates(psm_list, fasta_file) + overview_context = _get_overview_context(confidence_before, confidence_after) + else: + logger.debug("Generating overview from PSM dataframe...") + overview_context = _get_overview_context_df( + psm_df, + targets=targets, + targets_before_fdr=targets_before_fdr, + targets_after_fdr=targets_after_fdr, + ) - overview_context = _get_overview_context(confidence_before, confidence_after) - target_decoy_context = _get_target_decoy_context(psm_list) - features_context = _get_features_context(psm_list, files, feature_names=feature_names) + target_decoy_context = _get_target_decoy_context(psm_df) + features_context = _get_features_context( + psm_df, files, is_decoy=is_decoy, feature_names=feature_names + ) config_context = _get_config_context(config) log_context = _get_log_context(files) + # Get PSM filename(s) for metadata + if config.get("ms2rescore", {}).get("psm_file"): + psm_filenames = "\n".join( + [Path(id_file).name for id_file in config["ms2rescore"]["psm_file"]] + ) + elif files["PSMs"]: + psm_filenames = files["PSMs"].name + else: + psm_filenames = "Unknown" + context = { "plotlyjs_version": get_plotlyjs_version(), "metadata": { "generated_on": datetime.now().strftime("%d/%m/%Y %H:%M:%S"), "ms2rescore_version": ms2rescore.__version__, # TODO: Write during run? - "psm_filename": "\n".join( - [Path(id_file).name for id_file in config["ms2rescore"]["psm_file"]] - ), + "psm_filename": psm_filenames, }, "main_tabs": [ { @@ -136,7 +190,7 @@ def generate_report( ], } - _render_and_write(output_path_prefix, **context) + _render_and_write(output_path_prefix, output_file=output_file, **context) def _collect_files(output_path_prefix, use_txt_log=False): @@ -197,6 +251,116 @@ def _get_stats_context(confidence_before, confidence_after): return stats +def _get_stats_context_df( + psm_df: pd.DataFrame, + targets: Optional[pd.DataFrame] = None, + targets_before_fdr: Optional[pd.DataFrame] = None, + targets_after_fdr: Optional[pd.DataFrame] = None, + fdr_threshold: float = 0.01, +) -> list: + """Return context for overview statistics pane from dataframe.""" + stats = [] + + if "qvalue_before" not in psm_df.columns or "qvalue_after" not in psm_df.columns: + return stats + + # Use pre-computed subsets if available, otherwise compute now + if targets is None: + targets = psm_df[~psm_df["is_decoy"]] + + # PSM level stats - use pre-computed subsets if available + if targets_before_fdr is not None and targets_after_fdr is not None: + psms_before = len(targets_before_fdr) + psms_after = len(targets_after_fdr) + else: + psms_before = len(targets[targets["qvalue_before"] <= fdr_threshold]) + psms_after = len(targets[targets["qvalue_after"] <= fdr_threshold]) + + if psms_before > 0: + increase = (psms_after - psms_before) / psms_before * 100 + stats.append( + { + "item": "PSMs", + "card_color": "card-bg-blue", + "number": psms_after, + "diff": f"({psms_after - psms_before:+})", + "percentage": f"{increase:.1f}%", + "is_increase": increase > 0, + "bar_percentage": psms_before / psms_after * 100 + if increase > 0 + else psms_after / psms_before * 100, + "bar_color": "#24a143" if increase > 0 else "#a12424", + } + ) + + # Peptide level stats + if "peptidoform" in psm_df.columns: + if targets_before_fdr is not None and targets_after_fdr is not None: + peptides_before = targets_before_fdr["peptidoform"].nunique() + peptides_after = targets_after_fdr["peptidoform"].nunique() + else: + peptides_before = targets[targets["qvalue_before"] <= fdr_threshold][ + "peptidoform" + ].nunique() + peptides_after = targets[targets["qvalue_after"] <= fdr_threshold][ + "peptidoform" + ].nunique() + + if peptides_before > 0: + increase = (peptides_after - peptides_before) / peptides_before * 100 + stats.append( + { + "item": "Peptides", + "card_color": "card-bg-green", + "number": peptides_after, + "diff": f"({peptides_after - peptides_before:+})", + "percentage": f"{increase:.1f}%", + "is_increase": increase > 0, + "bar_percentage": peptides_before / peptides_after * 100 + if increase > 0 + else peptides_after / peptides_before * 100, + "bar_color": "#24a143" if increase > 0 else "#a12424", + } + ) + + return stats + + +def _get_overview_context_df( + psm_df: pd.DataFrame, + targets: Optional[pd.DataFrame] = None, + targets_before_fdr: Optional[pd.DataFrame] = None, + targets_after_fdr: Optional[pd.DataFrame] = None, +) -> dict: + """Return context for overview tab from dataframe.""" + logger.debug("Generating overview charts from dataframe...") + return { + "stats": _get_stats_context_df( + psm_df, + targets=targets, + targets_before_fdr=targets_before_fdr, + targets_after_fdr=targets_after_fdr, + ), + "charts": [ + { + "title": TEXTS["charts"]["score_comparison"]["title"], + "description": TEXTS["charts"]["score_comparison"]["description"], + "chart": charts.score_scatter_plot_df(psm_df).to_html(**PLOTLY_HTML_KWARGS), + }, + { + "title": TEXTS["charts"]["fdr_comparison"]["title"], + "description": TEXTS["charts"]["fdr_comparison"]["description"], + "chart": charts.fdr_plot_comparison_df(psm_df).to_html(**PLOTLY_HTML_KWARGS), + }, + { + "title": TEXTS["charts"]["identification_overlap"]["title"], + "description": TEXTS["charts"]["identification_overlap"]["description"], + "chart": charts.identification_overlap_df(psm_df).to_html(**PLOTLY_HTML_KWARGS), + }, + ], + } + + def _get_overview_context(confidence_before, confidence_after) -> dict: """Return context for overview tab.""" logger.debug("Generating overview charts...") @@ -231,9 +395,8 @@ def _get_overview_context(confidence_before, confidence_after) -> dict: } -def _get_target_decoy_context(psm_list) -> dict: +def _get_target_decoy_context(psm_df: pd.DataFrame) -> dict: logger.debug("Generating target-decoy charts...") - psm_df = psm_list.to_dataframe() return { "charts": [ { @@ -251,51 +414,133 @@ def _get_target_decoy_context(psm_list) -> dict: def _get_features_context( - psm_list: PSMList, + psm_df: pd.DataFrame, files: Dict[str, Path], + is_decoy: Optional[pd.Series] = None, feature_names: Optional[Dict[str, set]] = None, ) -> dict: """Return context for features tab.""" logger.debug("Generating feature-related charts...") + + # Use pre-computed is_decoy if provided, otherwise extract + if is_decoy is None: + is_decoy = psm_df["is_decoy"] context: dict[str, list] = {"charts": []} # Get feature names, mapping with generator, and flat list + if feature_names is None or not feature_names: + # Try to read from file first + feature_names = read_feature_names(files.get("feature names")) + + # If file doesn't exist or is empty, infer from feature generators + if not feature_names: + logger.info("Feature names file not found. Inferring from feature generators...") + # Get feature columns (exclude standard PSM columns) + standard_columns = { + "spectrum_id", + "run", + "collection", + "spectrum", + "peptidoform", + "precursor_mz", + "retention_time", + "protein_list", + "rank", + "source", + "provenance_data", + "metadata", + "rescoring_features", + "qvalue", + "pep", + "score", + "precursor_charge", + "is_decoy", + "score_before", + "qvalue_before", + "score_after", + "qvalue_after", + } + feature_cols = [col for col in psm_df.columns if col not in standard_columns] + + if feature_cols: + # Build mapping of feature name -> generator by checking each generator's feature_names + feature_to_generator = {} + for fgen_name, fgen_class in FEATURE_GENERATORS.items(): + try: + # Instantiate with empty config to get feature names + fgen = fgen_class() + fgen_features = fgen.feature_names + for fname in fgen_features: + feature_to_generator[fname] = fgen_name + except Exception: + # If instantiation fails, skip this generator + continue + + # Categorize features based on generator mapping + feature_names = defaultdict(list) + for fname in feature_cols: + if fname in feature_to_generator: + feature_names[feature_to_generator[fname]].append(fname) + else: + # Unknown feature - put in "other" category + feature_names["other"].append(fname) + + # If still no features, return empty context if not feature_names: - feature_names = read_feature_names(files["feature names"]) + logger.warning("No features found. Skipping feature charts.") + return context + + # Convert sets to lists if needed (for compatibility with both sources) + feature_names = {k: list(v) if isinstance(v, set) else v for k, v in feature_names.items()} + feature_names_flat = [f_name for f_list in feature_names.values() for f_name in f_list] feature_names_inv = {name: gen for gen, f_list in feature_names.items() for name in f_list} - # Get fixed color map for feature generators - color_map = dict(zip(feature_names.keys(), cycle(px.colors.qualitative.Plotly))) + # Fixed color map for feature generators (Okabe-Ito colorblind-safe palette) + FEATURE_GENERATOR_COLORS = { + "ms2pip": "#009E73", # Bluish green + "deeplc": "#E69F00", # Orange + "im2deep": "#0072B2", # Blue + "ms2": "#56B4E9", # Sky blue + "basic": "#000000", # Black + "psm_file": "#F0E442", # Yellow + "other": "#CC79A7", # Pink + } + color_map = {fg: FEATURE_GENERATOR_COLORS.get(fg, "#FFFFFF") for fg in feature_names.keys()} # feature weights - if not files["feature weights"]: - logger.warning("Could not find feature weights files. Skipping feature weights plot.") + if not files.get("feature weights") or not files["feature weights"].is_file(): + logger.info("Feature weights file not found. Skipping feature weights plot.") else: - feature_weights = pd.read_csv(files["feature weights"], sep="\t").melt( - var_name="feature", value_name="weight" - ) - feature_weights["feature"] = feature_weights["feature"].str.replace( - r"^(feature:)?", "", regex=True - ) - feature_weights["feature_generator"] = feature_weights["feature"].map(feature_names_inv) + try: + feature_weights = pd.read_csv(files["feature weights"], sep="\t").melt( + var_name="feature", value_name="weight" + ) + feature_weights["feature"] = feature_weights["feature"].str.replace( + r"^(feature:)?", "", regex=True + ) + feature_weights["feature_generator"] = feature_weights["feature"].map( + feature_names_inv + ) - context["charts"].append( - { - "title": TEXTS["charts"]["feature_usage"]["title"], - "description": TEXTS["charts"]["feature_usage"]["description"], - "chart": charts.feature_weights_by_generator( - feature_weights, color_discrete_map=color_map - ).to_html(**PLOTLY_HTML_KWARGS) - + charts.feature_weights(feature_weights, color_discrete_map=color_map).to_html( - **PLOTLY_HTML_KWARGS - ), - } - ) + context["charts"].append( + { + "title": TEXTS["charts"]["feature_usage"]["title"], + "description": TEXTS["charts"]["feature_usage"]["description"], + "chart": charts.feature_weights_by_generator( + feature_weights, color_discrete_map=color_map + ).to_html(**PLOTLY_HTML_KWARGS) + + charts.feature_weights( + feature_weights, color_discrete_map=color_map + ).to_html(**PLOTLY_HTML_KWARGS), + } + ) + except Exception as e: + logger.warning(f"Could not generate feature weights plot: {e}") - # Individual feature performance - features = get_feature_values(psm_list, feature_names_flat) - _, feature_ecdf_auc = charts.calculate_feature_qvalues(features, psm_list["is_decoy"]) + # Individual feature performance - extract features from dataframe + features = psm_df[feature_names_flat].copy() + _, feature_ecdf_auc = charts.calculate_feature_qvalues(features, is_decoy) feature_ecdf_auc["feature_generator"] = feature_ecdf_auc["feature"].map(feature_names_inv) context["charts"].append( @@ -314,23 +559,25 @@ def _get_features_context( { "title": TEXTS["charts"]["ms2pip_pearson"]["title"], "description": TEXTS["charts"]["ms2pip_pearson"]["description"], - "chart": charts.ms2pip_correlation( - features, psm_list["is_decoy"], psm_list["qvalue"] - ).to_html(**PLOTLY_HTML_KWARGS), + "chart": charts.ms2pip_correlation(features, is_decoy, psm_df["qvalue"]).to_html( + **PLOTLY_HTML_KWARGS + ), } ) + # Pre-compute filtered subset for feature-specific charts (high-confidence targets) + high_conf_mask = (~is_decoy) & (psm_df["qvalue"] <= 0.01) + high_conf_features = features[high_conf_mask] + # DeepLC specific charts if "deeplc" in feature_names: - import deeplc.plot - - scatter_chart = deeplc.plot.scatter( - df=features[(~psm_list["is_decoy"]) & (psm_list["qvalue"] <= 0.01)], + scatter_chart = charts.rt_scatter( + df=high_conf_features, predicted_column="predicted_retention_time_best", observed_column="observed_retention_time_best", ) - baseline_chart = deeplc.plot.distribution_baseline( - df=features[(~psm_list["is_decoy"]) & (psm_list["qvalue"] <= 0.01)], + baseline_chart = charts.rt_distribution_baseline( + df=high_conf_features, predicted_column="predicted_retention_time_best", observed_column="observed_retention_time_best", ) @@ -345,10 +592,8 @@ def _get_features_context( # IM2Deep specific charts if "im2deep" in feature_names: - import deeplc.plot - - scatter_chart = deeplc.plot.scatter( - df=features[(~psm_list["is_decoy"]) & (psm_list["qvalue"] <= 0.01)], + scatter_chart = charts.rt_scatter( + df=high_conf_features, predicted_column="ccs_predicted_im2deep", observed_column="ccs_observed_im2deep", xaxis_label="Observed CCS", @@ -364,33 +609,6 @@ def _get_features_context( } ) - # ionmob specific charts - if "ionmob" in feature_names: - try: - import deeplc.plot - - scatter_chart = deeplc.plot.scatter( - df=features[(~psm_list["is_decoy"]) & (psm_list["qvalue"] <= 0.01)], - predicted_column="ccs_predicted", - observed_column="ccs_observed", - xaxis_label="Observed CCS", - yaxis_label="Predicted CCS", - plot_title="Predicted vs. observed CCS - ionmob", - ) - - context["charts"].append( - { - "title": TEXTS["charts"]["ionmob_performance"]["title"], - "description": TEXTS["charts"]["ionmob_performance"]["description"], - "chart": scatter_chart.to_html(**PLOTLY_HTML_KWARGS), - } - ) - - # TODO: for now, ionmob plot will only be available if deeplc is installed. Since ionmob does not have a dependency on deeplc, this should be changed in the future. - except ImportError: - logger.warning( - "Could not import deeplc.plot, skipping ionmob CCS prediction performance plot. Please install DeepLC to generate this plot." - ) return context @@ -416,9 +634,13 @@ def _get_log_context(files: Dict[str, Path]) -> dict: return {"log": "Log file format not recognized."} -def _render_and_write(output_path_prefix: str, **context): +def _render_and_write(output_path_prefix: str, output_file: Optional[Path] = None, **context): """Render template with context and write to HTML file.""" - report_path = Path(output_path_prefix + ".report.html").resolve() + if output_file: + report_path = Path(output_file).resolve() + else: + report_path = Path(output_path_prefix + ".report.html").resolve() + logger.info("Writing report to %s", report_path.as_posix()) # Use importlib.resources for PyInstaller compatibility diff --git a/ms2rescore/report/utils.py b/ms2rescore/report/utils.py index d5fbfb8..7178fb9 100644 --- a/ms2rescore/report/utils.py +++ b/ms2rescore/report/utils.py @@ -4,7 +4,7 @@ from collections import defaultdict from csv import DictReader from pathlib import Path -from typing import Optional, Tuple +from typing import Dict, Optional, Tuple import pandas as pd import psm_utils @@ -15,13 +15,57 @@ logger = logging.getLogger(__name__) -def read_feature_names(feature_names_path: Path) -> dict: +def read_feature_names(feature_names_path: Optional[Path]) -> dict: """Read feature names and mapping with feature generator from file.""" feature_names = defaultdict(list) - with open(feature_names_path) as f: - reader = DictReader(f, delimiter="\t") - for line in reader: - feature_names[line["feature_generator"]].append(line["feature_name"]) + if not feature_names_path or not feature_names_path.is_file(): + return feature_names + + try: + with open(feature_names_path) as f: + reader = DictReader(f, delimiter="\t") + for line in reader: + feature_names[line["feature_generator"]].append(line["feature_name"]) + except (FileNotFoundError, KeyError, ValueError) as e: + logger.warning(f"Could not read feature names file: {e}") + + return feature_names + + +def infer_feature_names_from_psm_list(psm_list: psm_utils.PSMList) -> Dict[str, list]: + """Infer feature names and generators from PSM list when no feature_names file exists.""" + feature_names = defaultdict(list) + + if not psm_list or not psm_list[0].rescoring_features: + return feature_names + + # Get all feature names from the first PSM + all_features = list(psm_list[0].rescoring_features.keys()) + + # Try to infer generator from feature name patterns + for fname in all_features: + fname_lower = fname.lower() + + # Pattern matching for common feature generators + if any(x in fname_lower for x in ["ms2pip", "spec_pearson", "spec_spearman", "ionmatch"]): + feature_names["ms2pip"].append(fname) + elif any(x in fname_lower for x in ["deeplc", "retention_time", "rt_diff"]): + feature_names["deeplc"].append(fname) + elif any( + x in fname_lower for x in ["im2deep", "ccs_predicted_im2deep", "ccs_observed_im2deep"] + ): + feature_names["im2deep"].append(fname) + elif ( + any(x in fname_lower for x in ["ionmob", "ccs_predicted", "ccs_observed"]) + and "im2deep" not in fname_lower + ): + feature_names["ionmob"].append(fname) + elif any(x in fname_lower for x in ["basic", "charge", "missed_cleavages"]): + feature_names["basic"].append(fname) + else: + # Unknown generator - use "other" category + feature_names["other"].append(fname) + return feature_names @@ -75,3 +119,125 @@ def get_confidence_estimates( logger.warning("Could not assign confidence estimates for %s rescoring.", when) return confidence["before"], confidence["after"] + + +def create_psm_dataframe(psm_list: psm_utils.PSMList) -> pd.DataFrame: + """ + Create a comprehensive dataframe from PSM list with all necessary information. + + This dataframe includes: + - Basic PSM information (peptide, score, qvalue, is_decoy, etc.) + - Before rescoring scores from provenance data + - All rescoring features + + Parameters + ---------- + psm_list + PSM list to convert to dataframe. + + Returns + ------- + pd.DataFrame + Dataframe with all PSM information. + """ + # Start with basic PSM dataframe + psm_df = psm_list.to_dataframe() + + # Add before rescoring scores from provenance data + try: + provenance_df = pd.DataFrame.from_records(psm_list["provenance_data"]) + if "before_rescoring_score" in provenance_df.columns: + psm_df["score_before"] = provenance_df["before_rescoring_score"].astype(float) + if "before_rescoring_qvalue" in provenance_df.columns: + psm_df["qvalue_before"] = provenance_df["before_rescoring_qvalue"].astype(float) + except (KeyError, ValueError) as e: + logger.warning("Could not extract before rescoring scores from provenance data: %s", e) + psm_df["score_before"] = None + psm_df["qvalue_before"] = None + + # Add rescoring features - vectorized extraction + if psm_list[0].rescoring_features: + # Extract all rescoring_features dicts at once (much faster than looping) + features_df = pd.DataFrame.from_records(psm_list["rescoring_features"]).astype("float32") + # Merge features with PSM dataframe (they should have same index) + psm_df = pd.concat([psm_df, features_df], axis=1) + # Remove duplicate columns (keep last, i.e., from features_df) + psm_df = psm_df.loc[:, ~psm_df.columns.duplicated(keep="last")] + + # Rename current score/qvalue to score_after/qvalue_after for clarity + psm_df["score_after"] = psm_df["score"] + psm_df["qvalue_after"] = psm_df["qvalue"] + + return psm_df + + +def calculate_fdr_stats( + psm_df: pd.DataFrame, + score_column: str = "score", + fdr_threshold: float = 0.01, +) -> Dict[str, int]: + """ + Calculate FDR statistics from PSM dataframe. + + Parameters + ---------- + psm_df + Dataframe with PSM information including score, qvalue, and is_decoy columns. + score_column + Column name for scores to use. + fdr_threshold + FDR threshold for counting identifications. + + Returns + ------- + dict + Dictionary with counts of identifications at different levels. + """ + stats = {} + + # PSM level + targets = psm_df[~psm_df["is_decoy"]] + stats["psms"] = len(targets[targets["qvalue"] <= fdr_threshold]) + + # Peptide level (unique peptide sequences) + if "peptidoform" in psm_df.columns: + unique_peptides = targets[targets["qvalue"] <= fdr_threshold]["peptidoform"].nunique() + stats["peptides"] = unique_peptides + + return stats + + +def get_score_threshold_at_fdr( + psm_df: pd.DataFrame, + score_column: str = "score", + qvalue_column: str = "qvalue", + fdr_threshold: float = 0.01, +) -> Optional[float]: + """ + Get score threshold at a given FDR threshold. + + Parameters + ---------- + psm_df + Dataframe with PSM information. + score_column + Column name for scores. + qvalue_column + Column name for q-values. + fdr_threshold + FDR threshold. + + Returns + ------- + float or None + Score threshold at the FDR threshold, or None if no PSMs pass threshold. + """ + try: + threshold = ( + psm_df[psm_df[qvalue_column] <= fdr_threshold] + .sort_values(qvalue_column, ascending=False)[score_column] + .iloc[0] + ) + return threshold + except (IndexError, KeyError): + return None diff --git a/ms2rescore/rescoring_engines/percolator.py b/ms2rescore/rescoring_engines/percolator.py index 192950b..32d856c 100644 --- a/ms2rescore/rescoring_engines/percolator.py +++ b/ms2rescore/rescoring_engines/percolator.py @@ -19,8 +19,8 @@ import logging import subprocess -from typing import Any, Dict, Optional from copy import deepcopy +from typing import Any, Dict, Optional import psm_utils diff --git a/ms2rescore/utils.py b/ms2rescore/utils.py index 1fc686b..0780e81 100644 --- a/ms2rescore/utils.py +++ b/ms2rescore/utils.py @@ -3,8 +3,10 @@ from glob import glob from pathlib import Path from typing import Optional, Union +import numpy as np from ms2rescore_rs import is_supported_file_type +from psm_utils import PSMList from ms2rescore.exceptions import MS2RescoreConfigurationError @@ -92,3 +94,57 @@ def _is_minitdf(spectrum_file: str) -> bool: files = set(Path(spectrum_file).glob("*ms2spectrum.bin")) files.update(Path(spectrum_file).glob("*ms2spectrum.parquet")) return len(files) >= 2 + + +def filter_mumble_psms(psm_list: PSMList, threshold=1) -> PSMList: + """ + Filter out mumble PSMs with `matched_ions_pct` lower than the original hit. + + Parameters + ---------- + psm_list : PSMList + List of PSMs to filter + threshold : float, optional + Threshold to lower the maximum matched_ions_pct of the original hit + """ + # Extract relevant fields from the PSM list + original_hit = np.array([metadata.get("original_psm") for metadata in psm_list["metadata"]]) + spectrum_indices = np.array([psm.spectrum_id for psm in psm_list]) + runs = np.array([psm.run for psm in psm_list]) + + # Check if matched_ions_pct exists + if "matched_ions_pct" not in psm_list[0].rescoring_features: + return psm_list + + matched_ions = np.array([psm.rescoring_features["matched_ions_pct"] for psm in psm_list]) + + # Create unique keys for each (run, spectrum_id) + unique_keys = np.core.defchararray.add(runs.astype(str), spectrum_indices.astype(str)) + unique_keys_indices, inverse_indices = np.unique(unique_keys, return_inverse=True) + + # Initialize an array to store the `matched_ions_pct` of original hits per group + original_matched_ions_pct = np.full( + len(unique_keys_indices), -np.inf + ) # Default to -inf for groups without original hits + + # Assign the `matched_ions_pct` of original hits to their groups + np.maximum.at( + original_matched_ions_pct, inverse_indices[original_hit], matched_ions[original_hit] + ) + + # lower the maximum with the threshold + original_matched_ions_pct = original_matched_ions_pct * threshold + + # Broadcast the original `matched_ions_pct` back to all PSMs in each group + original_matched_ions_for_all = original_matched_ions_pct[inverse_indices] + + # Determine the filtering condition + keep = np.logical_or( + original_hit, # Always keep original hits + matched_ions + >= original_matched_ions_for_all, # Keep hits with `matched_ions_pct` >= the original + ) + + # Filter PSMs + logger.debug(f"Filtered out {len(psm_list) - np.sum(keep)} mumble PSMs.") + return psm_list[keep] diff --git a/pyproject.toml b/pyproject.toml index ca8e520..ccabcdb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,14 +37,13 @@ dependencies = [ "click>=7", "customtkinter>=5,<6", "deeplc>=3.1", - "deeplcretrainer", "im2deep>=1.1", "jinja2>=3", "lxml>=4.5", "mokapot==0.10", # 0.11.0 will introduce API changes "ms2pip>=4.0.0", "ms2rescore_rs>=0.4", - "numpy>=1.25", + "numpy>=1.25,<2.0", "packaging>=25.0", "pandas>=1", "plotly>=5", @@ -55,7 +54,7 @@ dependencies = [ ] [project.optional-dependencies] -ionmob = ["ionmob>=0.2", "tensorflow"] +mumble = ["rustyms>=0.8.3", "mumble>=0.2.0"] idxml = ["pyopenms>=3.3"] [dependency-groups] @@ -74,15 +73,12 @@ docs = [ ] installer = [ # Pinned dependencies for reproducible Windows installer builds - "pyinstaller", - "pyopenms==3.5", - "ionmob>=0.2", - "tensorflow", - "psm_utils==1.5.1", - "ms2rescore-rs==0.4.3", - "deeplc==3.1.13", - "im2deep==1.2.0", "cbor2", + # "deeplc==4.0.0", # TODO: Update upon stable release + # "im2deep==2.0.0", + # "ms2rescore-rs==0.5.0", + "psm_utils==1.5.2", + "pyopenms==3.5", ] [project.urls] diff --git a/uv.lock b/uv.lock index b1935c8..d42918b 100644 --- a/uv.lock +++ b/uv.lock @@ -25,15 +25,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, ] -[[package]] -name = "altgraph" -version = "0.17.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/f8/97fdf103f38fed6792a1601dbc16cc8aac56e7459a9fff08c812d8ae177a/altgraph-0.17.5.tar.gz", hash = "sha256:c87b395dd12fabde9c99573a9749d67da8d29ef9de0125c7f536699b4a9bc9e7", size = 48428, upload-time = "2025-11-21T20:35:50.583Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/ba/000a1996d4308bc65120167c21241a3b205464a2e0b58deda26ae8ac21d1/altgraph-0.17.5-py2.py3-none-any.whl", hash = "sha256:f3a22400bce1b0c701683820ac4f3b159cd301acab067c51c653e06961600597", size = 21228, upload-time = "2025-11-21T20:35:49.444Z" }, -] - [[package]] name = "annotated-types" version = "0.7.0" @@ -100,55 +91,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, ] -[[package]] -name = "biopython" -version = "1.86" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9d/61/c59a849bd457c8a1b408ae828dbcc15e674962b5a29705e869e15b32bf25/biopython-1.86.tar.gz", hash = "sha256:93a50b586a4d2cec68ab2f99d03ef583c5761d8fba5535cb8e81da781d0d92ff", size = 19835323, upload-time = "2025-10-28T21:18:31.041Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/f5/37d6bb3a1245ec5f5f1c66d5cd790b06cdb54a75b36849893405c17f3612/biopython-1.86-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba88b0754ad53c93eba11d910364cfc773686933c89a886522309ba903151e50", size = 2691944, upload-time = "2025-10-28T21:27:24.053Z" }, - { url = "https://files.pythonhosted.org/packages/14/12/44d71f333b7302b30788df80705f2207c47b54c17d0935a378dfc709507d/biopython-1.86-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6cceb32b9036bbdc59962e31bd1605ece24edc226c0d50f99839948b5b5c9dda", size = 2669434, upload-time = "2025-10-28T21:26:49.145Z" }, - { url = "https://files.pythonhosted.org/packages/a5/1b/731060090ed29b5ac2484865255f1f363a50afb7275717ceb2c6f20d3ea4/biopython-1.86-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0f040ff85bd7d0ee06574bc6d032bc666802f2fe781b0c316b936237eb3d17e", size = 3196718, upload-time = "2025-10-29T00:35:47.806Z" }, - { url = "https://files.pythonhosted.org/packages/1c/8d/8409535c341061b9c78faf151e73b484b456b3c3bdf59b27cf3984f16fbc/biopython-1.86-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6ac858fd71f1093380d8b0a16acf060e7c228ad65f9ecacdb9f5760cfb9f59b1", size = 3218383, upload-time = "2025-10-29T00:35:53.523Z" }, - { url = "https://files.pythonhosted.org/packages/f4/bc/5e93a11f70732122679747a728509d03a6a066b178cc1d7ca30ed2f1ebee/biopython-1.86-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da4bcf5a48ee647624e2d0bedac7fb1c24ef0facd514519cca074593b8a6a40e", size = 3168368, upload-time = "2025-10-28T23:53:16.425Z" }, - { url = "https://files.pythonhosted.org/packages/b2/c6/e187940571a3a24d20f407f1d7514ab1fe0dc9fa49e01790c4bd56ced0bc/biopython-1.86-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d4dd9090caaf364a08ab54cd561f37c5f4ea5bcc8f0189d332dcd36d6df5767", size = 3186451, upload-time = "2025-10-28T23:53:22.463Z" }, - { url = "https://files.pythonhosted.org/packages/2a/88/1e8ffb0db6a03888768613d682a79043e9975067b9095e644a6872905c88/biopython-1.86-cp311-cp311-win32.whl", hash = "sha256:90591f4554c09d311193e7774b5143442c67e178a5b7d929aaa2a054048b22a7", size = 2697756, upload-time = "2025-10-28T21:32:23.017Z" }, - { url = "https://files.pythonhosted.org/packages/f0/b2/e34e45d6cb46c96486a2ed5f07874b6c9493dec68b9d6262ae05f4fe909b/biopython-1.86-cp311-cp311-win_amd64.whl", hash = "sha256:0a95321ca929c04c934e62252c9e2cc5c4fd13ce575798d98af2d79512334b9b", size = 2733781, upload-time = "2025-10-28T21:32:18.409Z" }, - { url = "https://files.pythonhosted.org/packages/98/e2/199b8ccbd4b9bf234157db0668177b5b7784d62f29d9096fd0d3a70e3b86/biopython-1.86-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f8d372aae21d79b11613751c6ae23c88db0e94d25b7567b1f67aa0304fb61667", size = 2693171, upload-time = "2025-10-29T00:26:59.028Z" }, - { url = "https://files.pythonhosted.org/packages/d8/2f/1a7da2a55212b3d0a03866d22213f91273fee3722b5364575419fbe574a5/biopython-1.86-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:baf19d9237aaaa387a68f8f055f978af5c80338d7e037ab028e8d768928f1250", size = 2692543, upload-time = "2025-10-28T21:27:31.855Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e9/4057d4c2aa22ca25c180ecbed2ce9e7d65bf787999778bc63b41df0d03b5/biopython-1.86-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:04f9abdf6cbf0087850de5f8148da0d420c4cb87905bf4de3145ad24a8d55dcd", size = 2669975, upload-time = "2025-10-28T21:26:54.181Z" }, - { url = "https://files.pythonhosted.org/packages/a7/b2/3e6862720d7c51f0fbe7d6d25be72a95486779d9d98122283b4e8032fb40/biopython-1.86-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:187c3c24dd2255e7328f3e0523ab5d6350b73ff562517de0c1922385617101d2", size = 3209367, upload-time = "2025-10-29T00:36:06.522Z" }, - { url = "https://files.pythonhosted.org/packages/d7/cb/61877367bf08670573d62513b239dc65cf2b7488dc74322cc6051da2e55e/biopython-1.86-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1859830b8262785c6b59dfe0c82cddb643974f63b9d2779bb9f3e2c47c0a95da", size = 3235466, upload-time = "2025-10-29T00:36:11.516Z" }, - { url = "https://files.pythonhosted.org/packages/84/1a/3182a77776b76f3f5c64825ee1acf9355f665bed72ee9e8ff49e48f25d98/biopython-1.86-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfd906c47b6fb38e3abb9f52e0c06822e6e82a043d38c2000773692c29db1ed8", size = 3178776, upload-time = "2025-10-28T23:53:41.487Z" }, - { url = "https://files.pythonhosted.org/packages/1a/22/828b08fac8dbc8c1dbc1ad03815137cebc9c78303ec7d21b568544028119/biopython-1.86-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a6ab2c60742f1c8494cfbbe3b7a8b45f0400c8f2b36b686b895d5e4d625f04e", size = 3197586, upload-time = "2025-10-28T23:53:47.136Z" }, - { url = "https://files.pythonhosted.org/packages/36/7a/122aea7653fa93d7eb72978928e80759082efffa70afe0c25a17e18521da/biopython-1.86-cp312-cp312-win32.whl", hash = "sha256:192c61bc3d782c171b7d50bb7d8189d84790d6e3c4b24fd41d1d7ffc7d303efe", size = 2698043, upload-time = "2025-10-28T21:32:39.452Z" }, - { url = "https://files.pythonhosted.org/packages/a9/13/00db03b01e54070d5b0ec9c71eef86e61afa733d9af76e5b9b09f5dc9165/biopython-1.86-cp312-cp312-win_amd64.whl", hash = "sha256:35a6b9c5dcdfb5c2631a313a007f3f41a7d72573ba2b68c962e10ea92096ff3b", size = 2733610, upload-time = "2025-10-28T21:32:34.99Z" }, - { url = "https://files.pythonhosted.org/packages/fd/6e/84d6c66ab93095aa7adb998a8eef045328470eafd36b9237c4db213e587c/biopython-1.86-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fb3a11a98e49428720dca227e2a5bdd57c973ee7c4df3cf6734c0aa13fd134c7", size = 2693185, upload-time = "2025-10-28T21:27:39.709Z" }, - { url = "https://files.pythonhosted.org/packages/12/75/60386f2640f13765b1651f2f26d8b4f893c46ee663df3ca76eda966d4f6a/biopython-1.86-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e161f3d3b6e65fbfd1ce22a01c3e9fa9da789adde4972fd0cc2370795ea5357b", size = 2669980, upload-time = "2025-10-28T21:26:58.839Z" }, - { url = "https://files.pythonhosted.org/packages/dd/de/a39adb98a0552a257219503c236ef17f007598af55326c0d143db52e5a92/biopython-1.86-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aa8c9e92ee6fe59dfe0d2c2daf9a9eec6b812c78328caad038f79163c500218", size = 3209657, upload-time = "2025-10-29T00:36:28.842Z" }, - { url = "https://files.pythonhosted.org/packages/0b/c7/b2e7aca3de8981f4ecb6ab1e0334c3c4a512e5e9898b57b3d8734b086da7/biopython-1.86-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:593ec6a2a4fedec08ddcee1a8a0e0b0ed56835b2714904b352ec4a93d5b9d973", size = 3235774, upload-time = "2025-10-29T00:36:34.07Z" }, - { url = "https://files.pythonhosted.org/packages/52/ed/e6647b0b9cf2bb67347612e8e443b84378c44768a8d8439276e4ba881178/biopython-1.86-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd2f9ebf9b14d67ca92f48779c4f0ba404c35dba3e8b9d6c34d1a3591c3b746d", size = 3178415, upload-time = "2025-10-28T23:54:05.475Z" }, - { url = "https://files.pythonhosted.org/packages/ff/37/f6a14b835842c66a52f212136a99416265f5ce76813d668ceac1cb306357/biopython-1.86-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:137fe9aafd93baa5127d17534b473f6646f92a883f52b34f7c306b800ac50038", size = 3197201, upload-time = "2025-10-28T23:54:10.462Z" }, - { url = "https://files.pythonhosted.org/packages/f2/73/0eac930016c509763c174a0e25e92e6d7a711f6f5de1f7001e54fd5c49f7/biopython-1.86-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e784dc8382430c9893aa084ca18fe8a8815b5811f1c324492ef3f4b54e664fff", size = 3145106, upload-time = "2025-10-28T23:54:15.235Z" }, - { url = "https://files.pythonhosted.org/packages/00/aa/26e836274d03402e8011b04a1714d4ac2f704add303a493e54d2d5646973/biopython-1.86-cp313-cp313-win32.whl", hash = "sha256:5329a777ba90ea624447173046e77c4df2862acc46eea4e94fe2211fe041750f", size = 2698051, upload-time = "2025-10-28T21:32:55.225Z" }, - { url = "https://files.pythonhosted.org/packages/ae/27/fa1f8fa57f2ac8fdc41d14ab36001b8ba0fce5eac01585227b99a4da0e9d/biopython-1.86-cp313-cp313-win_amd64.whl", hash = "sha256:f6f2f1dc75423b15d8a22b8eceae32785736612b6740688526401b8c2d821270", size = 2733649, upload-time = "2025-10-28T21:32:51.052Z" }, - { url = "https://files.pythonhosted.org/packages/a4/2d/5b87ab859d38f2c7d7d1f9df375b4734737c2ef62cf8506983e882419a30/biopython-1.86-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:236ca61aa996f12cbc65a8d6a15abfac70b9ee800656629b784c6a240e7d8dc0", size = 2694733, upload-time = "2025-10-29T00:27:49.142Z" }, - { url = "https://files.pythonhosted.org/packages/24/7e/a80fad6dbfa1335c506b1565d2b3fdd78cda705408a839c5583a9cfca8b6/biopython-1.86-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f96b7441f456c7eecad5c6e61e75b0db1435c489be7cc5e4f97dd4e60921747c", size = 2670131, upload-time = "2025-10-29T00:27:53.758Z" }, - { url = "https://files.pythonhosted.org/packages/2d/0a/6c12e9262b99f395bd66535c4a4203bd70833c11f47ac0730fca6ba2b5f8/biopython-1.86-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d53a78bf960397826219f08f87b061ad7f227527d19986e830eeab60d370b597", size = 3209810, upload-time = "2025-10-29T00:36:45.88Z" }, - { url = "https://files.pythonhosted.org/packages/3a/f9/265211154d2bb4cffe78a57b8e57cfbb165cf41cf3d1b68e2a6b073b3b8a/biopython-1.86-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb86e4383c02fdb2571a38947153346e6f5cd38e22de1df40f54d2a3c51d02a8", size = 3235347, upload-time = "2025-10-29T00:36:51.164Z" }, - { url = "https://files.pythonhosted.org/packages/64/e5/58d8e48d3b4100a7fd8bae97f0dd7179c30f19861841d1a0bb7827e0033e/biopython-1.86-cp314-cp314-win32.whl", hash = "sha256:ffeba620c4786ea836efee235a9c6333b94e922b89de1449a4782dcc15246ff1", size = 2698198, upload-time = "2025-10-29T00:28:02.812Z" }, - { url = "https://files.pythonhosted.org/packages/e2/ca/aa166eb588a2d4eea381c92e5a2a3d09b4b4887b0f0e8f3acf999fb88157/biopython-1.86-cp314-cp314-win_amd64.whl", hash = "sha256:efbb9bc4415a1e2c1c986ba261b02857bc0c9eed098b15493f1cc5c4a1e02409", size = 2734693, upload-time = "2025-10-29T00:27:58.312Z" }, - { url = "https://files.pythonhosted.org/packages/50/da/8c227d701ec9c94d9870b1879982e3dd114da130b0816d3f9b937318d31a/biopython-1.86-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:caa70c1639b3306549605f9273753bdbf8cd6d6d352cecf23afbda3c911694f3", size = 2697389, upload-time = "2025-10-29T00:28:07.037Z" }, - { url = "https://files.pythonhosted.org/packages/8c/1e/66b0b5622ef6a3a14c449d1c8d69749480b37518e4c1e3a8a86fc668dad7/biopython-1.86-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d077f01d1f69f77a26cac46163d4ea45eb4e6509a68feb7f15e665b7e1de0a99", size = 2673857, upload-time = "2025-10-29T00:28:11.488Z" }, - { url = "https://files.pythonhosted.org/packages/76/05/7c8f9800e6960da2007eb75128c8ec0b22e1a0064e8802e8acfad53cdca8/biopython-1.86-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4506ce7dbdf885cb24d1f5439362c3c07f1b6f90761a0d20fe16a2a9ea5702a5", size = 3253007, upload-time = "2025-10-29T00:36:56.066Z" }, - { url = "https://files.pythonhosted.org/packages/14/dd/a2177328d841fda0a12e67c65d06279691e25363a2805f561b3665cae114/biopython-1.86-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcd94717e83ba891ebd9acaecbf05ad38313095ca5706caf6c38fa3f2aa17528", size = 3272883, upload-time = "2025-10-29T00:37:01.189Z" }, - { url = "https://files.pythonhosted.org/packages/ce/04/1aa91f64db5e0728d596fcf7302e2ae2035800c0676e94ea09645a948b91/biopython-1.86-cp314-cp314t-win32.whl", hash = "sha256:2f6b205dcb4101cefa5c615114bd35a19f656abb9d340eb3cf190f829e43800a", size = 2701649, upload-time = "2025-10-29T00:28:20.527Z" }, - { url = "https://files.pythonhosted.org/packages/63/7c/4acaca39102d667175bb3d6502dea91c346f8674c06d5df0dbb678971596/biopython-1.86-cp314-cp314t-win_amd64.whl", hash = "sha256:efeee7c37f2331d2c55704df39e122189cc237ffd7511f34158418ad728131b8", size = 2741364, upload-time = "2025-10-29T00:28:15.752Z" }, -] - [[package]] name = "black" version = "26.1.0" @@ -661,7 +603,8 @@ dependencies = [ { name = "pandas" }, { name = "psm-utils" }, { name = "scikit-learn" }, - { name = "tensorflow" }, + { name = "tensorflow", version = "2.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "tensorflow", version = "2.20.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c9/0d/a715d246c9b2e14c4d7185968757168af81a302db7db3b178008bda79762/deeplc-3.1.13.tar.gz", hash = "sha256:582a221774a8470f59792f53d3495a21b368b982595ce26958686b836968da1a", size = 33094421, upload-time = "2025-09-01T12:46:26.533Z" } wheels = [ @@ -676,7 +619,8 @@ dependencies = [ { name = "numpy" }, { name = "pandas" }, { name = "psm-utils" }, - { name = "tensorflow" }, + { name = "tensorflow", version = "2.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "tensorflow", version = "2.20.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/46/45/aa0869ada8c617708150895a471eb049895a1ccc301e1344bf5794b09001/deeplcretrainer-1.0.2.tar.gz", hash = "sha256:3402c747025a00d0f16b772736226c816b67ecfb633863e326618999daaabedd", size = 15972, upload-time = "2025-02-03T14:38:19.835Z" } wheels = [ @@ -1026,26 +970,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] -[[package]] -name = "ionmob" -version = "0.2.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "biopython" }, - { name = "h5py" }, - { name = "matplotlib" }, - { name = "pandas" }, - { name = "pyarrow" }, - { name = "pyopenms" }, - { name = "scikit-learn" }, - { name = "scipy" }, - { name = "tensorflow" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f5/36/bec5d29dd044d2516226ea3ffc29f07e098f8b4d13fd16da98a89c76b7f8/ionmob-0.2.2.tar.gz", hash = "sha256:cbee393b3611805554ba41a41095603a047ecff7de97571be5240c42f85f7f21", size = 17495346, upload-time = "2023-09-19T09:32:14.86Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/63/59/0da7384d78db85dd51ee1a1be8f7944045ae6a199236b30e8b8d3fad8a21/ionmob-0.2.2-py3-none-any.whl", hash = "sha256:afdb715dcc4e5aa45aa34dbcc26e8ea51aab4bfb0b507611701a03c590c5b8ad", size = 17480141, upload-time = "2023-09-19T09:32:10.656Z" }, -] - [[package]] name = "jinja2" version = "3.1.6" @@ -1060,15 +984,14 @@ wheels = [ [[package]] name = "job-pool" -version = "0.3.4" +version = "0.2.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "threadpoolctl" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d9/ab/d6e9b1639e02276932da473a80b6d21ef524edc3743cc76ed44010b88b48/job_pool-0.3.4.tar.gz", hash = "sha256:7f88bffe74b4ed771ae67c855b1df482597c8e29057f6e7ac2fa6bc447b235ee", size = 11625, upload-time = "2025-04-04T12:30:19.169Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/d8/06223bf9af44df259c5b38eea3f36712aab0d9f2f80ede73d9e890835ccb/job_pool-0.3.4-py3-none-any.whl", hash = "sha256:9f00e000a2521c7570c67a72d67902ec2e973801020fbf1e4b0e3744429684b0", size = 9922, upload-time = "2025-04-04T12:30:17.896Z" }, + { url = "https://files.pythonhosted.org/packages/08/b7/302f76de97ff6024ce006efc3ccb2f9031b72061d77e0a76daf788a636a1/job_pool-0.2.6-py3-none-any.whl", hash = "sha256:75bdc1965455d746ee3bf5710e265b15b06b96f2c39a43dbca505a93a64d57a7", size = 9064, upload-time = "2024-01-03T13:39:05.824Z" }, ] [[package]] @@ -1152,7 +1075,8 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "absl-py" }, { name = "h5py" }, - { name = "ml-dtypes" }, + { name = "ml-dtypes", version = "0.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "ml-dtypes", version = "0.5.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, { name = "namex" }, { name = "numpy" }, { name = "optree" }, @@ -1397,18 +1321,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6c/77/d7f491cbc05303ac6801651aabeb262d43f319288c1ea96c66b1d2692ff3/lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e", size = 3518768, upload-time = "2025-09-22T04:04:57.097Z" }, ] -[[package]] -name = "macholib" -version = "1.16.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "altgraph" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/10/2f/97589876ea967487978071c9042518d28b958d87b17dceb7cdc1d881f963/macholib-1.16.4.tar.gz", hash = "sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362", size = 59427, upload-time = "2025-11-22T08:28:38.373Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/d1/a9f36f8ecdf0fb7c9b1e78c8d7af12b8c8754e74851ac7b94a8305540fc7/macholib-1.16.4-py2.py3-none-any.whl", hash = "sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea", size = 38117, upload-time = "2025-11-22T08:28:36.939Z" }, -] - [[package]] name = "markdown" version = "3.10.2" @@ -1598,12 +1510,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1", size = 53598, upload-time = "2025-12-23T11:36:33.211Z" }, ] +[[package]] +name = "ml-dtypes" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", +] +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/15/76f86faa0902836cc133939732f7611ace68cf54148487a99c539c272dc8/ml_dtypes-0.4.1.tar.gz", hash = "sha256:fad5f2de464fd09127e49b7fd1252b9006fb43d2edc1ff112d390c324af5ca7a", size = 692594, upload-time = "2024-09-13T19:07:11.624Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/76/9835c8609c29f2214359e88f29255fc4aad4ea0f613fb48aa8815ceda1b6/ml_dtypes-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2d55b588116a7085d6e074cf0cdb1d6fa3875c059dddc4d2c94a4cc81c23e975", size = 397973, upload-time = "2024-09-13T19:06:51.748Z" }, + { url = "https://files.pythonhosted.org/packages/7e/99/e68c56fac5de973007a10254b6e17a0362393724f40f66d5e4033f4962c2/ml_dtypes-0.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e138a9b7a48079c900ea969341a5754019a1ad17ae27ee330f7ebf43f23877f9", size = 2185134, upload-time = "2024-09-13T19:06:53.197Z" }, + { url = "https://files.pythonhosted.org/packages/28/bc/6a2344338ea7b61cd7b46fb24ec459360a5a0903b57c55b156c1e46c644a/ml_dtypes-0.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74c6cfb5cf78535b103fde9ea3ded8e9f16f75bc07789054edc7776abfb3d752", size = 2163661, upload-time = "2024-09-13T19:06:54.519Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d3/ddfd9878b223b3aa9a930c6100a99afca5cfab7ea703662e00323acb7568/ml_dtypes-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:274cc7193dd73b35fb26bef6c5d40ae3eb258359ee71cd82f6e96a8c948bdaa6", size = 126727, upload-time = "2024-09-13T19:06:55.897Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1a/99e924f12e4b62139fbac87419698c65f956d58de0dbfa7c028fa5b096aa/ml_dtypes-0.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:827d3ca2097085cf0355f8fdf092b888890bb1b1455f52801a2d7756f056f54b", size = 405077, upload-time = "2024-09-13T19:06:57.538Z" }, + { url = "https://files.pythonhosted.org/packages/8f/8c/7b610bd500617854c8cc6ed7c8cfb9d48d6a5c21a1437a36a4b9bc8a3598/ml_dtypes-0.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:772426b08a6172a891274d581ce58ea2789cc8abc1c002a27223f314aaf894e7", size = 2181554, upload-time = "2024-09-13T19:06:59.196Z" }, + { url = "https://files.pythonhosted.org/packages/c7/c6/f89620cecc0581dc1839e218c4315171312e46c62a62da6ace204bda91c0/ml_dtypes-0.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:126e7d679b8676d1a958f2651949fbfa182832c3cd08020d8facd94e4114f3e9", size = 2160488, upload-time = "2024-09-13T19:07:03.131Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/a742d3c31b2cc8557a48efdde53427fd5f9caa2fa3c9c27d826e78a66f51/ml_dtypes-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:df0fb650d5c582a9e72bb5bd96cfebb2cdb889d89daff621c8fbc60295eba66c", size = 127462, upload-time = "2024-09-13T19:07:04.916Z" }, +] + [[package]] name = "ml-dtypes" version = "0.5.4" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.12.*'", + "python_full_version < '3.12'", +] dependencies = [ - { name = "numpy" }, + { name = "numpy", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0e/4a/c27b42ed9b1c7d13d9ba8b6905dece787d6259152f2309338aed29b2447b/ml_dtypes-0.5.4.tar.gz", hash = "sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453", size = 692314, upload-time = "2025-11-17T22:32:31.031Z" } wheels = [ @@ -1703,7 +1641,6 @@ dependencies = [ { name = "click" }, { name = "customtkinter" }, { name = "deeplc" }, - { name = "deeplcretrainer" }, { name = "im2deep" }, { name = "jinja2" }, { name = "lxml" }, @@ -1723,9 +1660,9 @@ dependencies = [ idxml = [ { name = "pyopenms" }, ] -ionmob = [ - { name = "ionmob" }, - { name = "tensorflow" }, +mumble = [ + { name = "mumble" }, + { name = "rustyms" }, ] [package.dev-dependencies] @@ -1751,14 +1688,8 @@ docs = [ ] installer = [ { name = "cbor2" }, - { name = "deeplc" }, - { name = "im2deep" }, - { name = "ionmob" }, - { name = "ms2rescore-rs" }, { name = "psm-utils" }, - { name = "pyinstaller" }, { name = "pyopenms" }, - { name = "tensorflow" }, ] [package.metadata] @@ -1767,15 +1698,14 @@ requires-dist = [ { name = "click", specifier = ">=7" }, { name = "customtkinter", specifier = ">=5,<6" }, { name = "deeplc", specifier = ">=3.1" }, - { name = "deeplcretrainer" }, { name = "im2deep", specifier = ">=1.1" }, - { name = "ionmob", marker = "extra == 'ionmob'", specifier = ">=0.2" }, { name = "jinja2", specifier = ">=3" }, { name = "lxml", specifier = ">=4.5" }, { name = "mokapot", specifier = "==0.10" }, { name = "ms2pip", specifier = ">=4.0.0" }, { name = "ms2rescore-rs", specifier = ">=0.4" }, - { name = "numpy", specifier = ">=1.25" }, + { name = "mumble", marker = "extra == 'mumble'", specifier = ">=0.2.0" }, + { name = "numpy", specifier = ">=1.25,<2.0" }, { name = "packaging", specifier = ">=25.0" }, { name = "pandas", specifier = ">=1" }, { name = "plotly", specifier = ">=5" }, @@ -1783,10 +1713,10 @@ requires-dist = [ { name = "pyopenms", marker = "extra == 'idxml'", specifier = ">=3.3" }, { name = "pyteomics", specifier = ">=4.7.2" }, { name = "rich", specifier = ">=12" }, - { name = "tensorflow", marker = "extra == 'ionmob'" }, + { name = "rustyms", marker = "extra == 'mumble'", specifier = ">=0.8.3" }, { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=2" }, ] -provides-extras = ["ionmob", "idxml"] +provides-extras = ["mumble", "idxml"] [package.metadata.requires-dev] dev = [ @@ -1810,14 +1740,8 @@ docs = [ ] installer = [ { name = "cbor2" }, - { name = "deeplc", specifier = "==3.1.13" }, - { name = "im2deep", specifier = "==1.2.0" }, - { name = "ionmob", specifier = ">=0.2" }, - { name = "ms2rescore-rs", specifier = "==0.4.3" }, - { name = "psm-utils", specifier = "==1.5.1" }, - { name = "pyinstaller" }, + { name = "psm-utils", specifier = "==1.5.2" }, { name = "pyopenms", specifier = "==3.5" }, - { name = "tensorflow" }, ] [[package]] @@ -1846,6 +1770,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/3c/fa1450558d874fc831267d77c4a2a1abe19da364dbc6d7e6a4d519810119/ms2rescore_rs-0.4.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9636b234c0c2bceca327b4312608ca42960408b9931298fee3de9a2507c31f4", size = 5132573, upload-time = "2025-09-30T12:01:53.042Z" }, ] +[[package]] +name = "mumble" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "psm-utils" }, + { name = "pyteomics" }, + { name = "rich" }, + { name = "rustyms" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a4/d4/6c25f3a04830c13ea5de395722faf567dea53bbb3ff9afc23dfda0a975b7/mumble-0.2.0.tar.gz", hash = "sha256:be62befe66e68b9db82285a28061a917db81bf0fb164b72f214208b26c6a3c39", size = 28065, upload-time = "2025-02-11T15:34:35.102Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/2e/963baa2f937ca0e43acb556d7996a3e284f6b98953030caab7099aab6d0b/mumble-0.2.0-py3-none-any.whl", hash = "sha256:0a822efe46f84a5b1d673c3ffa68808d36865f80fdcd4bb52e3e1ecddf9868c1", size = 24403, upload-time = "2025-02-11T15:34:32.304Z" }, +] + [[package]] name = "mypy-extensions" version = "1.1.0" @@ -2003,83 +1945,26 @@ wheels = [ [[package]] name = "numpy" -version = "2.3.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/77/84dd1d2e34d7e2792a236ba180b5e8fcc1e3e414e761ce0253f63d7f572e/numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10", size = 17034641, upload-time = "2025-11-16T22:49:19.336Z" }, - { url = "https://files.pythonhosted.org/packages/2a/ea/25e26fa5837106cde46ae7d0b667e20f69cbbc0efd64cba8221411ab26ae/numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218", size = 12528324, upload-time = "2025-11-16T22:49:22.582Z" }, - { url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d", size = 5356872, upload-time = "2025-11-16T22:49:25.408Z" }, - { url = "https://files.pythonhosted.org/packages/5c/bb/35ef04afd567f4c989c2060cde39211e4ac5357155c1833bcd1166055c61/numpy-2.3.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:872a5cf366aec6bb1147336480fef14c9164b154aeb6542327de4970282cd2f5", size = 6893148, upload-time = "2025-11-16T22:49:27.549Z" }, - { url = "https://files.pythonhosted.org/packages/f2/2b/05bbeb06e2dff5eab512dfc678b1cc5ee94d8ac5956a0885c64b6b26252b/numpy-2.3.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3095bdb8dd297e5920b010e96134ed91d852d81d490e787beca7e35ae1d89cf7", size = 14557282, upload-time = "2025-11-16T22:49:30.964Z" }, - { url = "https://files.pythonhosted.org/packages/65/fb/2b23769462b34398d9326081fad5655198fcf18966fcb1f1e49db44fbf31/numpy-2.3.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cba086a43d54ca804ce711b2a940b16e452807acebe7852ff327f1ecd49b0d4", size = 16897903, upload-time = "2025-11-16T22:49:34.191Z" }, - { url = "https://files.pythonhosted.org/packages/ac/14/085f4cf05fc3f1e8aa95e85404e984ffca9b2275a5dc2b1aae18a67538b8/numpy-2.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6cf9b429b21df6b99f4dee7a1218b8b7ffbbe7df8764dc0bd60ce8a0708fed1e", size = 16341672, upload-time = "2025-11-16T22:49:37.2Z" }, - { url = "https://files.pythonhosted.org/packages/6f/3b/1f73994904142b2aa290449b3bb99772477b5fd94d787093e4f24f5af763/numpy-2.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:396084a36abdb603546b119d96528c2f6263921c50df3c8fd7cb28873a237748", size = 18838896, upload-time = "2025-11-16T22:49:39.727Z" }, - { url = "https://files.pythonhosted.org/packages/cd/b9/cf6649b2124f288309ffc353070792caf42ad69047dcc60da85ee85fea58/numpy-2.3.5-cp311-cp311-win32.whl", hash = "sha256:b0c7088a73aef3d687c4deef8452a3ac7c1be4e29ed8bf3b366c8111128ac60c", size = 6563608, upload-time = "2025-11-16T22:49:42.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/44/9fe81ae1dcc29c531843852e2874080dc441338574ccc4306b39e2ff6e59/numpy-2.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:a414504bef8945eae5f2d7cb7be2d4af77c5d1cb5e20b296c2c25b61dff2900c", size = 13078442, upload-time = "2025-11-16T22:49:43.99Z" }, - { url = "https://files.pythonhosted.org/packages/6d/a7/f99a41553d2da82a20a2f22e93c94f928e4490bb447c9ff3c4ff230581d3/numpy-2.3.5-cp311-cp311-win_arm64.whl", hash = "sha256:0cd00b7b36e35398fa2d16af7b907b65304ef8bb4817a550e06e5012929830fa", size = 10458555, upload-time = "2025-11-16T22:49:47.092Z" }, - { url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873, upload-time = "2025-11-16T22:49:49.84Z" }, - { url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838, upload-time = "2025-11-16T22:49:52.863Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378, upload-time = "2025-11-16T22:49:55.055Z" }, - { url = "https://files.pythonhosted.org/packages/6d/9c/1ca85fb86708724275103b81ec4cf1ac1d08f465368acfc8da7ab545bdae/numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", size = 6628559, upload-time = "2025-11-16T22:49:57.371Z" }, - { url = "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", size = 14250702, upload-time = "2025-11-16T22:49:59.632Z" }, - { url = "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", size = 16606086, upload-time = "2025-11-16T22:50:02.127Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c5/5ad26fbfbe2012e190cc7d5003e4d874b88bb18861d0829edc140a713021/numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", size = 16025985, upload-time = "2025-11-16T22:50:04.536Z" }, - { url = "https://files.pythonhosted.org/packages/d2/fa/dd48e225c46c819288148d9d060b047fd2a6fb1eb37eae25112ee4cb4453/numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", size = 18542976, upload-time = "2025-11-16T22:50:07.557Z" }, - { url = "https://files.pythonhosted.org/packages/05/79/ccbd23a75862d95af03d28b5c6901a1b7da4803181513d52f3b86ed9446e/numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", size = 6285274, upload-time = "2025-11-16T22:50:10.746Z" }, - { url = "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", size = 12782922, upload-time = "2025-11-16T22:50:12.811Z" }, - { url = "https://files.pythonhosted.org/packages/78/a6/aae5cc2ca78c45e64b9ef22f089141d661516856cf7c8a54ba434576900d/numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", size = 10194667, upload-time = "2025-11-16T22:50:16.16Z" }, - { url = "https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251, upload-time = "2025-11-16T22:50:19.013Z" }, - { url = "https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", size = 12254652, upload-time = "2025-11-16T22:50:21.487Z" }, - { url = "https://files.pythonhosted.org/packages/78/da/8c7738060ca9c31b30e9301ee0cf6c5ffdbf889d9593285a1cead337f9a5/numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", size = 5083172, upload-time = "2025-11-16T22:50:24.562Z" }, - { url = "https://files.pythonhosted.org/packages/a4/b4/ee5bb2537fb9430fd2ef30a616c3672b991a4129bb1c7dcc42aa0abbe5d7/numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903", size = 6622990, upload-time = "2025-11-16T22:50:26.47Z" }, - { url = "https://files.pythonhosted.org/packages/95/03/dc0723a013c7d7c19de5ef29e932c3081df1c14ba582b8b86b5de9db7f0f/numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d", size = 14248902, upload-time = "2025-11-16T22:50:28.861Z" }, - { url = "https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017", size = 16597430, upload-time = "2025-11-16T22:50:31.56Z" }, - { url = "https://files.pythonhosted.org/packages/2a/51/c1e29be863588db58175175f057286900b4b3327a1351e706d5e0f8dd679/numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf", size = 16024551, upload-time = "2025-11-16T22:50:34.242Z" }, - { url = "https://files.pythonhosted.org/packages/83/68/8236589d4dbb87253d28259d04d9b814ec0ecce7cb1c7fed29729f4c3a78/numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce", size = 18533275, upload-time = "2025-11-16T22:50:37.651Z" }, - { url = "https://files.pythonhosted.org/packages/40/56/2932d75b6f13465239e3b7b7e511be27f1b8161ca2510854f0b6e521c395/numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", size = 6277637, upload-time = "2025-11-16T22:50:40.11Z" }, - { url = "https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", size = 12779090, upload-time = "2025-11-16T22:50:42.503Z" }, - { url = "https://files.pythonhosted.org/packages/8f/88/3f41e13a44ebd4034ee17baa384acac29ba6a4fcc2aca95f6f08ca0447d1/numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", size = 10194710, upload-time = "2025-11-16T22:50:44.971Z" }, - { url = "https://files.pythonhosted.org/packages/13/cb/71744144e13389d577f867f745b7df2d8489463654a918eea2eeb166dfc9/numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd", size = 16827292, upload-time = "2025-11-16T22:50:47.715Z" }, - { url = "https://files.pythonhosted.org/packages/71/80/ba9dc6f2a4398e7f42b708a7fdc841bb638d353be255655498edbf9a15a8/numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f", size = 12378897, upload-time = "2025-11-16T22:50:51.327Z" }, - { url = "https://files.pythonhosted.org/packages/2e/6d/db2151b9f64264bcceccd51741aa39b50150de9b602d98ecfe7e0c4bff39/numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a", size = 5207391, upload-time = "2025-11-16T22:50:54.542Z" }, - { url = "https://files.pythonhosted.org/packages/80/ae/429bacace5ccad48a14c4ae5332f6aa8ab9f69524193511d60ccdfdc65fa/numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139", size = 6721275, upload-time = "2025-11-16T22:50:56.794Z" }, - { url = "https://files.pythonhosted.org/packages/74/5b/1919abf32d8722646a38cd527bc3771eb229a32724ee6ba340ead9b92249/numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e", size = 14306855, upload-time = "2025-11-16T22:50:59.208Z" }, - { url = "https://files.pythonhosted.org/packages/a5/87/6831980559434973bebc30cd9c1f21e541a0f2b0c280d43d3afd909b66d0/numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9", size = 16657359, upload-time = "2025-11-16T22:51:01.991Z" }, - { url = "https://files.pythonhosted.org/packages/dd/91/c797f544491ee99fd00495f12ebb7802c440c1915811d72ac5b4479a3356/numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946", size = 16093374, upload-time = "2025-11-16T22:51:05.291Z" }, - { url = "https://files.pythonhosted.org/packages/74/a6/54da03253afcbe7a72785ec4da9c69fb7a17710141ff9ac5fcb2e32dbe64/numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1", size = 18594587, upload-time = "2025-11-16T22:51:08.585Z" }, - { url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940, upload-time = "2025-11-16T22:51:11.541Z" }, - { url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341, upload-time = "2025-11-16T22:51:14.312Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507, upload-time = "2025-11-16T22:51:16.846Z" }, - { url = "https://files.pythonhosted.org/packages/ba/97/1a914559c19e32d6b2e233cf9a6a114e67c856d35b1d6babca571a3e880f/numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82", size = 16735706, upload-time = "2025-11-16T22:51:19.558Z" }, - { url = "https://files.pythonhosted.org/packages/57/d4/51233b1c1b13ecd796311216ae417796b88b0616cfd8a33ae4536330748a/numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0", size = 12264507, upload-time = "2025-11-16T22:51:22.492Z" }, - { url = "https://files.pythonhosted.org/packages/45/98/2fe46c5c2675b8306d0b4a3ec3494273e93e1226a490f766e84298576956/numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63", size = 5093049, upload-time = "2025-11-16T22:51:25.171Z" }, - { url = "https://files.pythonhosted.org/packages/ce/0e/0698378989bb0ac5f1660c81c78ab1fe5476c1a521ca9ee9d0710ce54099/numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9", size = 6626603, upload-time = "2025-11-16T22:51:27Z" }, - { url = "https://files.pythonhosted.org/packages/5e/a6/9ca0eecc489640615642a6cbc0ca9e10df70df38c4d43f5a928ff18d8827/numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b", size = 14262696, upload-time = "2025-11-16T22:51:29.402Z" }, - { url = "https://files.pythonhosted.org/packages/c8/f6/07ec185b90ec9d7217a00eeeed7383b73d7e709dae2a9a021b051542a708/numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520", size = 16597350, upload-time = "2025-11-16T22:51:32.167Z" }, - { url = "https://files.pythonhosted.org/packages/75/37/164071d1dde6a1a84c9b8e5b414fa127981bad47adf3a6b7e23917e52190/numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c", size = 16040190, upload-time = "2025-11-16T22:51:35.403Z" }, - { url = "https://files.pythonhosted.org/packages/08/3c/f18b82a406b04859eb026d204e4e1773eb41c5be58410f41ffa511d114ae/numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8", size = 18536749, upload-time = "2025-11-16T22:51:39.698Z" }, - { url = "https://files.pythonhosted.org/packages/40/79/f82f572bf44cf0023a2fe8588768e23e1592585020d638999f15158609e1/numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248", size = 6335432, upload-time = "2025-11-16T22:51:42.476Z" }, - { url = "https://files.pythonhosted.org/packages/a3/2e/235b4d96619931192c91660805e5e49242389742a7a82c27665021db690c/numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e", size = 12919388, upload-time = "2025-11-16T22:51:45.275Z" }, - { url = "https://files.pythonhosted.org/packages/07/2b/29fd75ce45d22a39c61aad74f3d718e7ab67ccf839ca8b60866054eb15f8/numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2", size = 10476651, upload-time = "2025-11-16T22:51:47.749Z" }, - { url = "https://files.pythonhosted.org/packages/17/e1/f6a721234ebd4d87084cfa68d081bcba2f5cfe1974f7de4e0e8b9b2a2ba1/numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41", size = 16834503, upload-time = "2025-11-16T22:51:50.443Z" }, - { url = "https://files.pythonhosted.org/packages/5c/1c/baf7ffdc3af9c356e1c135e57ab7cf8d247931b9554f55c467efe2c69eff/numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad", size = 12381612, upload-time = "2025-11-16T22:51:53.609Z" }, - { url = "https://files.pythonhosted.org/packages/74/91/f7f0295151407ddc9ba34e699013c32c3c91944f9b35fcf9281163dc1468/numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39", size = 5210042, upload-time = "2025-11-16T22:51:56.213Z" }, - { url = "https://files.pythonhosted.org/packages/2e/3b/78aebf345104ec50dd50a4d06ddeb46a9ff5261c33bcc58b1c4f12f85ec2/numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20", size = 6724502, upload-time = "2025-11-16T22:51:58.584Z" }, - { url = "https://files.pythonhosted.org/packages/02/c6/7c34b528740512e57ef1b7c8337ab0b4f0bddf34c723b8996c675bc2bc91/numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52", size = 14308962, upload-time = "2025-11-16T22:52:01.698Z" }, - { url = "https://files.pythonhosted.org/packages/80/35/09d433c5262bc32d725bafc619e095b6a6651caf94027a03da624146f655/numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b", size = 16655054, upload-time = "2025-11-16T22:52:04.267Z" }, - { url = "https://files.pythonhosted.org/packages/7a/ab/6a7b259703c09a88804fa2430b43d6457b692378f6b74b356155283566ac/numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3", size = 16091613, upload-time = "2025-11-16T22:52:08.651Z" }, - { url = "https://files.pythonhosted.org/packages/c2/88/330da2071e8771e60d1038166ff9d73f29da37b01ec3eb43cb1427464e10/numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227", size = 18591147, upload-time = "2025-11-16T22:52:11.453Z" }, - { url = "https://files.pythonhosted.org/packages/51/41/851c4b4082402d9ea860c3626db5d5df47164a712cb23b54be028b184c1c/numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5", size = 6479806, upload-time = "2025-11-16T22:52:14.641Z" }, - { url = "https://files.pythonhosted.org/packages/90/30/d48bde1dfd93332fa557cff1972fbc039e055a52021fbef4c2c4b1eefd17/numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf", size = 13105760, upload-time = "2025-11-16T22:52:17.975Z" }, - { url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459, upload-time = "2025-11-16T22:52:20.55Z" }, - { url = "https://files.pythonhosted.org/packages/c6/65/f9dea8e109371ade9c782b4e4756a82edf9d3366bca495d84d79859a0b79/numpy-2.3.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f0963b55cdd70fad460fa4c1341f12f976bb26cb66021a5580329bd498988310", size = 16910689, upload-time = "2025-11-16T22:52:23.247Z" }, - { url = "https://files.pythonhosted.org/packages/00/4f/edb00032a8fb92ec0a679d3830368355da91a69cab6f3e9c21b64d0bb986/numpy-2.3.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f4255143f5160d0de972d28c8f9665d882b5f61309d8362fdd3e103cf7bf010c", size = 12457053, upload-time = "2025-11-16T22:52:26.367Z" }, - { url = "https://files.pythonhosted.org/packages/16/a4/e8a53b5abd500a63836a29ebe145fc1ab1f2eefe1cfe59276020373ae0aa/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:a4b9159734b326535f4dd01d947f919c6eefd2d9827466a696c44ced82dfbc18", size = 5285635, upload-time = "2025-11-16T22:52:29.266Z" }, - { url = "https://files.pythonhosted.org/packages/a3/2f/37eeb9014d9c8b3e9c55bc599c68263ca44fdbc12a93e45a21d1d56df737/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2feae0d2c91d46e59fcd62784a3a83b3fb677fead592ce51b5a6fbb4f95965ff", size = 6801770, upload-time = "2025-11-16T22:52:31.421Z" }, - { url = "https://files.pythonhosted.org/packages/7d/e4/68d2f474df2cb671b2b6c2986a02e520671295647dad82484cde80ca427b/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffac52f28a7849ad7576293c0cb7b9f08304e8f7d738a8cb8a90ec4c55a998eb", size = 14391768, upload-time = "2025-11-16T22:52:33.593Z" }, - { url = "https://files.pythonhosted.org/packages/b8/50/94ccd8a2b141cb50651fddd4f6a48874acb3c91c8f0842b08a6afc4b0b21/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c0e9e7eea69588479ebf4a8a270d5ac22763cc5854e9a7eae952a3908103f7", size = 16729263, upload-time = "2025-11-16T22:52:36.369Z" }, - { url = "https://files.pythonhosted.org/packages/2d/ee/346fa473e666fe14c52fcdd19ec2424157290a032d4c41f98127bfb31ac7/numpy-2.3.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425", size = 12967213, upload-time = "2025-11-16T22:52:39.38Z" }, +version = "1.26.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554, upload-time = "2024-02-05T23:51:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", size = 13997127, upload-time = "2024-02-05T23:52:15.314Z" }, + { url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", size = 14222994, upload-time = "2024-02-05T23:52:47.569Z" }, + { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005, upload-time = "2024-02-05T23:53:15.637Z" }, + { url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", size = 13885297, upload-time = "2024-02-05T23:53:42.16Z" }, + { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567, upload-time = "2024-02-05T23:54:11.696Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812, upload-time = "2024-02-05T23:54:26.453Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913, upload-time = "2024-02-05T23:54:53.933Z" }, + { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901, upload-time = "2024-02-05T23:55:32.801Z" }, + { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868, upload-time = "2024-02-05T23:55:56.28Z" }, + { url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109, upload-time = "2024-02-05T23:56:20.368Z" }, + { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613, upload-time = "2024-02-05T23:56:56.054Z" }, + { url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172, upload-time = "2024-02-05T23:57:21.56Z" }, + { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643, upload-time = "2024-02-05T23:57:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803, upload-time = "2024-02-05T23:58:08.963Z" }, + { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754, upload-time = "2024-02-05T23:58:36.364Z" }, ] [[package]] @@ -2284,15 +2169,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] -[[package]] -name = "pefile" -version = "2024.8.26" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/03/4f/2750f7f6f025a1507cd3b7218691671eecfd0bbebebe8b39aa0fe1d360b8/pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632", size = 76008, upload-time = "2024-08-26T20:58:38.155Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f", size = 74766, upload-time = "2024-08-26T21:01:02.632Z" }, -] - [[package]] name = "pillow" version = "12.1.0" @@ -2427,10 +2303,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, ] +[[package]] +name = "protobuf" +version = "5.29.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/57/394a763c103e0edf87f0938dafcd918d53b4c011dfc5c8ae80f3b0452dbb/protobuf-5.29.6.tar.gz", hash = "sha256:da9ee6a5424b6b30fd5e45c5ea663aef540ca95f9ad99d1e887e819cdf9b8723", size = 425623, upload-time = "2026-02-04T22:54:40.584Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/88/9ee58ff7863c479d6f8346686d4636dd4c415b0cbeed7a6a7d0617639c2a/protobuf-5.29.6-cp310-abi3-win32.whl", hash = "sha256:62e8a3114992c7c647bce37dcc93647575fc52d50e48de30c6fcb28a6a291eb1", size = 423357, upload-time = "2026-02-04T22:54:25.805Z" }, + { url = "https://files.pythonhosted.org/packages/1c/66/2dc736a4d576847134fb6d80bd995c569b13cdc7b815d669050bf0ce2d2c/protobuf-5.29.6-cp310-abi3-win_amd64.whl", hash = "sha256:7e6ad413275be172f67fdee0f43484b6de5a904cc1c3ea9804cb6fe2ff366eda", size = 435175, upload-time = "2026-02-04T22:54:28.592Z" }, + { url = "https://files.pythonhosted.org/packages/06/db/49b05966fd208ae3f44dcd33837b6243b4915c57561d730a43f881f24dea/protobuf-5.29.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:b5a169e664b4057183a34bdc424540e86eea47560f3c123a0d64de4e137f9269", size = 418619, upload-time = "2026-02-04T22:54:30.266Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d7/48cbf6b0c3c39761e47a99cb483405f0fde2be22cf00d71ef316ce52b458/protobuf-5.29.6-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:a8866b2cff111f0f863c1b3b9e7572dc7eaea23a7fae27f6fc613304046483e6", size = 320284, upload-time = "2026-02-04T22:54:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/e3/dd/cadd6ec43069247d91f6345fa7a0d2858bef6af366dbd7ba8f05d2c77d3b/protobuf-5.29.6-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:e3387f44798ac1106af0233c04fb8abf543772ff241169946f698b3a9a3d3ab9", size = 320478, upload-time = "2026-02-04T22:54:32.909Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cb/e3065b447186cb70aa65acc70c86baf482d82bf75625bf5a2c4f6919c6a3/protobuf-5.29.6-py3-none-any.whl", hash = "sha256:6b9edb641441b2da9fa8f428760fc136a49cf97a52076010cf22a2ff73438a86", size = 173126, upload-time = "2026-02-04T22:54:39.462Z" }, +] + [[package]] name = "protobuf" version = "6.33.5" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.12.*'", + "python_full_version < '3.12'", +] sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" }, @@ -2459,7 +2356,7 @@ wheels = [ [[package]] name = "psm-utils" -version = "1.5.1" +version = "1.5.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -2473,9 +2370,9 @@ dependencies = [ { name = "rich" }, { name = "sqlalchemy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/25/45569f23641e35f4c93ed6379a6cfede2875b3bdcadd8ef78f4686bed7f1/psm_utils-1.5.1.tar.gz", hash = "sha256:9044403112c035d0d130f8ce1bb285eb877f2bbb10734568663ab7f3ba5c6fa5", size = 80273, upload-time = "2025-12-22T15:50:50.326Z" } +sdist = { url = "https://files.pythonhosted.org/packages/71/59/73a7b64ce948f6870e119836e1b4c6fdd8dc66860023b06eea42eba7863d/psm_utils-1.5.2.tar.gz", hash = "sha256:f49f444ad7bf6de964d2f04c114582f89e15979ff56ad813019b408cce743218", size = 80300, upload-time = "2026-02-12T12:48:15.027Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/7d/99922e224e841eab2f1ed1883566b7b786f7a65e0c02998406ef6de6d42b/psm_utils-1.5.1-py3-none-any.whl", hash = "sha256:20daf92d67500aa52044cf4ea5d115ed192da3de73d7ef14344b18b0faa442a5", size = 94932, upload-time = "2025-12-22T15:50:49.189Z" }, + { url = "https://files.pythonhosted.org/packages/e8/11/5cfffad59b509a07a249e7b827d942ef08c5e2a32ea0ea31c24634e02838/psm_utils-1.5.2-py3-none-any.whl", hash = "sha256:abcb312d6a72ee8a9af93796431490d0380167b88f1eba57e652ea2c5e714a39", size = 94936, upload-time = "2026-02-12T12:48:13.497Z" }, ] [[package]] @@ -2658,47 +2555,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] -[[package]] -name = "pyinstaller" -version = "6.18.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "altgraph" }, - { name = "macholib", marker = "sys_platform == 'darwin'" }, - { name = "packaging" }, - { name = "pefile", marker = "sys_platform == 'win32'" }, - { name = "pyinstaller-hooks-contrib" }, - { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, - { name = "setuptools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9f/b8/0fe3359920b0a4e7008e0e93ff383003763e3eee3eb31a07c52868722960/pyinstaller-6.18.0.tar.gz", hash = "sha256:cdc507542783511cad4856fce582fdc37e9f29665ca596889c663c83ec8c6ec9", size = 4034976, upload-time = "2026-01-13T03:13:23.886Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/73/e6/51b0146a1a3eec619e58f5d69fb4e3d0f65a31cbddbeef557c9bb83eeed9/pyinstaller-6.18.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:cb7aa5a71bfa7c0af17a4a4e21855663c89e4bd7c40f1d337c8370636d8847c3", size = 1040056, upload-time = "2026-01-13T03:12:15.397Z" }, - { url = "https://files.pythonhosted.org/packages/4c/9c/a3634c0ec8e1ed31b373b548848b5c0b39b56edc191cf737e697d484ec23/pyinstaller-6.18.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:07785459b3bf8a48889eac0b4d0667ade84aef8930ce030bc7cbb32f41283b33", size = 734971, upload-time = "2026-01-13T03:12:20.912Z" }, - { url = "https://files.pythonhosted.org/packages/2c/04/6756442078ccfcd552ccce636be1574035e62f827ffa1f5d8a0382682546/pyinstaller-6.18.0-py3-none-manylinux2014_i686.whl", hash = "sha256:f998675b7ccb2dabbb1dc2d6f18af61d55428ad6d38e6c4d700417411b697d37", size = 746637, upload-time = "2026-01-13T03:12:29.302Z" }, - { url = "https://files.pythonhosted.org/packages/54/39/fbc56519000cdbf450f472692a7b9b55d42077ce8529f1be631db7b75a36/pyinstaller-6.18.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:779817a0cf69604cddcdb5be1fd4959dc2ce048d6355c73e5da97884df2f3387", size = 744343, upload-time = "2026-01-13T03:12:33.369Z" }, - { url = "https://files.pythonhosted.org/packages/36/f2/50887badf282fee776e83d1e4feab74c026f50a1ea16e109ed939e32aa28/pyinstaller-6.18.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:31b5d109f8405be0b7cddcede43e7b074792bc9a5bbd54ec000a3e779183c2af", size = 741084, upload-time = "2026-01-13T03:12:37.528Z" }, - { url = "https://files.pythonhosted.org/packages/1c/08/3a1419183e4713ef77d912ecbdd6ef858689ed9deb34d547133f724ca745/pyinstaller-6.18.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:4328c9837f1aef4fe1a127d4ff1b09a12ce53c827ce87c94117628b0e1fd098b", size = 740943, upload-time = "2026-01-13T03:12:41.589Z" }, - { url = "https://files.pythonhosted.org/packages/c2/47/309305e36d116f1434b42d91c420ff951fa79b2c398bbd59930c830450be/pyinstaller-6.18.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:3638fc81eb948e5e5eab1d4ad8f216e3fec6d4a350648304f0adb227b746ee5e", size = 740107, upload-time = "2026-01-13T03:12:45.694Z" }, - { url = "https://files.pythonhosted.org/packages/83/0f/a59a95cd1df59ddbc9e74d5a663387551333bcf19a5dd3086f5c81a2e83c/pyinstaller-6.18.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe59da34269e637f97fd3c43024f764586fc319141d245ff1a2e9af1036aa3", size = 739843, upload-time = "2026-01-13T03:12:49.728Z" }, - { url = "https://files.pythonhosted.org/packages/9a/09/e7a870e7205cdbd2f8785010a5d3fe48a9df2591156ee34a8b29b774fa14/pyinstaller-6.18.0-py3-none-win32.whl", hash = "sha256:496205e4fa92ec944f9696eb597962a83aef4d4c3479abfab83d730e1edf016b", size = 1323811, upload-time = "2026-01-13T03:12:55.717Z" }, - { url = "https://files.pythonhosted.org/packages/fb/d5/48eef2002b6d3937ceac2717fe17e9ca3a43a4c9826bafee367dfc75ba85/pyinstaller-6.18.0-py3-none-win_amd64.whl", hash = "sha256:976fabd90ecfbda47571c87055ad73413ec615ff7dea35e12a4304174de78de9", size = 1384389, upload-time = "2026-01-13T03:13:01.993Z" }, - { url = "https://files.pythonhosted.org/packages/1b/8d/1a88e6e94107de3ea1c842fd59c3aa132d344ad8e52ea458ffa9a748726e/pyinstaller-6.18.0-py3-none-win_arm64.whl", hash = "sha256:dba4b70e3c9ba09aab51152c72a08e58a751851548f77ad35944d32a300c8381", size = 1324869, upload-time = "2026-01-13T03:13:08.192Z" }, -] - -[[package]] -name = "pyinstaller-hooks-contrib" -version = "2026.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, - { name = "setuptools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/31/8f/8052ff65067697ee80fde45b9731842e160751c41ac5690ba232c22030e8/pyinstaller_hooks_contrib-2026.0.tar.gz", hash = "sha256:0120893de491a000845470ca9c0b39284731ac6bace26f6849dea9627aaed48e", size = 170311, upload-time = "2026-01-20T00:15:23.922Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/b1/9da6ec3e88696018ee7bb9dc4a7310c2cfaebf32923a19598cd342767c10/pyinstaller_hooks_contrib-2026.0-py3-none-any.whl", hash = "sha256:0590db8edeba3e6c30c8474937021f5cd39c0602b4d10f74a064c73911efaca5", size = 452318, upload-time = "2026-01-20T00:15:21.88Z" }, -] - [[package]] name = "pyopenms" version = "3.5.0" @@ -2835,15 +2691,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, ] -[[package]] -name = "pywin32-ctypes" -version = "0.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, -] - [[package]] name = "pyyaml" version = "6.0.3" @@ -3141,6 +2988,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" }, ] +[[package]] +name = "rustyms" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/62/0f/a672488611347d7007e7af4670fbc498c423bae3f6e6bf78176616c57866/rustyms-0.10.0.tar.gz", hash = "sha256:96b5c772d9d2a2a5a979aa4bd05e8ba80dfe202f4a2b4e24d9ccc33232c7b05e", size = 8231061, upload-time = "2025-08-12T14:08:09.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/0a/7ebe970d930dffb9ce880e8ca2f863579d7d8c462637e40472d0fbcc7cdb/rustyms-0.10.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4e154e73ff0a1fb45e67d599e2b23a943c0981eb144fbc6f7953c8ed7fe2520b", size = 9111434, upload-time = "2025-08-12T14:07:01.348Z" }, + { url = "https://files.pythonhosted.org/packages/75/7a/70ade98bdd827e7495df25af8df8e8dd255c2cdf6b62119fdb451eb73894/rustyms-0.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b094b58f9ebe4f768d122d9f065be283b6986845068f1856dce44013836c9c", size = 9477531, upload-time = "2025-08-12T14:07:03.386Z" }, + { url = "https://files.pythonhosted.org/packages/18/fa/b2bbbadde0c4799ebad1a8f202628dbdaa38e30910ff55f9db81e5698875/rustyms-0.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5638e348424356a24f4ed17955b532a6c36335f37cf74b173e4090b64832e2f4", size = 16018517, upload-time = "2025-08-12T14:07:05.95Z" }, + { url = "https://files.pythonhosted.org/packages/25/b0/80a7cb5e56efd6f60006103b308431b6af31709dd0b27be06dce2fd4588d/rustyms-0.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:615a14b77caf5babf6b6316cfcbfac1f74c562a0b90d522a68619f900d7fb451", size = 16438023, upload-time = "2025-08-12T14:07:08.9Z" }, + { url = "https://files.pythonhosted.org/packages/3b/a6/5ea649fd0c6373688c7b102d2f9ddcc572ba8d63166485fc52378725d0c3/rustyms-0.10.0-cp311-cp311-win32.whl", hash = "sha256:00e803fb56b7c4bd7837a4f11b1515753222dcdaa9ae2032516427fc8c268b92", size = 8791466, upload-time = "2025-08-12T14:07:11.36Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/428e11b762f5d7d94154b414097c72fcf43eff7cfe07b36dcbde26080bad/rustyms-0.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:05330b02d4871ad6cf0d69a12144f2c5a02f35ae5a5a55671353bdb30b9ae79c", size = 8943539, upload-time = "2025-08-12T14:07:13.666Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2c/fbb558cedfdf06cf21debf5b8a52b28a1a40f0149f667ec43a4011fbb277/rustyms-0.10.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:da7d8b4074351727995648d1abffe0467b46fd2d304e5b7bbddb1fa2832e9fce", size = 9108721, upload-time = "2025-08-12T14:07:15.654Z" }, + { url = "https://files.pythonhosted.org/packages/9c/32/13131b86cd0cde149bd377c03781af8b9d25bc60298f79ad015b754372ec/rustyms-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5d7f3f6c0234c8c87e28428ef769025c48add2a522b8c09d0c7085cb3b97ae1e", size = 9475022, upload-time = "2025-08-12T14:07:18Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f7/9951204de6cbebca5d898a8a2880cdd56883ef689efb1d22db911b74d23e/rustyms-0.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59f0de55ab71b1b6d9d2dfc6f757fe177c6e96d901bb499c9da8f5e80c537b7c", size = 16016367, upload-time = "2025-08-12T14:07:20.08Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ea/64fe3af11515706df9fe4ae53ce8d73c0ca45fff024cc876423785baea1b/rustyms-0.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c37f44c2df19d198f4296e508c42c2f39d40bef7fc686629b9aac601a01a2c44", size = 16438561, upload-time = "2025-08-12T14:07:22.621Z" }, + { url = "https://files.pythonhosted.org/packages/34/c0/c5ad574919005dd4abe7640a35276d3a317dd37a5e736f3bb579d3c34793/rustyms-0.10.0-cp312-cp312-win32.whl", hash = "sha256:44da7082970627ec63e824ee04b7327d769c6723d630513cd45648f3dd8dd7e6", size = 8791301, upload-time = "2025-08-12T14:07:24.743Z" }, + { url = "https://files.pythonhosted.org/packages/e3/58/9d5b76da4c303d52b3e883639181f3497c84b6f21ccb8d425eb529574e6f/rustyms-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:22ac22917e7cf2859631ad18959c8ed0846a675f800ac3f862ce6168259bfece", size = 8940093, upload-time = "2025-08-12T14:07:26.96Z" }, + { url = "https://files.pythonhosted.org/packages/98/6b/58a2cdaa414ed22dafb131feffe730b56a0b90859807944c863a572a9698/rustyms-0.10.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9ed589275ee316083690807de2fec4487963333a7efddb1cf2c9e78942c4feeb", size = 9108719, upload-time = "2025-08-12T14:07:29.367Z" }, + { url = "https://files.pythonhosted.org/packages/18/5c/570523bb82a6292532827f196433f57d94a11e29a381b32192bbc0b5cddb/rustyms-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15c81c5c7e16d1909033639bdc5bb829cbeab8a7e1a5ea58212f35ea0462e4db", size = 9475019, upload-time = "2025-08-12T14:07:31.61Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0e/38556b15ef251fa334e61f767686bfd1b0b68fe052ab725bc7d4bb835bbb/rustyms-0.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a80ddff6056fb1e913ead5c7db1919517ef02482cabd0a88fcb8f88c0c1b086a", size = 16015464, upload-time = "2025-08-12T14:07:33.95Z" }, + { url = "https://files.pythonhosted.org/packages/ef/cf/426268a1965e638a0ff54d3e5e1766bdcc6baca0228ab6873177270c7001/rustyms-0.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7cde07389125502c44138a2b4bfa60d9a69781d589b80ca633b84c2f51f8da4", size = 16437436, upload-time = "2025-08-12T14:07:36.162Z" }, + { url = "https://files.pythonhosted.org/packages/ab/bd/9598924c64445bced13ce401bcb6912380f9fe5fead10d2fcc5f623eaac6/rustyms-0.10.0-cp313-cp313-win32.whl", hash = "sha256:fabd598c2d69ca162af9a5367b96c1310402a557e0526258bac2a4add231d9a6", size = 8791297, upload-time = "2025-08-12T14:07:38.719Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9b/d5ab6cfb78d0a93fc5a2778a368c31005deadbc8e55f99e898732aa5f38d/rustyms-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:61b623798095eb30569094eff632c6ba81e88332bd2a5228d75851e038fc6057", size = 8940139, upload-time = "2025-08-12T14:07:42.286Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4f/9d4ab110706520b5e007dd70108dc4ef8d0490a40383dbd32c7fdecc9f1c/rustyms-0.10.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0d4c044f5bafca20eb0e0403f69e0935b3d2b3076ee2588fc359b0794015e7d", size = 16007487, upload-time = "2025-08-12T14:07:44.651Z" }, + { url = "https://files.pythonhosted.org/packages/95/15/e6e0b252eac68cbed6fcdadbecb5c8d8c65fca6652668f3ed38af30628b9/rustyms-0.10.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06b9d447a07ade425731af9636d58196e124314fa3367cac7a5f1db13a7dc48f", size = 16011286, upload-time = "2025-08-12T14:08:01.783Z" }, + { url = "https://files.pythonhosted.org/packages/33/f3/6e167086236cba122022d92b8121c9581a780fe22342fb99d853d3e71943/rustyms-0.10.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7354422f7767536cf7104bcb12a467214e4678c354781fd6cbae8e3b670114ab", size = 16431938, upload-time = "2025-08-12T14:08:04.054Z" }, +] + [[package]] name = "scikit-learn" version = "1.8.0" @@ -3559,21 +3435,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, ] +[[package]] +name = "tensorboard" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", +] +dependencies = [ + { name = "absl-py", marker = "python_full_version >= '3.13'" }, + { name = "grpcio", marker = "python_full_version >= '3.13'" }, + { name = "markdown", marker = "python_full_version >= '3.13'" }, + { name = "numpy", marker = "python_full_version >= '3.13'" }, + { name = "packaging", marker = "python_full_version >= '3.13'" }, + { name = "protobuf", version = "5.29.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "setuptools", marker = "python_full_version >= '3.13'" }, + { name = "six", marker = "python_full_version >= '3.13'" }, + { name = "tensorboard-data-server", marker = "python_full_version >= '3.13'" }, + { name = "werkzeug", marker = "python_full_version >= '3.13'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/de/021c1d407befb505791764ad2cbd56ceaaa53a746baed01d2e2143f05f18/tensorboard-2.18.0-py3-none-any.whl", hash = "sha256:107ca4821745f73e2aefa02c50ff70a9b694f39f790b11e6f682f7d326745eab", size = 5503036, upload-time = "2024-09-25T21:21:50.169Z" }, +] + [[package]] name = "tensorboard" version = "2.20.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.12.*'", + "python_full_version < '3.12'", +] dependencies = [ - { name = "absl-py" }, - { name = "grpcio" }, - { name = "markdown" }, - { name = "numpy" }, - { name = "packaging" }, - { name = "pillow" }, - { name = "protobuf" }, - { name = "setuptools" }, - { name = "tensorboard-data-server" }, - { name = "werkzeug" }, + { name = "absl-py", marker = "python_full_version < '3.13'" }, + { name = "grpcio", marker = "python_full_version < '3.13'" }, + { name = "markdown", marker = "python_full_version < '3.13'" }, + { name = "numpy", marker = "python_full_version < '3.13'" }, + { name = "packaging", marker = "python_full_version < '3.13'" }, + { name = "pillow", marker = "python_full_version < '3.13'" }, + { name = "protobuf", version = "6.33.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "setuptools", marker = "python_full_version < '3.13'" }, + { name = "tensorboard-data-server", marker = "python_full_version < '3.13'" }, + { name = "werkzeug", marker = "python_full_version < '3.13'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/9c/d9/a5db55f88f258ac669a92858b70a714bbbd5acd993820b41ec4a96a4d77f/tensorboard-2.20.0-py3-none-any.whl", hash = "sha256:9dc9f978cb84c0723acf9a345d96c184f0293d18f166bb8d59ee098e6cfaaba6", size = 5525680, upload-time = "2025-07-17T19:20:49.638Z" }, @@ -3589,32 +3492,77 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/73/c6/825dab04195756cf8ff2e12698f22513b3db2f64925bdd41671bfb33aaa5/tensorboard_data_server-0.7.2-py3-none-manylinux_2_31_x86_64.whl", hash = "sha256:ef687163c24185ae9754ed5650eb5bc4d84ff257aabdc33f0cc6f74d8ba54530", size = 6590363, upload-time = "2023-10-23T21:23:35.583Z" }, ] +[[package]] +name = "tensorflow" +version = "2.18.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", +] +dependencies = [ + { name = "absl-py", marker = "python_full_version >= '3.13'" }, + { name = "astunparse", marker = "python_full_version >= '3.13'" }, + { name = "flatbuffers", marker = "python_full_version >= '3.13'" }, + { name = "gast", marker = "python_full_version >= '3.13'" }, + { name = "google-pasta", marker = "python_full_version >= '3.13'" }, + { name = "grpcio", marker = "python_full_version >= '3.13'" }, + { name = "h5py", marker = "python_full_version >= '3.13'" }, + { name = "keras", marker = "python_full_version >= '3.13'" }, + { name = "libclang", marker = "python_full_version >= '3.13'" }, + { name = "ml-dtypes", version = "0.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "numpy", marker = "python_full_version >= '3.13'" }, + { name = "opt-einsum", marker = "python_full_version >= '3.13'" }, + { name = "packaging", marker = "python_full_version >= '3.13'" }, + { name = "protobuf", version = "5.29.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "requests", marker = "python_full_version >= '3.13'" }, + { name = "setuptools", marker = "python_full_version >= '3.13'" }, + { name = "six", marker = "python_full_version >= '3.13'" }, + { name = "tensorboard", version = "2.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "termcolor", marker = "python_full_version >= '3.13'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.13'" }, + { name = "wrapt", marker = "python_full_version >= '3.13'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/88/57e2acd11a2587cc5c0a6612a389a57f3bda3cd60d132934cb7a9bb00a81/tensorflow-2.18.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:661029cd769b311db910b79a3a6ef50a5a61ecc947172228c777a49989722508", size = 239549037, upload-time = "2025-03-12T00:12:38.202Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b3/902588dcffbc0603893f1df482840ff9c596430155d62e159bc8fc155230/tensorflow-2.18.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a6485edd2148f70d011dbd1d8dc2c775e91774a5a159466e83d0d1f21580944", size = 231937898, upload-time = "2025-03-12T00:12:47.544Z" }, + { url = "https://files.pythonhosted.org/packages/45/c6/05d862ebeaaf63343dffc4f97dab62ac493e8c2bbc6b1a256199bcc78ed4/tensorflow-2.18.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9f87e5d2a680a4595f5dc30daf6bbaec9d4129b46d7ef1b2af63c46ac7d2828", size = 615510377, upload-time = "2025-03-12T00:13:03.792Z" }, + { url = "https://files.pythonhosted.org/packages/28/2a/5f5ade4be81e521a16e143234747570ffd0d1a90e001ecc2688aa54bb419/tensorflow-2.18.1-cp311-cp311-win_amd64.whl", hash = "sha256:99223d0dde08aec4ceebb3bf0f80da7802e18462dab0d5048225925c064d2af7", size = 369183850, upload-time = "2025-03-12T00:13:24.786Z" }, + { url = "https://files.pythonhosted.org/packages/67/8c/1cad54f8634897ad9421de8f558df9aa63d3f2747eb803ce5dbb2db1ef5b/tensorflow-2.18.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:98afa9c7f21481cdc6ccd09507a7878d533150fbb001840cc145e2132eb40942", size = 239622377, upload-time = "2025-03-12T00:13:36.89Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c2/35a3542a91f4ffd4cf01e72d7f0fb59596cd5f467ff64878b0caef8e0f31/tensorflow-2.18.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ba52b9c06ab8102b31e50acfaf56899b923171e603c8942f2bfeb181d6bb59e", size = 231996787, upload-time = "2025-03-12T00:13:47.54Z" }, + { url = "https://files.pythonhosted.org/packages/64/42/812539a8878c242eb0bacf106f5ea8936c2cc4d7f663868bd872a79772ac/tensorflow-2.18.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:442d2a774811789a8ad948e7286cb950fe3d87d3754e8cc6449d53b03dbfdaa6", size = 615623178, upload-time = "2025-03-12T00:14:03.541Z" }, + { url = "https://files.pythonhosted.org/packages/20/28/9c5e935b76eebdf46df524980d49700a9c9af56abc8c62bfd93f57709563/tensorflow-2.18.1-cp312-cp312-win_amd64.whl", hash = "sha256:210baf6d421f3e044b6e09efd04494a33b75334922fe6cf11970e2885172620a", size = 369234070, upload-time = "2025-03-12T00:14:23.423Z" }, +] + [[package]] name = "tensorflow" version = "2.20.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.12.*'", + "python_full_version < '3.12'", +] dependencies = [ - { name = "absl-py" }, - { name = "astunparse" }, - { name = "flatbuffers" }, - { name = "gast" }, - { name = "google-pasta" }, - { name = "grpcio" }, - { name = "h5py" }, - { name = "keras" }, - { name = "libclang" }, - { name = "ml-dtypes" }, - { name = "numpy" }, - { name = "opt-einsum" }, - { name = "packaging" }, - { name = "protobuf" }, - { name = "requests" }, - { name = "setuptools" }, - { name = "six" }, - { name = "tensorboard" }, - { name = "termcolor" }, - { name = "typing-extensions" }, - { name = "wrapt" }, + { name = "absl-py", marker = "python_full_version < '3.13'" }, + { name = "astunparse", marker = "python_full_version < '3.13'" }, + { name = "flatbuffers", marker = "python_full_version < '3.13'" }, + { name = "gast", marker = "python_full_version < '3.13'" }, + { name = "google-pasta", marker = "python_full_version < '3.13'" }, + { name = "grpcio", marker = "python_full_version < '3.13'" }, + { name = "h5py", marker = "python_full_version < '3.13'" }, + { name = "keras", marker = "python_full_version < '3.13'" }, + { name = "libclang", marker = "python_full_version < '3.13'" }, + { name = "ml-dtypes", version = "0.5.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "numpy", marker = "python_full_version < '3.13'" }, + { name = "opt-einsum", marker = "python_full_version < '3.13'" }, + { name = "packaging", marker = "python_full_version < '3.13'" }, + { name = "protobuf", version = "6.33.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "requests", marker = "python_full_version < '3.13'" }, + { name = "setuptools", marker = "python_full_version < '3.13'" }, + { name = "six", marker = "python_full_version < '3.13'" }, + { name = "tensorboard", version = "2.20.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "termcolor", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "wrapt", marker = "python_full_version < '3.13'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/ef/69/de33bd90dbddc8eede8f99ddeccfb374f7e18f84beb404bfe2cbbdf8df90/tensorflow-2.20.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:5f964016c5035d09b85a246a6b739be89282a7839743f3ea63640224f0c63aee", size = 200507363, upload-time = "2025-08-13T16:51:28.27Z" }, @@ -3766,19 +3714,17 @@ wheels = [ [[package]] name = "triqler" -version = "0.9.1" +version = "0.7.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "job-pool" }, { name = "numpy" }, { name = "pandas" }, - { name = "pyarrow" }, { name = "scipy" }, { name = "threadpoolctl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/98/186d0f0380f6a5b9b4b51c4ae75c920d7aaf41d833f416bbf15714fb76c6/triqler-0.9.1.tar.gz", hash = "sha256:0fb6752f84471af275d9c6d24393c1a87e548089e77fc7ae97736e50772fa0e2", size = 58182, upload-time = "2025-10-02T13:30:38.931Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/a4/e427c9e1717c53befb5d44edc906ecec47b93c501d9ac8e5bb5c6899bc1f/triqler-0.9.1-py3-none-any.whl", hash = "sha256:5abebb2899795cd75a2c879d1acf7a9b41e5e039b32ae09f9fd4995d20f78f95", size = 64899, upload-time = "2025-10-02T13:30:38.047Z" }, + { url = "https://files.pythonhosted.org/packages/f3/42/b72411c6e0853f2abd59a63801b492857188dbaa7f6da5fa23ada061e0d2/triqler-0.7.3-py3-none-any.whl", hash = "sha256:d14b3e924ac9065ebca856396db5e908da5302cc332adf53b9f95f67be186ee8", size = 64170, upload-time = "2024-01-04T10:51:19.007Z" }, ] [[package]]