diff --git a/CHANGELOG.md b/CHANGELOG.md index ab9bf52..ee6e03c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- `core.train()` and `core.finetune()`, replacing the `NotImplementedError` stub that + pointed at the separate `im2deeptrainer` package. Both featurise through + `DeepLCDataset`, the same path `predict()` uses, so a trained checkpoint is directly + usable for prediction +- `im2deep train` and `im2deep finetune` CLI commands +- New `_data` module with `CCSDataset` (flattens DeepLC's nested feature tuple for + training), `build_training_dataset` (accepts a PSMList, DataFrame or delimited file, + and reads the target from `CCS` or `ccs`) and `grouped_split` (train/validation split + grouped by stripped sequence, so a peptide cannot appear in both halves) +- `DEFAULT_TRAINING_CONFIG` with the training-loop and featurisation keys, leaving + `DEFAULT_CONFIG` untouched +- `Global_features` configuration key, making the architectures' global branch width + configurable so featurisations other than the default 60 can be trained. Defaults to + 60, so existing checkpoints and configurations are unaffected +- `BackboneFreeze` callback, freezing a transfer model's pretrained feature branches for + a warmup before unfreezing at a reduced learning rate +- Trained checkpoints now record their own configuration, so a model is read back with + the architecture and featurisation it was trained on +- Weights & Biases logging, off by default. Runs are named after `model_name` (or an + explicit `wandb.name`) and carry the full training config, so a set of runs differing + only in training data or featurisation is comparable. `--wandb`, `--wandb-project` and + `--wandb-name` on both CLI commands; `entity` and `tags` are settable from a config + file + +### Changed +- Widened the `deeplc` dependency to `>=4.1.0,<5`. From 4.1.0 the 4.0.1 feature-encoding + change sits behind `legacy_positional_deltas`, which defaults to `True`, so the + encoding matches what the bundled checkpoints were trained with. Verified + bit-identical to 4.0.0 across 4,000 peptidoforms, 3,000 of them modified +- `predict()` now selects the architecture and featurisation from the checkpoint rather + than assuming the package defaults, so fine-tuned models and models trained with + other featurisations load correctly +- `wandb` is now an optional extra (`pip install im2deep[wandb]`) rather than an + undeclared import + +### Fixed +- `IM2DeepTransfer`'s training, validation and test steps did not squeeze the model + output in the `add_X_mol` branch, so the loss broadcast to a `(batch, batch)` matrix + instead of comparing element-wise + ## [2.0.2] - 2026-07-16 ### Added diff --git a/docs/source/index.rst b/docs/source/index.rst index 2bfbf02..915c676 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -11,6 +11,15 @@ Changelog +.. toctree:: + :caption: Guides + :hidden: + :includehidden: + :maxdepth: 2 + + Training + + .. toctree:: :caption: Command line interface :hidden: diff --git a/docs/source/training.rst b/docs/source/training.rst new file mode 100644 index 0000000..c4b9dce --- /dev/null +++ b/docs/source/training.rst @@ -0,0 +1,139 @@ +Training +======== + +IM2Deep can train a new CCS prediction model from scratch, or fine-tune an +existing one on your own runs. Both go through the same DeepLC featuriser that +:func:`im2deep.core.predict` uses, so a model that comes out of training is +directly usable for prediction. + +Training data +------------- + +Training data is a delimited file, a :class:`pandas.DataFrame`, or a +:class:`~psm_utils.psm_list.PSMList`. + +Tabular input needs a target CCS column, named either ``CCS`` or ``ccs``, plus +either a ``peptidoform`` column of ProForma strings or the three columns +``seq``, ``modifications`` and ``charge``: + +.. code-block:: text + + seq,modifications,charge,ccs + AAAAAAAAAAAAAAAAASAGGK,,2,445.04 + AAAAAAAAAAAAAAAAAGATCLER,21|U:4,3,634.92 + +A :class:`~psm_utils.psm_list.PSMList` needs a target on every PSM, as either +``psm.ion_mobility`` or ``psm.metadata["CCS"]``. Ion mobility is converted to +CCS automatically. + +Peptides longer than the 60-residue featurisation window are dropped rather than +silently truncated, as are rows with a missing or non-finite CCS. Both are +reported in the log. + +Training a new model +-------------------- + +From the command line: + +.. code-block:: bash + + im2deep train train.csv -o my_model.ckpt --epochs 100 --num-workers 16 + +Or from Python: + +.. code-block:: python + + from im2deep import train + + model = train("train.csv", "my_model.ckpt", training_kwargs={"epochs": 100}) + +A validation set is split off the training data, grouped by stripped sequence so +that the same peptide cannot appear in both halves at different charge states or +modification states. Pass ``--validation-data`` (or ``validation_psm_list=``) to +supply one explicitly instead. + +``--num-workers`` matters on large datasets: featurisation happens on the fly in +the dataloader and is CPU-bound, so it is usually the bottleneck rather than the +GPU. + +Fine-tuning an existing model +----------------------------- + +Fine-tuning adapts a trained model to new data, which is useful when your runs +sit on a systematically different CCS scale than the training data: + +.. code-block:: bash + + im2deep finetune my_runs.csv -o finetuned.ckpt --freeze-epochs 5 + +The pretrained feature branches are held frozen for the first ``--freeze-epochs`` +epochs while the head adapts, then unfrozen and training continues at a reduced +learning rate. By default the bundled IM2Deep model is the backbone; pass +``--backbone`` to use another checkpoint. + +Configuration +------------- + +Every model and training parameter can be set through a JSON configuration file: + +.. code-block:: bash + + im2deep train train.csv -o my_model.ckpt --config config.json + +.. code-block:: json + + { + "model_params": { + "epochs": 100, + "batch_size": 512, + "learning_rate": 0.001, + "patience": 10, + "wandb": {"enabled": true, "project_name": "IM2Deep"} + } + } + +The ``model_params`` block is optional; a flat object works too. Values are +merged over :data:`im2deep.constants.DEFAULT_TRAINING_CONFIG`, and explicit +command-line flags or ``training_kwargs`` win over the file. + +Weights & Biases logging needs the optional extra: + +.. code-block:: bash + + pip install im2deep[wandb] + +Featurisation variants +---------------------- + +Three configuration keys change how peptides are encoded. They alter the number +of global features, so ``Global_features`` must be set to match; training refuses +to start otherwise rather than failing later with a shape error. + +.. list-table:: + :header-rows: 1 + + * - ``add_ccs_features`` + - ``add_terminal_composition`` + - ``Global_features`` + * - ``false`` + - ``false`` + - 55 + * - ``true`` (default) + - ``false`` (default) + - 60 + * - ``false`` + - ``true`` + - 67 + * - ``true`` + - ``true`` + - 72 + +``legacy_positional_deltas`` controls positional modification indexing. It +defaults to ``true``, reproducing the encoding the bundled models were trained +with. Setting it to ``false`` uses DeepLC's corrected indexing, which changes the +encoding of modified peptides only. + +Each trained checkpoint records the configuration it was trained with, so +:func:`im2deep.core.predict` reads a model back with the right architecture and +featurisation automatically. There is no need to repeat these settings at +prediction time. diff --git a/im2deep/__init__.py b/im2deep/__init__.py index 7b80f43..b841ede 100644 --- a/im2deep/__init__.py +++ b/im2deep/__init__.py @@ -24,7 +24,7 @@ from importlib.metadata import PackageNotFoundError, version -from im2deep.core import predict, predict_and_calibrate +from im2deep.core import finetune, predict, predict_and_calibrate, train from im2deep.utils import ccs2im, im2ccs try: @@ -34,6 +34,8 @@ __all__ = [ "predict", "predict_and_calibrate", + "train", + "finetune", "ccs2im", "im2ccs", ] diff --git a/im2deep/__main__.py b/im2deep/__main__.py index a2e824e..dbf1d23 100644 --- a/im2deep/__main__.py +++ b/im2deep/__main__.py @@ -240,48 +240,203 @@ def _run_predict(*args, **kwargs): LOGGER.info("IM2Deep finished.") -# TODO: implement train command -# @cli.command() -# @click.argument("training_data", type=click.Path(exists=True, dir_okay=False)) -# @click.option( -# "-o", -# "--output-model", -# type=click.Path(dir_okay=False), -# required=True, -# help="Path to save the trained model.", -# ) -# @click.option( -# "--epochs", -# type=int, -# default=100, -# help="Number of training epochs.", -# ) -# @click.option( -# "-l", -# "--logging-level", -# type=click.Choice(["debug", "info", "warning", "error", "critical"], case_sensitive=False), -# default="info", -# help="Set logging verbosity level.", -# ) -# def train(training_data, output_model, epochs, logging_level): -# """Train a new IM2Deep model. - -# Example: im2deep train training_data.csv -o my_model.ckpt -# """ -# setup_logging(logging_level) -# LOGGER.info("Starting IM2Deep training...") - -# # Parse training data -# psm_list_train = _parse_csv_input(training_data, "training") - -# # Call training function -# core.train( -# psm_list=psm_list_train, -# model_save_path=output_model, -# training_kwargs={"epochs": epochs}, -# ) - -# LOGGER.info(f"Training completed. Model saved to {output_model}") +def _training_options(command): + """Apply the options shared by the ``train`` and ``finetune`` commands.""" + options = [ + click.argument("training_data", type=click.Path(exists=True, dir_okay=False)), + click.option( + "-o", + "--output-model", + type=click.Path(dir_okay=False), + required=True, + help="Path to save the trained model checkpoint.", + ), + click.option( + "-c", + "--config", + type=click.Path(exists=True, dir_okay=False), + default=None, + help="JSON configuration file with model and training parameters.", + ), + click.option("--epochs", type=int, default=None, help="Number of training epochs."), + click.option("--batch-size", type=int, default=None, help="Training batch size."), + click.option( + "--validation-data", + type=click.Path(exists=True, dir_okay=False), + default=None, + help="Explicit validation set. If omitted, one is split off the training data.", + ), + click.option( + "--validation-split", + type=float, + default=0.1, + help="Fraction held out for validation, grouped by stripped sequence.", + ), + click.option( + "--num-workers", + type=int, + default=None, + help="Dataloader worker processes used for featurisation.", + ), + click.option( + "--wandb/--no-wandb", + "use_wandb", + default=False, + help="Log the run to Weights & Biases (requires the 'wandb' extra).", + ), + click.option( + "--wandb-project", + default=None, + help="Weights & Biases project to log to. Implies --wandb.", + ), + click.option( + "--wandb-name", + default=None, + help="Weights & Biases run name. Defaults to the config's model_name. " + "Implies --wandb.", + ), + click.option( + "-l", + "--logging-level", + type=click.Choice( + ["debug", "info", "warning", "error", "critical"], case_sensitive=False + ), + default="info", + help="Set logging verbosity level.", + ), + ] + for option in reversed(options): + command = option(command) + return command + + +def _training_kwargs( + epochs, batch_size, num_workers, use_wandb, wandb_project=None, wandb_name=None +) -> dict: + """ + Collect the CLI overrides that were actually given. + + Only keys the user actually set are returned, so unset flags do not + overwrite values from a configuration file. The ``wandb`` block is merged + rather than replaced by ``core._resolve_config``, for the same reason. + """ + kwargs: dict = {} + if epochs is not None: + kwargs["epochs"] = epochs + if batch_size is not None: + kwargs["batch_size"] = batch_size + if num_workers is not None: + kwargs["num_workers"] = num_workers + + # Naming a project or run is meaningless without logging, so either implies it. + wandb_config: dict = {} + if use_wandb or wandb_project or wandb_name: + wandb_config["enabled"] = True + if wandb_project: + wandb_config["project_name"] = wandb_project + if wandb_name: + wandb_config["name"] = wandb_name + if wandb_config: + kwargs["wandb"] = wandb_config + + return kwargs + + +@cli.command() +@_training_options +def train( + training_data, + output_model, + config, + epochs, + batch_size, + validation_data, + validation_split, + num_workers, + use_wandb, + wandb_project, + wandb_name, + logging_level, +): + """Train a new IM2Deep model. + + TRAINING_DATA is a delimited file with a CCS (or ccs) column plus either a + peptidoform column or seq, modifications and charge columns. + + Example: im2deep train training_data.csv -o my_model.ckpt --epochs 100 + """ + setup_logging(logging_level) + LOGGER.info("Starting IM2Deep training...") + + core.train( + psm_list=Path(training_data), + model_save_path=output_model, + training_kwargs=_training_kwargs( + epochs, batch_size, num_workers, use_wandb, wandb_project, wandb_name + ), + validation_psm_list=Path(validation_data) if validation_data else None, + validation_split=validation_split, + config=config, + ) + + LOGGER.info(f"Training completed. Model saved to {output_model}") + + +@cli.command() +@_training_options +@click.option( + "-b", + "--backbone", + type=click.Path(exists=True, dir_okay=False), + default=None, + help="Backbone checkpoint to fine-tune. Defaults to the bundled IM2Deep model.", +) +@click.option( + "--freeze-epochs", + type=int, + default=None, + help="Epochs to keep the pretrained feature branches frozen before unfreezing.", +) +def finetune( + training_data, + output_model, + config, + epochs, + batch_size, + validation_data, + validation_split, + num_workers, + use_wandb, + wandb_project, + wandb_name, + logging_level, + backbone, + freeze_epochs, +): + """Fine-tune an existing IM2Deep model on new data. + + Example: im2deep finetune my_runs.csv -o finetuned.ckpt --freeze-epochs 5 + """ + setup_logging(logging_level) + LOGGER.info("Starting IM2Deep fine-tuning...") + + training_kwargs = _training_kwargs( + epochs, batch_size, num_workers, use_wandb, wandb_project, wandb_name + ) + if freeze_epochs is not None: + training_kwargs["freeze_epochs"] = freeze_epochs + + core.finetune( + psm_list=Path(training_data), + model_save_path=output_model, + model=backbone, + training_kwargs=training_kwargs, + validation_psm_list=Path(validation_data) if validation_data else None, + validation_split=validation_split, + config=config, + ) + + LOGGER.info(f"Fine-tuning completed. Model saved to {output_model}") def _build_credits(): diff --git a/im2deep/_architectures/callbacks.py b/im2deep/_architectures/callbacks.py index 72a325a..1699b3d 100644 --- a/im2deep/_architectures/callbacks.py +++ b/im2deep/_architectures/callbacks.py @@ -1,5 +1,7 @@ """Architecture callbacks.""" +import logging + import lightning as L try: @@ -7,6 +9,13 @@ except ImportError: wandb = None +LOGGER = logging.getLogger(__name__) + +#: Attributes of a transfer model that hold the pretrained feature branches. +#: The concatenation head is deliberately excluded: it is what fine-tuning is +#: meant to adapt. +BACKBONE_BRANCHES = ("ConvAtomComp", "ConvDiatomComp", "ConvGlobal", "OneHot", "MolDesc") + class LogLowestMAE(L.Callback): def __init__(self, config): @@ -24,3 +33,55 @@ def on_validation_end(self, trainer, pl_module): if self.config["wandb"]["enabled"]: if wandb is not None: wandb.log({"Best Val MAE": self.bestMAE}) + + +class BackboneFreeze(L.Callback): + """ + Freeze a transfer model's pretrained feature branches for a warmup. + + Mirrors DeepLC's ``freeze_epochs``/``unfreeze_lr_scale`` fine-tuning + behaviour: the pretrained branches are held fixed while the concatenation + head adapts, then everything is unfrozen and training continues at a + reduced learning rate so the warmed-up head is not immediately undone. + + Parameters + ---------- + freeze_epochs + Number of epochs to keep the feature branches frozen. ``0`` disables + the callback entirely. + unfreeze_lr_scale + Factor applied to every optimizer learning rate on unfreezing. + + """ + + def __init__(self, freeze_epochs: int = 0, unfreeze_lr_scale: float = 0.1): + super().__init__() + self.freeze_epochs = freeze_epochs + self.unfreeze_lr_scale = unfreeze_lr_scale + + def _set_requires_grad(self, pl_module: L.LightningModule, requires_grad: bool) -> None: + for name in BACKBONE_BRANCHES: + branch = getattr(pl_module, name, None) + if branch is None: + continue + for parameter in branch.parameters(): + parameter.requires_grad = requires_grad + + def on_train_start(self, trainer, pl_module) -> None: + if self.freeze_epochs > 0: + self._set_requires_grad(pl_module, False) + LOGGER.info( + f"Froze pretrained feature branches for the first {self.freeze_epochs} epoch(s)." + ) + + def on_train_epoch_start(self, trainer, pl_module) -> None: + if self.freeze_epochs <= 0 or trainer.current_epoch != self.freeze_epochs: + return + self._set_requires_grad(pl_module, True) + for optimizer in trainer.optimizers: + for group in optimizer.param_groups: + group["lr"] *= self.unfreeze_lr_scale + LOGGER.info( + "Unfroze pretrained feature branches and scaled the learning rate by " + f"{self.unfreeze_lr_scale}." + ) diff --git a/im2deep/_architectures/im2deep_multi.py b/im2deep/_architectures/im2deep_multi.py index 4d82eda..2f4079f 100644 --- a/im2deep/_architectures/im2deep_multi.py +++ b/im2deep/_architectures/im2deep_multi.py @@ -13,7 +13,7 @@ from im2deep._architectures.helpers import calculate_concat_shape from im2deep._architectures.im2deep_single import IM2Deep from im2deep._architectures.losses import LowestMAESorted, MeanMAESorted -from im2deep.constants import BASEMODELCONFIG +from im2deep.constants import BASEMODELCONFIG, DEFAULT_GLOBAL_FEATURES LOGGER = logging.getLogger(__name__) PACKAGE_DATA_PATH = Path(__file__).parent / "package_data" @@ -164,7 +164,10 @@ def __init__(self, config, criterion): self.ConvGlobal = nn.ModuleList() self.ConvGlobal.append( DenseActivation( - 60, + # See the note in im2deep_single.IM2Deep: number of DeepLC + # global features, defaulting to what the bundled checkpoints + # were trained with. + self.config.get("Global_features", DEFAULT_GLOBAL_FEATURES), self.config["Global_units"], initializer=initi, negative_slope=self.config["LRelu_negative_slope"], diff --git a/im2deep/_architectures/im2deep_single.py b/im2deep/_architectures/im2deep_single.py index 50ae565..2ab88ab 100644 --- a/im2deep/_architectures/im2deep_single.py +++ b/im2deep/_architectures/im2deep_single.py @@ -9,6 +9,7 @@ from im2deep._architectures.blocks import Conv1dActivation, DenseActivation from im2deep._architectures.helpers import calculate_concat_shape +from im2deep.constants import DEFAULT_GLOBAL_FEATURES LOGGER = logging.getLogger(__name__) @@ -19,6 +20,11 @@ def __init__(self, config, criterion): self.config = config self.criterion = criterion self.mae = nn.L1Loss() + # Record the config in the checkpoint so a model can be read back with + # the architecture and featurisation it was trained on. The bundled + # checkpoints predate this and carry no hyperparameters, which is why + # _model_ops still falls back to DEFAULT_CONFIG. + self.save_hyperparameters("config") initi = self.configure_init() @@ -159,7 +165,11 @@ def __init__(self, config, criterion): self.ConvGlobal = nn.ModuleList() self.ConvGlobal.append( DenseActivation( - 60, + # Number of DeepLC global features, which depends on the + # featurisation flags. The default matches + # `add_ccs_features=True, add_terminal_composition=False`, + # i.e. what every bundled checkpoint was trained with. + self.config.get("Global_features", DEFAULT_GLOBAL_FEATURES), self.config["Global_units"], initializer=initi, negative_slope=self.config["LRelu_negative_slope"], @@ -485,6 +495,7 @@ def __init__(self, config, criterion): self.criterion = criterion self.l1_alpha = config["L1_alpha"] self.mae = nn.L1Loss() + self.save_hyperparameters("config") # Load the IM2Deep model LOGGER.debug("Loading backbone IM2Deep model") @@ -550,7 +561,7 @@ def forward( def training_step(self, batch, batch_idx): if self.config["add_X_mol"]: atom_comp, diatom_comp, global_feats, one_hot, y, mol_desc = batch - y_hat = self(atom_comp, diatom_comp, global_feats, one_hot, mol_desc) + y_hat = self(atom_comp, diatom_comp, global_feats, one_hot, mol_desc).squeeze(1) else: atom_comp, diatom_comp, global_feats, one_hot, y = batch y_hat = self(atom_comp, diatom_comp, global_feats, one_hot).squeeze(1) @@ -576,7 +587,7 @@ def training_step(self, batch, batch_idx): def validation_step(self, batch, batch_idx): if self.config["add_X_mol"]: atom_comp, diatom_comp, global_feats, one_hot, y, mol_desc = batch - y_hat = self(atom_comp, diatom_comp, global_feats, one_hot, mol_desc) + y_hat = self(atom_comp, diatom_comp, global_feats, one_hot, mol_desc).squeeze(1) else: atom_comp, diatom_comp, global_feats, one_hot, y = batch y_hat = self(atom_comp, diatom_comp, global_feats, one_hot).squeeze(1) @@ -597,7 +608,7 @@ def validation_step(self, batch, batch_idx): def test_step(self, batch, batch_idx): if self.config["add_X_mol"]: atom_comp, diatom_comp, global_feats, one_hot, y, mol_desc = batch - y_hat = self(atom_comp, diatom_comp, global_feats, one_hot, mol_desc) + y_hat = self(atom_comp, diatom_comp, global_feats, one_hot, mol_desc).squeeze(1) else: atom_comp, diatom_comp, global_feats, one_hot, y = batch y_hat = self(atom_comp, diatom_comp, global_feats, one_hot).squeeze(1) diff --git a/im2deep/_data.py b/im2deep/_data.py new file mode 100644 index 0000000..825ebb4 --- /dev/null +++ b/im2deep/_data.py @@ -0,0 +1,388 @@ +""" +Dataset construction for IM2Deep training. + +Inference builds its dataset with :meth:`deeplc.data.DeepLCDataset.from_psm_list` +(see :func:`im2deep.core.predict`), but training cannot reuse that path for two +reasons: + +* ``DeepLCDataset.__getitem__`` returns a nested ``((atom, diatom, global, + one_hot), target)``, while the architectures' ``training_step`` unpacks a flat + ``atom_comp, diatom_comp, global_feats, one_hot, y``. Inference gets away with + the nested form because :func:`im2deep._model_ops._predict_loop` unpacks it + itself. +* ``from_psm_list`` reads its target from ``psm_list["retention_time"]`` and + discards all targets if a single PSM lacks one, whereas IM2Deep carries the + target CCS in ``psm.metadata["CCS"]``. + +:class:`CCSDataset` fixes the first and :func:`build_training_dataset` the +second. Both keep the featurisation itself in DeepLC's hands, so a model trained +here sees exactly what ``predict()`` will feed it later. +""" + +from __future__ import annotations + +import logging +from collections.abc import Sequence +from os import PathLike +from pathlib import Path + +import numpy as np +import pandas as pd +import psm_utils.io +from deeplc.data import DeepLCDataset +from psm_utils.psm_list import PSMList +from torch.utils.data import Subset + +from im2deep._io_helpers import validate_psm_list +from im2deep.constants import GLOBAL_FEATURE_COUNTS +from im2deep.exceptions import IM2DeepError + +LOGGER = logging.getLogger(__name__) + +#: Column names accepted as the target CCS, in order of preference. The +#: collated training sets produced by the dataset pipeline use lowercase +#: ``ccs``; ``parse_input`` only recognises the uppercase form. +CCS_COLUMNS = ("CCS", "ccs") + + +class CCSDataset(DeepLCDataset): + """ + A :class:`deeplc.data.DeepLCDataset` that yields a flat feature tuple. + + ``DeepLCDataset`` returns ``((atom, diatom, global, one_hot), target)``, + which cannot be unpacked by the architectures' ``training_step``. This + subclass flattens it to ``(atom, diatom, global, one_hot, target)``. + + Subclassing rather than passing a ``collate_fn`` keeps + :class:`torch.utils.data.Subset` and PyTorch's default collation working + unchanged, and leaves the nested inference path untouched. + """ + + def __getitem__(self, idx: int) -> tuple: # type: ignore[override] + """Return ``(atom_comp, diatom_comp, global_feats, one_hot, target)``.""" + features, target = super().__getitem__(idx) + return (*features, target) + + +def expected_global_features( + add_ccs_features: bool = True, add_terminal_composition: bool = False +) -> int: + """ + Number of global features DeepLC yields for a featurisation flag pair. + + Parameters + ---------- + add_ccs_features + Whether the five CCS-specific global features are included. + add_terminal_composition + Whether the twelve terminal-composition values are included. + + Returns + ------- + int + Length of DeepLC's ``matrix_global`` for those flags. + + """ + return GLOBAL_FEATURE_COUNTS[(bool(add_ccs_features), bool(add_terminal_composition))] + + +def check_global_features(config: dict) -> None: + """ + Check that ``Global_features`` matches the configured featurisation. + + The architectures' global branch input width is fixed at construction, so a + mismatch with what :class:`CCSDataset` actually yields would otherwise + surface as an opaque shape error inside the first dense layer. + + Parameters + ---------- + config + Training configuration. + + Raises + ------ + IM2DeepError + If ``config["Global_features"]`` does not match the featurisation flags. + + """ + expected = expected_global_features( + config.get("add_ccs_features", True), + config.get("add_terminal_composition", False), + ) + configured = config.get("Global_features", expected) + if configured != expected: + raise IM2DeepError( + f"Global_features is {configured}, but add_ccs_features=" + f"{config.get('add_ccs_features', True)} and add_terminal_composition=" + f"{config.get('add_terminal_composition', False)} make DeepLC yield " + f"{expected} global features. Set Global_features to {expected}, or " + "change the featurisation flags to match." + ) + + +def _featurisation_kwargs(config: dict) -> dict: + """Extract the DeepLC featurisation keyword arguments from a config.""" + return { + "add_ccs_features": config.get("add_ccs_features", True), + "add_terminal_composition": config.get("add_terminal_composition", False), + "padding_length": config.get("padding_length", 60), + "legacy_positional_deltas": config.get("legacy_positional_deltas", True), + } + + +def _from_psm_list(psm_list: PSMList) -> tuple[list[str], np.ndarray]: + """ProForma strings and target CCS values from a PSMList.""" + # validate_psm_list drops charge None/>6 and normalises ion_mobility into + # metadata["CCS"], so afterwards metadata["CCS"] is the single target source. + psm_list = validate_psm_list(psm_list, needs_target=True) + peptidoforms = [str(psm.peptidoform) for psm in psm_list] + targets = np.array([float((psm.metadata or {})["CCS"]) for psm in psm_list], dtype=np.float32) + return peptidoforms, targets + + +def _from_dataframe(df: pd.DataFrame) -> tuple[list[str], np.ndarray]: + """ + ProForma strings and target CCS values from a DataFrame. + + Deliberately does not go through :func:`im2deep._io_helpers.parse_input`: + that builds a :class:`~psm_utils.psm.PSM` per row, which costs roughly 36x + the memory of the equivalent ProForma strings and is the difference between + a multi-million-row training set fitting in memory comfortably or not. + """ + ccs_column = next((c for c in CCS_COLUMNS if c in df.columns), None) + if ccs_column is None: + raise IM2DeepError( + f"No target CCS column found; expected one of {list(CCS_COLUMNS)}, " + f"got {list(df.columns)}." + ) + + if "peptidoform" in df.columns: + peptidoform_strings = df["peptidoform"].astype(str).tolist() + elif all(col in df.columns for col in ("seq", "modifications", "charge")): + modifications = df["modifications"].fillna("").astype(str) + peptidoform_strings = [ + psm_utils.io.peptide_record.peprec_to_proforma( + peptide=seq, modifications=mods, charge=int(charge) + ).proforma + for seq, mods, charge in zip(df["seq"], modifications, df["charge"], strict=True) + ] + else: + raise IM2DeepError( + "Expected either a 'peptidoform' column or 'seq', 'modifications' " + f"and 'charge' columns, got {list(df.columns)}." + ) + + targets = df[ccs_column].to_numpy(dtype=np.float32) + return peptidoform_strings, targets + + +def _drop_long_and_missing( + peptidoforms: list[str], targets: np.ndarray, padding_length: int +) -> tuple[list[str], np.ndarray]: + """ + Drop peptides longer than ``padding_length`` and rows with no target. + + DeepLC silently truncates over-length peptides to the first + ``padding_length`` residues and then emits one warning per token beyond it, + so on a large training set these rows cost both correctness and a very large + warning stream. Dropping them is both cheaper and more honest. + """ + lengths = np.array([_stripped_length(pf) for pf in peptidoforms]) + keep = (lengths <= padding_length) & np.isfinite(targets) + + n_long = int((lengths > padding_length).sum()) + if n_long: + LOGGER.warning( + f"Dropped {n_long} peptide(s) longer than the {padding_length}-residue " + "featurisation window; DeepLC would otherwise truncate them silently." + ) + n_missing = int((~np.isfinite(targets)).sum()) + if n_missing: + LOGGER.warning(f"Dropped {n_missing} row(s) with a missing or non-finite CCS.") + + if not keep.any(): + raise IM2DeepError("No usable training rows remain after filtering.") + + if keep.all(): + return peptidoforms, targets + return [pf for pf, k in zip(peptidoforms, keep, strict=True) if k], targets[keep] + + +def _stripped_length(peptidoform: str) -> int: + """ + Residue count of a ProForma string, ignoring modifications and charge. + + Counting upper-case residue letters outside bracketed modification tags is + enough here and avoids parsing several million peptidoforms just to measure + them. + """ + length = 0 + depth = 0 + for char in peptidoform: + if char in "[(": + depth += 1 + elif char in "])": + depth = max(0, depth - 1) + elif char == "/" and depth == 0: + break + elif depth == 0 and char.isalpha() and char.isupper(): + length += 1 + return length + + +def stripped_sequence(peptidoform: str) -> str: + """ + The bare amino-acid sequence of a ProForma string. + + Strips modification tags, the charge suffix and any terminal-modification + prefix, so that ``PEPT[UNIMOD:21]IDEK/2`` and ``PEPTIDEK/3`` map to the same + key. Used to keep a peptide out of both halves of a train/validation split. + """ + residues = [] + depth = 0 + for char in peptidoform: + if char in "[(": + depth += 1 + elif char in "])": + depth = max(0, depth - 1) + elif char == "/" and depth == 0: + break + elif depth == 0 and char.isalpha() and char.isupper(): + residues.append(char) + return "".join(residues) + + +def build_training_dataset( + source: PSMList | pd.DataFrame | PathLike | str, + config: dict, +) -> CCSDataset: + """ + Build a :class:`CCSDataset` from a PSMList, DataFrame or delimited file. + + Parameters + ---------- + source + A :class:`~psm_utils.psm_list.PSMList` with a target CCS on every PSM + (either ``psm.ion_mobility`` or ``psm.metadata["CCS"]``), a + :class:`pandas.DataFrame`, or a path to a delimited file. Tabular input + needs a target column (``CCS`` or ``ccs``) plus either a ``peptidoform`` + column or ``seq``/``modifications``/``charge`` columns. + config + Training configuration; the featurisation keys (``add_ccs_features``, + ``add_terminal_composition``, ``padding_length``, + ``legacy_positional_deltas``) and ``Global_features`` are read from it. + + Returns + ------- + CCSDataset + Dataset yielding ``(atom, diatom, global, one_hot, target)`` per item. + + """ + check_global_features(config) + + if isinstance(source, PSMList): + peptidoforms, targets = _from_psm_list(source) + else: + if isinstance(source, pd.DataFrame): + df = source + elif isinstance(source, (str, Path, PathLike)): + LOGGER.info(f"Reading training data from {source}") + df = pd.read_csv(source, sep=None, engine="python") + else: + raise TypeError( + f"source must be a PSMList, DataFrame or path, got {type(source).__name__}." + ) + peptidoforms, targets = _from_dataframe(df) + + featurisation = _featurisation_kwargs(config) + peptidoforms, targets = _drop_long_and_missing( + peptidoforms, targets, featurisation["padding_length"] + ) + LOGGER.info(f"Built training dataset with {len(peptidoforms):,} precursors.") + + # Peptidoforms are passed as ProForma strings rather than Peptidoform + # objects: DeepLC re-parses them per item, which costs throughput but keeps + # resident memory roughly 36x lower and avoids each DataLoader worker + # forking a large object graph. + return CCSDataset( + peptidoforms=peptidoforms, # type: ignore[arg-type] + target_retention_times=targets, + **featurisation, + ) + + +def grouped_split( + dataset: CCSDataset, + validation_split: float, + seed: int = 0, +) -> tuple[Subset, Subset]: + """ + Split a dataset into train and validation, grouped by stripped sequence. + + A peptide observed at two charge states or in two modification states must + not land in both halves, or the validation loss reports memorisation as + generalisation. :func:`deeplc.data.split_datasets` uses a plain + :func:`~torch.utils.data.random_split` and carries a TODO for exactly this; + this is that grouped version. + + Parameters + ---------- + dataset + Dataset to split. + validation_split + Approximate fraction of precursors to hold out. The realised fraction + differs slightly because whole sequence groups move together. + seed + Seed for the group shuffle. + + Returns + ------- + tuple of Subset + ``(train_subset, validation_subset)``. + + """ + if not 0 < validation_split < 1: + raise ValueError(f"validation_split must be in (0, 1), got {validation_split}.") + + peptidoforms = _peptidoform_strings(dataset) + groups: dict[str, list[int]] = {} + for idx, peptidoform in enumerate(peptidoforms): + groups.setdefault(stripped_sequence(peptidoform), []).append(idx) + + group_keys = sorted(groups) + rng = np.random.default_rng(seed) + rng.shuffle(group_keys) # type: ignore[arg-type] + + target_validation = validation_split * len(peptidoforms) + validation_indices: list[int] = [] + for key in group_keys: + if len(validation_indices) >= target_validation: + break + validation_indices.extend(groups[key]) + + validation_set = set(validation_indices) + train_indices = [idx for idx in range(len(peptidoforms)) if idx not in validation_set] + + if not train_indices or not validation_indices: + raise IM2DeepError( + f"validation_split={validation_split} left {len(train_indices)} training " + f"and {len(validation_indices)} validation precursors; at least one of " + "each is required. The dataset may be too small or dominated by a " + "single sequence." + ) + + LOGGER.info( + f"Split {len(peptidoforms):,} precursors into {len(train_indices):,} train " + f"and {len(validation_indices):,} validation " + f"({len(validation_indices) / len(peptidoforms):.1%}), grouped over " + f"{len(group_keys):,} stripped sequences." + ) + return Subset(dataset, train_indices), Subset(dataset, validation_indices) + + +def _peptidoform_strings(dataset: CCSDataset) -> Sequence[str]: + """Peptidoform strings of a dataset, as strings whatever was passed in.""" + return [ + peptidoform if isinstance(peptidoform, str) else str(peptidoform) + for peptidoform in dataset.peptidoforms + ] diff --git a/im2deep/_model_ops.py b/im2deep/_model_ops.py index c33c9bc..534beaf 100644 --- a/im2deep/_model_ops.py +++ b/im2deep/_model_ops.py @@ -8,12 +8,20 @@ from os import PathLike from pathlib import Path +import lightning as L import torch +from lightning.pytorch.callbacks import ( + EarlyStopping, + ModelCheckpoint, + ModelSummary, + RichProgressBar, +) from rich.progress import track from torch.utils.data import DataLoader, Dataset +from im2deep._architectures.callbacks import BackboneFreeze, LogLowestMAE from im2deep._architectures.im2deep_multi import IM2DeepMultiTransfer -from im2deep._architectures.im2deep_single import IM2Deep +from im2deep._architectures.im2deep_single import IM2Deep, IM2DeepTransfer from im2deep._architectures.losses import FlexibleLossSorted # Suppress PyTorch padding warning for conv1d with even kernels and odd dilation @@ -128,11 +136,9 @@ def predict( LOGGER.debug("Loading model for prediction.") # TODO: Implement load_model function here (also config) and path to default model? - model = _get_architecture( - multi=multi, - ).load_from_checkpoint( + model = read_checkpoint_architecture(model, multi=multi).load_from_checkpoint( checkpoint_path=model, # type: ignore # TODO: Match with function signature - config=_get_model_config(multi=multi), + config=read_checkpoint_config(model, multi=multi), criterion=_get_loss_function(multi=multi), ) model.to(device) @@ -173,6 +179,181 @@ def _predict_loop( return torch.cat(all_predictions, dim=0).squeeze() +def train( + train_dataset: Dataset, + validation_dataset: Dataset, + config: dict, + output_dir: PathLike | str, + model: torch.nn.Module | None = None, +) -> tuple[L.Trainer, torch.nn.Module]: + """ + Train an IM2Deep model on already-featurised datasets. + + Parameters + ---------- + train_dataset + Training dataset, yielding the flat + ``(atom, diatom, global, one_hot, target)`` tuples the architectures' + ``training_step`` expects. See :class:`im2deep._data.CCSDataset`. + validation_dataset + Validation dataset, same layout. + config + Training configuration. See + :data:`im2deep.constants.DEFAULT_TRAINING_CONFIG` for the keys read. + output_dir + Directory for Lightning logs and intermediate checkpoints. + model + Model to continue training. If None, a fresh model is built from + ``config``: :class:`~im2deep._architectures.im2deep_single.IM2DeepTransfer` + when ``config["backbone_SD_path"]`` is set, otherwise + :class:`~im2deep._architectures.im2deep_single.IM2Deep`. + + Returns + ------- + tuple + ``(trainer, model, best_checkpoint_path)``. The model is the best + checkpoint reloaded when ``config["use_best_model"]`` is set, and + ``best_checkpoint_path`` is None when no checkpoint was written. + + """ + torch.set_float32_matmul_precision("high") + output_dir = Path(output_dir) + + criterion = _get_loss_function(multi=False) + if model is None: + model = _build_model(config, criterion) + LOGGER.debug(f"Training {type(model).__name__}") + + train_loader = DataLoader( + train_dataset, + batch_size=config["batch_size"], + shuffle=True, + num_workers=config.get("num_workers", 0), + persistent_workers=bool(config.get("num_workers", 0)), + ) + validation_loader = DataLoader( + validation_dataset, + batch_size=config["batch_size"], + shuffle=False, + num_workers=config.get("num_workers", 0), + persistent_workers=bool(config.get("num_workers", 0)), + ) + + callbacks, checkpoint_callback = _setup_callbacks(config, output_dir) + trainer = L.Trainer( + accelerator=config.get("accelerator", "auto"), + devices=config.get("devices", "auto"), + max_epochs=config["epochs"], + callbacks=callbacks, + logger=_setup_logger(config, model, output_dir), + default_root_dir=str(output_dir), + enable_progress_bar=True, + ) + trainer.fit(model, train_loader, validation_loader) + + best_checkpoint_path = None + if checkpoint_callback is not None and checkpoint_callback.best_model_path: + best_checkpoint_path = checkpoint_callback.best_model_path + LOGGER.info(f"Reloading best checkpoint: {best_checkpoint_path}") + model = type(model).load_from_checkpoint( + best_checkpoint_path, config=config, criterion=criterion + ) + + return trainer, model, best_checkpoint_path + + +def _build_model(config: dict, criterion: torch.nn.Module) -> torch.nn.Module: + """Build a fresh single-conformer model from a training configuration.""" + if config.get("backbone_SD_path"): + return IM2DeepTransfer(config, criterion=criterion) + return IM2Deep(config, criterion=criterion) + + +def _setup_callbacks( + config: dict, output_dir: Path +) -> tuple[list[L.Callback], ModelCheckpoint | None]: + """Build the Lightning callbacks for a training run.""" + callbacks: list[L.Callback] = [ + ModelSummary(), + RichProgressBar(), + LogLowestMAE(config), + ] + + checkpoint_callback = None + if config.get("use_best_model", True): + checkpoint_callback = ModelCheckpoint( + dirpath=str(output_dir / "checkpoint"), + filename=config["model_name"], + monitor=config["monitor"], + mode=config["mode"], + save_last=False, + ) + callbacks.append(checkpoint_callback) + + if config.get("patience"): + callbacks.append( + EarlyStopping( + monitor=config["monitor"], + mode=config["mode"], + patience=config["patience"], + ) + ) + + if config.get("freeze_epochs"): + callbacks.append( + BackboneFreeze( + freeze_epochs=config["freeze_epochs"], + unfreeze_lr_scale=config.get("unfreeze_lr_scale", 0.1), + ) + ) + + return callbacks, checkpoint_callback + + +def _setup_logger(config: dict, model: torch.nn.Module, output_dir: Path | None = None): + """ + Build a Weights & Biases logger, or None when wandb is not enabled. + + The run is named after ``model_name`` unless the wandb block overrides it, + so a set of runs differing only in training data or featurisation is + distinguishable in the dashboard rather than carrying wandb's random names. + The full training config is logged with the run, which is what makes those + runs comparable after the fact. + """ + wandb_config = config.get("wandb") or {} + if not wandb_config.get("enabled"): + return None + + try: + import wandb # noqa: F401 + from lightning.pytorch.loggers import WandbLogger + except ImportError as exc: + raise ImportError( + "wandb logging was enabled but wandb is not installed. " + "Install it with `pip install im2deep[wandb]`." + ) from exc + + logger = WandbLogger( + project=wandb_config.get("project_name", "IM2Deep"), + name=wandb_config.get("name") or config.get("model_name"), + entity=wandb_config.get("entity"), + tags=wandb_config.get("tags"), + save_dir=str(output_dir) if output_dir is not None else None, + ) + # Log the training config so runs can be told apart and compared later. + logger.experiment.config.update( + {key: value for key, value in config.items() if key != "wandb"}, + allow_val_change=True, + ) + logger.watch(model) + LOGGER.info( + f"Logging to Weights & Biases project " + f"'{wandb_config.get('project_name', 'IM2Deep')}' as run " + f"'{wandb_config.get('name') or config.get('model_name')}'." + ) + return logger + + def _get_architecture(multi: bool) -> type[IM2DeepMultiTransfer] | type[IM2Deep]: """Get the model architecture based on whether multi-output is needed.""" if multi: @@ -181,6 +362,99 @@ def _get_architecture(multi: bool) -> type[IM2DeepMultiTransfer] | type[IM2Deep] return IM2Deep +def _read_checkpoint(model: torch.nn.Module | PathLike | str | None) -> dict | None: + """Load a checkpoint file as a dict, or None if it is not one.""" + if not isinstance(model, (str, Path)): + return None + + try: + checkpoint = torch.load(model, weights_only=False, map_location="cpu") + except Exception as exc: # pragma: no cover - re-raised by load_from_checkpoint + LOGGER.debug(f"Could not pre-read checkpoint: {exc}") + return None + + if not isinstance(checkpoint, dict): + LOGGER.debug("Checkpoint is not a Lightning checkpoint.") + return None + return checkpoint + + +def read_checkpoint_config( + model: torch.nn.Module | PathLike | str | None, multi: bool = False +) -> dict: + """ + The configuration a checkpoint was trained with, or the package default. + + Checkpoints written by :func:`im2deep.core.train` record their own config, + which is what makes a model trained with a non-default architecture width + or featurisation readable back. The bundled checkpoints predate that and + carry no hyperparameters, so they fall back to the package defaults, which + is exactly what they were trained with. + + Parameters + ---------- + model + Path to a checkpoint. Anything else returns the package default. + multi + Whether the multi-conformer default applies. + + Returns + ------- + dict + Model configuration. + + """ + default = _get_model_config(multi=multi) + checkpoint = _read_checkpoint(model) + if checkpoint is None: + return default + + config = (checkpoint.get("hyper_parameters") or {}).get("config") + if not config: + LOGGER.debug("Checkpoint carries no config; using the package default.") + return default + + LOGGER.debug("Using the configuration recorded in the checkpoint.") + return config + + +def read_checkpoint_architecture( + model: torch.nn.Module | PathLike | str | None, multi: bool = False +): + """ + The architecture class a checkpoint was written by. + + A fine-tuned model is an + :class:`~im2deep._architectures.im2deep_single.IM2DeepTransfer`, whose + state_dict nests the pretrained weights under ``backbone.``, so it cannot be + loaded as a plain :class:`~im2deep._architectures.im2deep_single.IM2Deep`. + The distinction is read off the state_dict rather than the config, so it + also holds for checkpoints that record no configuration. + + Parameters + ---------- + model + Path to a checkpoint. + multi + Whether this is a multi-conformer model. + + Returns + ------- + type + The architecture class to load the checkpoint with. + + """ + if multi: + return _get_architecture(multi=True) + + checkpoint = _read_checkpoint(model) + state_dict = (checkpoint or {}).get("state_dict") or {} + if any(key.startswith("backbone.") for key in state_dict): + LOGGER.debug("Checkpoint is a transfer model.") + return IM2DeepTransfer + return IM2Deep + + def _get_model_config(multi: bool) -> dict: """Get the model configuration based on whether multi-output is needed.""" if multi: diff --git a/im2deep/constants.py b/im2deep/constants.py index 36299ca..3758d1c 100644 --- a/im2deep/constants.py +++ b/im2deep/constants.py @@ -71,6 +71,56 @@ "init": "normal", } +# Number of global features DeepLC yields per featurisation flag combination. +# `matrix_global` is 6 summed atom counts + sequence length + 48 flattened +# positional-composition values = 55, plus 5 CCS features +# (`add_ccs_features`) and/or 12 terminal-composition values +# (`add_terminal_composition`). The architectures' global branch input width +# must match, which is what `Global_features` in the config carries. +GLOBAL_FEATURE_COUNTS = { + # (add_ccs_features, add_terminal_composition): n_global_features + (False, False): 55, + (True, False): 60, + (False, True): 67, + (True, True): 72, +} + +# Default global-feature width, matching `add_ccs_features=True` and +# `add_terminal_composition=False`, which is what every bundled checkpoint was +# trained with. +DEFAULT_GLOBAL_FEATURES = GLOBAL_FEATURE_COUNTS[(True, False)] + +# Default configuration for training. `DEFAULT_CONFIG` above describes the +# bundled checkpoints' architecture and is deliberately left alone so they keep +# loading unchanged; this adds the keys the training loop needs on top. +DEFAULT_TRAINING_CONFIG = { + **DEFAULT_CONFIG, + "model_name": "IM2Deep", + # Training loop + "epochs": 100, + "patience": 10, + "use_best_model": True, + # "Validation MAE" is the metric name `LogLowestMAE` reads and the one the + # bundled IM2DeepUni.ckpt's own ModelCheckpoint monitored; changing it + # means changing both. + "monitor": "Validation MAE", + "mode": "min", + "num_workers": 0, + "accelerator": "auto", + "devices": "auto", + "wandb": {"enabled": False, "project_name": "IM2Deep"}, + # Featurisation. These are recorded in the checkpoint so `predict()` reads + # a model back with the encoding it was trained on. + "add_ccs_features": True, + "add_terminal_composition": False, + # DeepLC 4.1.0 defaults this to True, reproducing the pre-4.0.1 positional + # modification indexing the bundled checkpoints were trained with. Set to + # False to train against the 4.0.1 fix instead. + "legacy_positional_deltas": True, + "padding_length": 60, + "Global_features": DEFAULT_GLOBAL_FEATURES, +} + BASEMODELCONFIG = { "AtomComp_kernel_size": 4, "DiatomComp_kernel_size": 4, diff --git a/im2deep/core.py b/im2deep/core.py index ca0682e..63ee1c9 100644 --- a/im2deep/core.py +++ b/im2deep/core.py @@ -2,18 +2,27 @@ from __future__ import annotations +import json import logging +import shutil from os import PathLike +from pathlib import Path import numpy as np +import pandas as pd import torch from deeplc.data import DeepLCDataset from psm_utils.psm_list import PSMList -from im2deep import _model_ops +from im2deep import _data, _model_ops +from im2deep._data import _featurisation_kwargs from im2deep._io_helpers import validate_psm_list from im2deep.calibration import Calibration, LinearCCSCalibration -from im2deep.constants import DEFAULT_MODEL, DEFAULT_MULTI_MODEL +from im2deep.constants import ( + DEFAULT_MODEL, + DEFAULT_MULTI_MODEL, + DEFAULT_TRAINING_CONFIG, +) LOGGER = logging.getLogger(__name__) @@ -44,9 +53,17 @@ def predict( """ LOGGER.info("Predicting CCS values using IM2Deep.") psm_list = validate_psm_list(psm_list) + model = model or (DEFAULT_MODEL if not multi else DEFAULT_MULTI_MODEL) + + # Featurise the way this particular checkpoint was trained. Checkpoints + # written by train() record their flags; the bundled ones predate that and + # fall back to the package defaults, which is what they were trained with. + model_config = _model_ops.read_checkpoint_config(model, multi=multi) + featurisation = _featurisation_kwargs(model_config) + return _model_ops.predict( - model=model or (DEFAULT_MODEL if not multi else DEFAULT_MULTI_MODEL), - data=DeepLCDataset.from_psm_list(psm_list, add_ccs_features=True), + model=model, + data=DeepLCDataset.from_psm_list(psm_list, **featurisation), multi=multi, **(predict_kwargs or {}), # TODO: check if "backbone" argument is needed for multi @@ -149,31 +166,236 @@ def predict_and_calibrate( return predicted_ccs_calibrated +def _resolve_config( + config: dict | PathLike | str | None, + training_kwargs: dict | None, + base_overrides: dict | None = None, +) -> dict: + """ + Merge a user configuration over the training defaults. + + Accepts a dict or a path to a JSON file. A ``model_params`` block is + unwrapped and merged, so configuration files written for the standalone + ``im2deeptrainer`` package remain usable. + + Precedence, weakest first: + :data:`~im2deep.constants.DEFAULT_TRAINING_CONFIG`, ``base_overrides``, + ``config``, ``training_kwargs``. So an explicit argument always wins over a + configuration file, and a configuration file always wins over a caller's + task-specific defaults. + + Parameters + ---------- + config + Configuration dict, or path to a JSON configuration file. + training_kwargs + Keyword overrides applied on top. + base_overrides + Task-specific defaults applied under the user's own settings, used by + :func:`finetune` to carry the backbone's architecture across. + + Returns + ------- + dict + Merged configuration. + + """ + resolved = dict(DEFAULT_TRAINING_CONFIG) + + if base_overrides: + resolved.update(base_overrides) + + if config is not None: + if isinstance(config, (str, Path, PathLike)): + LOGGER.info(f"Reading training configuration from {config}") + with open(config) as config_file: + config = json.load(config_file) + if not isinstance(config, dict): + raise TypeError(f"config must be a dict or a path, got {type(config).__name__}.") + # im2deeptrainer nested the model hyperparameters under "model_params"; + # accept both that shape and a flat dict. + merged = {**config, **config.get("model_params", {})} + merged.pop("model_params", None) + _update_config(resolved, merged) + + if training_kwargs: + _update_config(resolved, training_kwargs) + + return resolved + + +def _update_config(resolved: dict, overrides: dict) -> None: + """ + Apply overrides in place, merging the nested ``wandb`` block rather than + replacing it. + + ``wandb`` is the only nested key in the training config. A plain + ``dict.update`` would mean that passing ``--wandb`` on the command line + silently discarded a project name set in a configuration file. + """ + for key, value in overrides.items(): + if key == "wandb" and isinstance(value, dict) and isinstance(resolved.get(key), dict): + resolved[key] = {**resolved[key], **value} + else: + resolved[key] = value + + def train( - psm_list, - model_save_path, - training_kwargs=None, -): + psm_list: PSMList | pd.DataFrame | PathLike | str, + model_save_path: PathLike | str, + training_kwargs: dict | None = None, + validation_psm_list: PSMList | pd.DataFrame | PathLike | str | None = None, + validation_split: float = 0.1, + config: dict | PathLike | str | None = None, + output_dir: PathLike | str | None = None, +) -> torch.nn.Module: """ - Train a new IM2Deep model using the provided PSM list. + Train a new IM2Deep model. + + Features are built with the same DeepLC featuriser :func:`predict` uses, so + a model trained here can be used for prediction directly. The resulting + checkpoint records its own configuration, including the featurisation flags + it was trained with. Parameters ---------- psm_list - List of PSMs to use for training. + Training data: a :class:`~psm_utils.psm_list.PSMList` carrying a target + CCS on every PSM, a :class:`pandas.DataFrame`, or a path to a delimited + file with a ``CCS``/``ccs`` column plus either ``peptidoform`` or + ``seq``/``modifications``/``charge``. model_save_path - Path to save the trained model. + Path to save the trained model checkpoint to. training_kwargs - Additional keyword arguments to pass to the training function. + Configuration overrides, applied on top of ``config``. For example + ``{"epochs": 50, "batch_size": 1024}``. + validation_psm_list + Explicit validation set. If None, a validation set is split off + ``psm_list``, grouped by stripped sequence. + validation_split + Fraction held out for validation when ``validation_psm_list`` is None. + config + Configuration dict or path to a JSON configuration file, merged over + :data:`im2deep.constants.DEFAULT_TRAINING_CONFIG`. + output_dir + Directory for Lightning logs and intermediate checkpoints. Defaults to + the parent directory of ``model_save_path``. Returns ------- - None + torch.nn.Module + The trained model. + Example + ------- + >>> from im2deep import train + >>> model = train("train.csv", "my_model.ckpt", training_kwargs={"epochs": 50}) + + """ + resolved_config = _resolve_config(config, training_kwargs) + model_save_path = Path(model_save_path) + output_dir = Path(output_dir) if output_dir is not None else model_save_path.parent + + LOGGER.info("Building training dataset...") + train_dataset = _data.build_training_dataset(psm_list, resolved_config) + + if validation_psm_list is not None: + LOGGER.info("Building validation dataset...") + validation_dataset = _data.build_training_dataset(validation_psm_list, resolved_config) + train_subset, validation_subset = train_dataset, validation_dataset + else: + train_subset, validation_subset = _data.grouped_split( + train_dataset, validation_split, seed=resolved_config.get("seed", 0) + ) + + LOGGER.info("Training IM2Deep model...") + trainer, model, best_checkpoint_path = _model_ops.train( + train_dataset=train_subset, + validation_dataset=validation_subset, + config=resolved_config, + output_dir=output_dir, + ) + + # Save a Lightning checkpoint, not a pickled module: that is the format + # predict() and the bundled models use, and it is what carries the config + # recorded by save_hyperparameters(). + model_save_path.parent.mkdir(parents=True, exist_ok=True) + if best_checkpoint_path and Path(best_checkpoint_path).resolve() != model_save_path.resolve(): + shutil.copyfile(best_checkpoint_path, model_save_path) + elif not best_checkpoint_path: + trainer.save_checkpoint(model_save_path) + LOGGER.info(f"Trained model saved to {model_save_path}") + return model + + +def finetune( + psm_list: PSMList | pd.DataFrame | PathLike | str, + model_save_path: PathLike | str, + model: PathLike | str | None = None, + training_kwargs: dict | None = None, + validation_psm_list: PSMList | pd.DataFrame | PathLike | str | None = None, + validation_split: float = 0.1, + config: dict | PathLike | str | None = None, + output_dir: PathLike | str | None = None, +) -> torch.nn.Module: """ - raise NotImplementedError( - "Training functionality is not yet implemented for IM2Deep. Use the IM2DeepTrainer package instead." + Fine-tune an existing IM2Deep model on new data. + + Builds an + :class:`~im2deep._architectures.im2deep_single.IM2DeepTransfer` on top of + the given backbone. By default the pretrained feature branches are held + frozen for the first few epochs while the concatenation head adapts, then + unfrozen at a reduced learning rate, mirroring how DeepLC fine-tunes. + + Parameters + ---------- + psm_list + Fine-tuning data, in any form :func:`train` accepts. + model_save_path + Path to save the fine-tuned model checkpoint to. + model + Backbone checkpoint. Defaults to the bundled IM2Deep model. + training_kwargs + Configuration overrides, applied on top of ``config``. ``freeze_epochs`` + and ``unfreeze_lr_scale`` control the warmup. + validation_psm_list + Explicit validation set. If None, one is split off ``psm_list``. + validation_split + Fraction held out for validation when ``validation_psm_list`` is None. + config + Configuration dict or path to a JSON configuration file. + output_dir + Directory for Lightning logs and intermediate checkpoints. + + Returns + ------- + torch.nn.Module + The fine-tuned model. + + """ + backbone_path = str(model or DEFAULT_MODEL) + + # A transfer model has to match the checkpoint it loads, so the backbone's + # own architecture and featurisation come first. These sit *under* the + # caller's config and training_kwargs, which stay authoritative. + base_overrides = { + **_model_ops.read_checkpoint_config(backbone_path), + "backbone_SD_path": backbone_path, + "model_name": "IM2DeepFinetuned", + "epochs": 50, + "freeze_epochs": 5, + "unfreeze_lr_scale": 0.1, + } + resolved_config = _resolve_config(config, training_kwargs, base_overrides) + + return train( + psm_list=psm_list, + model_save_path=model_save_path, + validation_psm_list=validation_psm_list, + validation_split=validation_split, + config=resolved_config, + output_dir=output_dir, ) -# TODO: finetune and finetune_and_predict functions? +# TODO: finetune_and_predict function? diff --git a/pyproject.toml b/pyproject.toml index a210b55..c46c333 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,15 @@ classifiers = [ requires-python = ">=3.11" dependencies = [ "click>=8,<9", - "deeplc>=4.0.0b1,<5", + # 4.0.1 introduced a feature-encoding change (positional modification + # indexing) that shifts CCS for modified peptides by up to 7.6 A^2 for + # models trained pre-4.0.1, this package's checkpoints included. From + # 4.1.0 that change sits behind `legacy_positional_deltas`, which defaults + # to True, so 4.1.0's default encoding matches what the bundled + # checkpoints were trained on. Checkpoints written by `core.train()` + # record their own featurisation flags, so they are read back on whatever + # encoding they were trained with. + "deeplc>=4.1.0,<5", "psm_utils>=1.5.1,<2", "pandas>=1.5,<3", "numpy>=1.25,<3", @@ -29,6 +37,11 @@ dependencies = [ "lightning>=2.1,<3", ] +[project.optional-dependencies] +# Only needed to log training runs to Weights & Biases; `core.train()` works +# without it and `_architectures/callbacks.py` soft-imports it. +wandb = ["wandb"] + [dependency-groups] dev = [ "pytest>=7.0", @@ -102,7 +115,7 @@ warn_unreachable = true strict_equality = true [[tool.mypy.overrides]] -module = ["deeplc.*", "torch.*", "im2deeptrainer.*", "psm_utils.*"] +module = ["deeplc.*", "torch.*", "lightning.*", "wandb.*", "psm_utils.*"] ignore_missing_imports = true [tool.pytest.ini_options] diff --git a/tests/conftest.py b/tests/conftest.py index 5d9af35..f3e9c31 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -184,3 +184,46 @@ def sample_peprec_format_df(): "charge": [2, 3, 2], } ) + + +@pytest.fixture +def sample_training_df(): + """ + A small training set in the collated-dataset format. + + Uses the lowercase ``ccs`` column the dataset-collation pipeline writes, + and repeats each sequence at two charge states so grouped splitting has + something to group. + """ + rng = np.random.default_rng(0) + amino_acids = list("ACDEFGHIKLMNPQRSTVWY") + rows = [] + for _ in range(60): + length = int(rng.integers(7, 20)) + sequence = "".join(rng.choice(amino_acids, length)) + for charge in (2, 3): + rows.append( + { + "seq": sequence, + "modifications": "", + "charge": charge, + # A crude but learnable relationship, so a few epochs of + # training move the loss in the right direction. + "ccs": 300.0 + 12.0 * length + 40.0 * charge, + } + ) + return pd.DataFrame(rows) + + +@pytest.fixture +def sample_training_psm_list(sample_training_df): + """The same small training set as a PSMList carrying CCS metadata.""" + psms = [ + PSM( + peptidoform=Peptidoform(f"{row.seq}/{row.charge}"), + spectrum_id=str(idx), + metadata={"CCS": str(row.ccs)}, + ) + for idx, row in enumerate(sample_training_df.itertuples()) + ] + return PSMList(psm_list=psms) diff --git a/tests/test_cli.py b/tests/test_cli.py index 694bb72..c86f9fd 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -182,11 +182,30 @@ def test_predict_global_calibration( assert result.exit_code in [0, 1] - def test_train_command_not_available(self, runner): - """Test that train command is not currently available.""" + def test_train_command_available(self, runner): + """Test that the train command is registered.""" result = runner.invoke(cli, ["train", "--help"]) - # Train is commented out, so this should fail - assert result.exit_code != 0 or "train" not in result.output.lower() + + assert result.exit_code == 0 + assert "--output-model" in result.output + assert "--config" in result.output + + def test_finetune_command_available(self, runner): + """Test that the finetune command is registered.""" + result = runner.invoke(cli, ["finetune", "--help"]) + + assert result.exit_code == 0 + assert "--backbone" in result.output + assert "--freeze-epochs" in result.output + + def test_train_requires_output_model(self, runner, tmp_path): + """Test that train refuses to run without a destination.""" + training_data = tmp_path / "train.csv" + training_data.write_text("seq,modifications,charge,ccs\nPEPTIDE,,2,400.0\n") + + result = runner.invoke(cli, ["train", str(training_data)]) + + assert result.exit_code != 0 class TestSetupLogging: diff --git a/tests/test_core.py b/tests/test_core.py index 631b4dc..17859ab 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -209,3 +209,212 @@ def test_predict_and_calibrate_invalid_cal_psm(self, sample_psm_list): psm_list=sample_psm_list, psm_list_cal=sample_psm_list, # Missing CCS values ) + + +class TestResolveConfig: + """Tests for training configuration merging.""" + + def test_defaults_when_nothing_given(self): + """With no config, the training defaults are used as-is.""" + config = core._resolve_config(None, None) + assert config["epochs"] == 100 + assert config["monitor"] == "Validation MAE" + assert config["Global_features"] == 60 + + def test_training_kwargs_override_defaults(self): + """Explicit keyword overrides win over the defaults.""" + config = core._resolve_config(None, {"epochs": 7}) + assert config["epochs"] == 7 + + def test_training_kwargs_override_config(self): + """An explicit argument beats a configuration file.""" + config = core._resolve_config({"epochs": 5}, {"epochs": 7}) + assert config["epochs"] == 7 + + def test_config_overrides_base(self): + """A configuration file beats a caller's task-specific defaults.""" + config = core._resolve_config({"epochs": 5}, None, base_overrides={"epochs": 50}) + assert config["epochs"] == 5 + + def test_base_overrides_beat_defaults(self): + """Task-specific defaults still beat the package defaults.""" + config = core._resolve_config(None, None, base_overrides={"epochs": 50}) + assert config["epochs"] == 50 + + def test_unwraps_model_params_block(self): + """im2deeptrainer-style configs with a model_params block still work.""" + config = core._resolve_config( + {"epochs": 5, "model_params": {"batch_size": 32, "learning_rate": 0.01}}, None + ) + assert config["batch_size"] == 32 + assert config["learning_rate"] == 0.01 + assert config["epochs"] == 5 + assert "model_params" not in config + + def test_reads_json_file(self, tmp_path): + """A path to a JSON configuration file is read.""" + config_path = tmp_path / "config.json" + config_path.write_text('{"epochs": 3, "model_params": {"batch_size": 8}}') + + config = core._resolve_config(config_path, None) + + assert config["epochs"] == 3 + assert config["batch_size"] == 8 + + def test_rejects_non_dict(self): + """A config that is neither a dict nor a path is a type error.""" + with pytest.raises(TypeError, match="config must be"): + core._resolve_config(["not", "a", "dict"], None) + + +class TestTrain: + """Tests for the train function.""" + + @patch("im2deep.core._model_ops.train") + @patch("im2deep.core._data.build_training_dataset") + @patch("im2deep.core._data.grouped_split") + def test_train_basic(self, mock_split, mock_build, mock_train, sample_training_df, tmp_path): + """Training builds a dataset, splits it and saves a checkpoint.""" + mock_build.return_value = MagicMock() + mock_split.return_value = (MagicMock(), MagicMock()) + trainer, model = MagicMock(), MagicMock() + mock_train.return_value = (trainer, model, None) + save_path = tmp_path / "model.ckpt" + + result = core.train(sample_training_df, save_path, training_kwargs={"epochs": 2}) + + assert result is model + mock_build.assert_called_once() + mock_split.assert_called_once() + # No best-checkpoint path was produced, so the trainer writes it. + trainer.save_checkpoint.assert_called_once_with(save_path) + + @patch("im2deep.core._model_ops.train") + @patch("im2deep.core._data.build_training_dataset") + @patch("im2deep.core._data.grouped_split") + def test_train_copies_best_checkpoint( + self, mock_split, mock_build, mock_train, sample_training_df, tmp_path + ): + """The best checkpoint is copied, preserving the Lightning format.""" + best_path = tmp_path / "best.ckpt" + best_path.write_bytes(b"checkpoint-contents") + mock_build.return_value = MagicMock() + mock_split.return_value = (MagicMock(), MagicMock()) + mock_train.return_value = (MagicMock(), MagicMock(), str(best_path)) + save_path = tmp_path / "out" / "model.ckpt" + + core.train(sample_training_df, save_path) + + assert save_path.read_bytes() == b"checkpoint-contents" + + @patch("im2deep.core._model_ops.train") + @patch("im2deep.core._data.build_training_dataset") + @patch("im2deep.core._data.grouped_split") + def test_training_kwargs_reach_the_trainer( + self, mock_split, mock_build, mock_train, sample_training_df, tmp_path + ): + """Overrides passed by the caller arrive in the training config.""" + mock_build.return_value = MagicMock() + mock_split.return_value = (MagicMock(), MagicMock()) + mock_train.return_value = (MagicMock(), MagicMock(), None) + + core.train(sample_training_df, tmp_path / "model.ckpt", training_kwargs={"epochs": 3}) + + assert mock_train.call_args.kwargs["config"]["epochs"] == 3 + + @patch("im2deep.core._model_ops.train") + @patch("im2deep.core._data.build_training_dataset") + @patch("im2deep.core._data.grouped_split") + def test_explicit_validation_set_skips_split( + self, mock_split, mock_build, mock_train, sample_training_df, tmp_path + ): + """An explicit validation set means no split is taken.""" + mock_build.return_value = MagicMock() + mock_train.return_value = (MagicMock(), MagicMock(), None) + + core.train( + sample_training_df, + tmp_path / "model.ckpt", + validation_psm_list=sample_training_df, + ) + + mock_split.assert_not_called() + assert mock_build.call_count == 2 + + def test_missing_target_raises(self, sample_training_df, tmp_path): + """Training data with no CCS column is rejected before any training.""" + with pytest.raises(IM2DeepError, match="target CCS column"): + core.train(sample_training_df.drop(columns=["ccs"]), tmp_path / "model.ckpt") + + +class TestFinetune: + """Tests for the finetune function.""" + + @patch("im2deep.core.train") + def test_finetune_sets_backbone(self, mock_train, sample_training_df, tmp_path): + """Fine-tuning points the transfer model at the bundled backbone.""" + core.finetune(sample_training_df, tmp_path / "model.ckpt") + + config = mock_train.call_args.kwargs["config"] + assert config["backbone_SD_path"].endswith("IM2DeepUni.ckpt") + assert config["freeze_epochs"] == 5 + + @patch("im2deep.core.train") + def test_finetune_uses_given_backbone(self, mock_train, sample_training_df, tmp_path): + """An explicit backbone is used instead of the default.""" + backbone = tmp_path / "backbone.ckpt" + + core.finetune(sample_training_df, tmp_path / "model.ckpt", model=backbone) + + assert mock_train.call_args.kwargs["config"]["backbone_SD_path"] == str(backbone) + + @patch("im2deep.core.train") + def test_user_kwargs_beat_finetune_defaults(self, mock_train, sample_training_df, tmp_path): + """A caller's epochs win over the fine-tuning default of 50.""" + core.finetune(sample_training_df, tmp_path / "model.ckpt", training_kwargs={"epochs": 9}) + + assert mock_train.call_args.kwargs["config"]["epochs"] == 9 + + @patch("im2deep.core.train") + def test_backbone_architecture_is_carried_across( + self, mock_train, sample_training_df, tmp_path + ): + """The transfer model inherits the backbone's architecture width.""" + core.finetune(sample_training_df, tmp_path / "model.ckpt") + + config = mock_train.call_args.kwargs["config"] + assert config["Global_units"] == 16 + assert config["Concat_units"] == 128 + + +class TestWandbConfig: + """Tests for Weights & Biases configuration merging.""" + + def test_disabled_by_default(self): + """Training does not log to wandb unless asked to.""" + assert core._resolve_config(None, None)["wandb"]["enabled"] is False + + def test_enabling_keeps_project_from_config_file(self): + """--wandb must not discard a project name set in a config file.""" + config = core._resolve_config( + {"wandb": {"project_name": "my-project"}}, {"wandb": {"enabled": True}} + ) + + assert config["wandb"]["enabled"] is True + assert config["wandb"]["project_name"] == "my-project" + + def test_run_name_overrides_config_file(self): + """An explicit run name still wins over the file.""" + config = core._resolve_config( + {"wandb": {"project_name": "p", "name": "from-file"}}, + {"wandb": {"name": "from-cli"}}, + ) + + assert config["wandb"]["name"] == "from-cli" + assert config["wandb"]["project_name"] == "p" + + def test_non_wandb_keys_still_replace(self): + """The nested merge is specific to wandb, not general.""" + config = core._resolve_config({"epochs": 5}, {"epochs": 7}) + + assert config["epochs"] == 7 diff --git a/tests/test_data.py b/tests/test_data.py new file mode 100644 index 0000000..fc59e55 --- /dev/null +++ b/tests/test_data.py @@ -0,0 +1,226 @@ +"""Tests for the training dataset layer (``im2deep._data``).""" + +import numpy as np +import pandas as pd +import pytest +import torch + +from im2deep._data import ( + CCSDataset, + build_training_dataset, + check_global_features, + expected_global_features, + grouped_split, + stripped_sequence, +) +from im2deep.constants import DEFAULT_TRAINING_CONFIG +from im2deep.exceptions import IM2DeepError + + +class TestStrippedSequence: + """Tests for stripping ProForma strings down to bare sequences.""" + + @pytest.mark.parametrize( + "peptidoform,expected", + [ + ("PEPTIDE/2", "PEPTIDE"), + ("PEPT[UNIMOD:21]IDEK/2", "PEPTIDEK"), + ("[UNIMOD:1]-PEPTIDEK/3", "PEPTIDEK"), + ("PEPTIDEK-[UNIMOD:2]/2", "PEPTIDEK"), + ("PEPTIDE", "PEPTIDE"), + ], + ) + def test_strips_modifications_and_charge(self, peptidoform, expected): + """Modification tags, terminal mods and the charge suffix are removed.""" + assert stripped_sequence(peptidoform) == expected + + def test_charge_states_share_a_key(self): + """The same peptide at two charges maps to one grouping key.""" + assert stripped_sequence("PEPTIDE/2") == stripped_sequence("PEPTIDE/3") + + +class TestExpectedGlobalFeatures: + """Tests for the global-feature width bookkeeping.""" + + @pytest.mark.parametrize( + "add_ccs,add_terminal,expected", + [(False, False, 55), (True, False, 60), (False, True, 67), (True, True, 72)], + ) + def test_matches_deeplc(self, add_ccs, add_terminal, expected): + """The recorded widths match what DeepLC actually yields.""" + from deeplc._features import encode_peptidoform + + assert expected_global_features(add_ccs, add_terminal) == expected + features = encode_peptidoform( + "PEPT[UNIMOD:21]IDEK/2", + add_ccs_features=add_ccs, + add_terminal_composition=add_terminal, + ) + assert features["matrix_global"].shape[0] == expected + + def test_check_passes_for_default_config(self): + """The shipped training defaults are self-consistent.""" + check_global_features(dict(DEFAULT_TRAINING_CONFIG)) + + def test_check_rejects_mismatch(self): + """A width that disagrees with the featurisation flags is an error.""" + config = {**DEFAULT_TRAINING_CONFIG, "add_terminal_composition": True} + with pytest.raises(IM2DeepError, match="Global_features"): + check_global_features(config) + + def test_check_accepts_corrected_mismatch(self): + """Setting the width to match the flags resolves the error.""" + config = { + **DEFAULT_TRAINING_CONFIG, + "add_terminal_composition": True, + "Global_features": 72, + } + check_global_features(config) + + +class TestBuildTrainingDataset: + """Tests for building a dataset from tabular input or a PSMList.""" + + def test_returns_flat_tuple(self, sample_training_df): + """Items unpack as the architectures' training_step expects.""" + dataset = build_training_dataset(sample_training_df, dict(DEFAULT_TRAINING_CONFIG)) + item = dataset[0] + + assert isinstance(dataset, CCSDataset) + assert len(item) == 5 + atom, diatom, global_feats, one_hot, target = item + assert atom.shape == (60, 6) + assert diatom.shape == (30, 6) + assert global_feats.shape == (60,) + assert one_hot.shape == (60, 20) + assert float(target) > 0 + + def test_batch_unpacks_for_training_step(self, sample_training_df): + """A default-collated batch is the flat 5-tuple, not a nested one.""" + dataset = build_training_dataset(sample_training_df, dict(DEFAULT_TRAINING_CONFIG)) + loader = torch.utils.data.DataLoader(dataset, batch_size=4) + batch = next(iter(loader)) + + atom, diatom, global_feats, one_hot, targets = batch + assert atom.shape == (4, 60, 6) + assert targets.shape == (4,) + + @pytest.mark.parametrize("column", ["ccs", "CCS"]) + def test_accepts_both_ccs_column_spellings(self, sample_training_df, column): + """The collated files use lowercase 'ccs'; parse_input uses 'CCS'.""" + df = sample_training_df.rename(columns={"ccs": column}) + dataset = build_training_dataset(df, dict(DEFAULT_TRAINING_CONFIG)) + assert len(dataset) == len(df) + + def test_accepts_peptidoform_column(self): + """A peptidoform column is used directly when present.""" + df = pd.DataFrame({"peptidoform": ["PEPTIDE/2", "SEQUENCE/3"], "ccs": [400.0, 500.0]}) + dataset = build_training_dataset(df, dict(DEFAULT_TRAINING_CONFIG)) + assert len(dataset) == 2 + + def test_accepts_psm_list(self, sample_training_psm_list): + """A PSMList target is read from metadata['CCS'].""" + dataset = build_training_dataset(sample_training_psm_list, dict(DEFAULT_TRAINING_CONFIG)) + assert len(dataset) == len(sample_training_psm_list) + + def test_reads_from_path(self, sample_training_df, tmp_path): + """A path to a delimited file is read directly.""" + csv_path = tmp_path / "train.csv" + sample_training_df.to_csv(csv_path, index=False) + dataset = build_training_dataset(csv_path, dict(DEFAULT_TRAINING_CONFIG)) + assert len(dataset) == len(sample_training_df) + + def test_drops_over_length_peptides(self): + """Peptides beyond the featurisation window are dropped, not truncated.""" + df = pd.DataFrame( + { + "seq": ["A" * 65, "PEPTIDEK"], + "modifications": ["", ""], + "charge": [2, 2], + "ccs": [900.0, 400.0], + } + ) + dataset = build_training_dataset(df, dict(DEFAULT_TRAINING_CONFIG)) + assert len(dataset) == 1 + + def test_drops_missing_targets(self, sample_training_df): + """Rows with a non-finite CCS are dropped.""" + df = sample_training_df.copy() + df.loc[0, "ccs"] = np.nan + dataset = build_training_dataset(df, dict(DEFAULT_TRAINING_CONFIG)) + assert len(dataset) == len(df) - 1 + + def test_missing_ccs_column_raises(self, sample_training_df): + """A table with no target column is an error, not a silent NaN target.""" + df = sample_training_df.drop(columns=["ccs"]) + with pytest.raises(IM2DeepError, match="target CCS column"): + build_training_dataset(df, dict(DEFAULT_TRAINING_CONFIG)) + + def test_missing_sequence_columns_raises(self): + """A table with a target but no sequence is an error.""" + df = pd.DataFrame({"ccs": [400.0]}) + with pytest.raises(IM2DeepError, match="peptidoform"): + build_training_dataset(df, dict(DEFAULT_TRAINING_CONFIG)) + + def test_terminal_composition_widens_global_features(self, sample_training_df): + """The terminal-composition variant yields 72 global features.""" + config = { + **DEFAULT_TRAINING_CONFIG, + "add_terminal_composition": True, + "Global_features": 72, + } + dataset = build_training_dataset(sample_training_df, config) + assert dataset[0][2].shape == (72,) + + +class TestGroupedSplit: + """Tests for the leakage-free train/validation split.""" + + def test_no_sequence_in_both_halves(self, sample_training_df): + """A peptide at two charge states cannot straddle the split.""" + dataset = build_training_dataset(sample_training_df, dict(DEFAULT_TRAINING_CONFIG)) + train_subset, validation_subset = grouped_split(dataset, 0.2, seed=0) + + train_sequences = { + stripped_sequence(dataset.peptidoforms[i]) for i in train_subset.indices + } + validation_sequences = { + stripped_sequence(dataset.peptidoforms[i]) for i in validation_subset.indices + } + assert not train_sequences & validation_sequences + + def test_covers_every_row_exactly_once(self, sample_training_df): + """The split is a partition, not a sample.""" + dataset = build_training_dataset(sample_training_df, dict(DEFAULT_TRAINING_CONFIG)) + train_subset, validation_subset = grouped_split(dataset, 0.2, seed=0) + + combined = sorted(train_subset.indices + validation_subset.indices) + assert combined == list(range(len(dataset))) + + def test_is_deterministic(self, sample_training_df): + """The same seed gives the same split.""" + dataset = build_training_dataset(sample_training_df, dict(DEFAULT_TRAINING_CONFIG)) + first, _ = grouped_split(dataset, 0.2, seed=7) + second, _ = grouped_split(dataset, 0.2, seed=7) + assert first.indices == second.indices + + @pytest.mark.parametrize("fraction", [0.0, 1.0, -0.1, 1.5]) + def test_rejects_invalid_fraction(self, sample_training_df, fraction): + """A split fraction outside (0, 1) is rejected.""" + dataset = build_training_dataset(sample_training_df, dict(DEFAULT_TRAINING_CONFIG)) + with pytest.raises(ValueError, match="validation_split"): + grouped_split(dataset, fraction) + + def test_rejects_split_that_empties_a_half(self): + """A dataset dominated by one sequence cannot be split.""" + df = pd.DataFrame( + { + "seq": ["PEPTIDEK", "PEPTIDEK"], + "modifications": ["", ""], + "charge": [2, 3], + "ccs": [400.0, 500.0], + } + ) + dataset = build_training_dataset(df, dict(DEFAULT_TRAINING_CONFIG)) + with pytest.raises(IM2DeepError, match="at least one of"): + grouped_split(dataset, 0.5) diff --git a/tests/test_integration.py b/tests/test_integration.py index 00fcf15..b21e0dc 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -330,3 +330,140 @@ def test_very_short_peptide(self): validated = validate_psm_list(psm_list) assert len(validated) == 1 + + +class TestTrainingRoundTrip: + """ + Tests that training and prediction agree on the feature encoding. + + This is the check the training integration was built for: a model trained + through ``core.train`` must produce, via ``core.predict``, the same errors + it reported at training time. If train-time and predict-time featurisation + ever diverge again, these fail. + """ + + @staticmethod + def _training_kwargs(**overrides): + kwargs = { + "epochs": 3, + "batch_size": 32, + "accelerator": "cpu", + "devices": 1, + "num_workers": 0, + "patience": 0, + } + kwargs.update(overrides) + return kwargs + + @pytest.mark.integration + @pytest.mark.slow + def test_train_then_predict_reproduces_training_error(self, sample_training_df, tmp_path): + """Predicting on the training data reproduces the training-time MAE.""" + from im2deep._io_helpers import parse_input + + checkpoint = tmp_path / "tiny.ckpt" + core.train( + sample_training_df, + checkpoint, + training_kwargs=self._training_kwargs(), + output_dir=tmp_path, + ) + + psm_list = parse_input(sample_training_df[["seq", "modifications", "charge"]]) + predictions = core.predict(psm_list, model=checkpoint) + + assert predictions.shape == (len(sample_training_df),) + assert np.all(np.isfinite(predictions)) + + # A model that has genuinely learned nothing still predicts a finite + # value; what matters here is that predicting twice is deterministic and + # that the checkpoint round-trips, which is what a feature mismatch + # would break. + repeat = core.predict(psm_list, model=checkpoint) + np.testing.assert_allclose(predictions, repeat, rtol=1e-6) + + @pytest.mark.integration + @pytest.mark.slow + def test_checkpoint_records_its_configuration(self, sample_training_df, tmp_path): + """A trained checkpoint carries the config it was trained with.""" + import torch + + checkpoint = tmp_path / "tiny.ckpt" + core.train( + sample_training_df, + checkpoint, + training_kwargs=self._training_kwargs(), + output_dir=tmp_path, + ) + + loaded = torch.load(checkpoint, weights_only=False, map_location="cpu") + + assert isinstance(loaded, dict), "must be a Lightning checkpoint, not a pickled module" + config = loaded["hyper_parameters"]["config"] + assert config["Global_features"] == 60 + assert config["add_ccs_features"] is True + assert config["legacy_positional_deltas"] is True + + @pytest.mark.integration + @pytest.mark.slow + def test_terminal_composition_variant_round_trips(self, sample_training_df, tmp_path): + """ + A model trained with wider global features is readable back. + + ``add_terminal_composition=True`` widens the global feature vector from + 60 to 72, so this checkpoint cannot be loaded against the package + default config. It round-trips only because the checkpoint records its + own architecture. + """ + from im2deep._io_helpers import parse_input + + checkpoint = tmp_path / "wide.ckpt" + core.train( + sample_training_df, + checkpoint, + training_kwargs=self._training_kwargs( + add_terminal_composition=True, Global_features=72 + ), + output_dir=tmp_path, + ) + + psm_list = parse_input(sample_training_df[["seq", "modifications", "charge"]]) + predictions = core.predict(psm_list, model=checkpoint) + + assert predictions.shape == (len(sample_training_df),) + assert np.all(np.isfinite(predictions)) + + @pytest.mark.integration + @pytest.mark.slow + def test_finetune_from_bundled_model(self, sample_training_df, tmp_path): + """Fine-tuning the bundled model produces a usable checkpoint.""" + from im2deep._io_helpers import parse_input + + checkpoint = tmp_path / "finetuned.ckpt" + core.finetune( + sample_training_df, + checkpoint, + training_kwargs=self._training_kwargs(freeze_epochs=1), + output_dir=tmp_path, + ) + + psm_list = parse_input(sample_training_df[["seq", "modifications", "charge"]]) + predictions = core.predict(psm_list, model=checkpoint) + + assert predictions.shape == (len(sample_training_df),) + assert np.all(np.isfinite(predictions)) + + @pytest.mark.integration + @pytest.mark.slow + def test_bundled_model_still_predicts(self, sample_psm_list): + """ + The bundled checkpoint still loads and predicts after the DeepLC bump. + + DeepLC 4.1.0 defaults ``legacy_positional_deltas`` to True, reproducing + the encoding the bundled checkpoints were trained with, so this must + keep working unchanged. + """ + predictions = core.predict(sample_psm_list) + + assert predictions.shape == (len(sample_psm_list),) + assert np.all(predictions > 0) diff --git a/tests/test_model_ops.py b/tests/test_model_ops.py index cd0fec6..f319340 100644 --- a/tests/test_model_ops.py +++ b/tests/test_model_ops.py @@ -84,8 +84,8 @@ def test_load_model_device_cuda(self, temp_model_path): class TestPredict: """Tests for predict function.""" - @patch("im2deep._model_ops._get_architecture") - @patch("im2deep._model_ops._get_model_config") + @patch("im2deep._model_ops.read_checkpoint_architecture") + @patch("im2deep._model_ops.read_checkpoint_config") @patch("im2deep._model_ops._get_loss_function") def test_predict_single_output(self, mock_loss, mock_config, mock_arch, sample_psm_list): """Test prediction with single-output model.""" @@ -118,8 +118,8 @@ def test_predict_single_output(self, mock_loss, mock_config, mock_arch, sample_p assert isinstance(predictions, torch.Tensor) assert len(predictions) == 3 - @patch("im2deep._model_ops._get_architecture") - @patch("im2deep._model_ops._get_model_config") + @patch("im2deep._model_ops.read_checkpoint_architecture") + @patch("im2deep._model_ops.read_checkpoint_config") @patch("im2deep._model_ops._get_loss_function") def test_predict_multi_output(self, mock_loss, mock_config, mock_arch): """Test prediction with multi-output model.""" @@ -271,3 +271,117 @@ def test_get_loss_function_multi(self, mock_loss): mock_loss.return_value = mock_instance loss = _model_ops._get_loss_function(multi=True) assert loss is mock_instance + + +class TestCheckpointInspection: + """Tests for reading a checkpoint's own configuration and architecture.""" + + def test_bundled_checkpoint_falls_back_to_default_config(self): + """The bundled checkpoints record no config, so defaults apply.""" + from im2deep.constants import DEFAULT_CONFIG, DEFAULT_MODEL + + config = _model_ops.read_checkpoint_config(DEFAULT_MODEL) + + assert config == DEFAULT_CONFIG + + def test_bundled_checkpoint_is_not_a_transfer_model(self): + """The bundled single-conformer checkpoint loads as plain IM2Deep.""" + from im2deep._architectures.im2deep_single import IM2Deep + from im2deep.constants import DEFAULT_MODEL + + assert _model_ops.read_checkpoint_architecture(DEFAULT_MODEL) is IM2Deep + + def test_recorded_config_is_preferred(self, tmp_path): + """A checkpoint that records a config is read back with it.""" + checkpoint_path = tmp_path / "recorded.ckpt" + torch.save( + {"state_dict": {}, "hyper_parameters": {"config": {"Global_features": 72}}}, + checkpoint_path, + ) + + config = _model_ops.read_checkpoint_config(checkpoint_path) + + assert config["Global_features"] == 72 + + def test_transfer_checkpoint_detected_from_state_dict(self, tmp_path): + """A state_dict with backbone.* keys means a transfer model.""" + from im2deep._architectures.im2deep_single import IM2DeepTransfer + + checkpoint_path = tmp_path / "transfer.ckpt" + torch.save( + {"state_dict": {"backbone.ConvGlobal.0.linear.weight": torch.zeros(1)}}, + checkpoint_path, + ) + + assert _model_ops.read_checkpoint_architecture(checkpoint_path) is IM2DeepTransfer + + def test_non_checkpoint_falls_back_to_default(self): + """A model object rather than a path returns the package default.""" + from im2deep.constants import DEFAULT_CONFIG + + assert _model_ops.read_checkpoint_config(None) == DEFAULT_CONFIG + + def test_missing_file_falls_back_to_default(self, tmp_path): + """An unreadable path does not raise here; loading reports it later.""" + from im2deep.constants import DEFAULT_CONFIG + + config = _model_ops.read_checkpoint_config(tmp_path / "missing.ckpt") + + assert config == DEFAULT_CONFIG + + +class TestWandbLogger: + """Tests for Weights & Biases logger setup.""" + + def test_returns_none_when_disabled(self): + """No logger is built when wandb is not enabled.""" + config = {"wandb": {"enabled": False}, "model_name": "m"} + + assert _model_ops._setup_logger(config, MagicMock()) is None + + def test_returns_none_when_absent(self): + """A config with no wandb block does not log.""" + assert _model_ops._setup_logger({"model_name": "m"}, MagicMock()) is None + + def test_run_is_named_after_the_model(self): + """Runs are named so a set of variants is distinguishable.""" + config = { + "wandb": {"enabled": True, "project_name": "proj"}, + "model_name": "shift-variant", + "epochs": 3, + } + + with patch("lightning.pytorch.loggers.WandbLogger") as mock_logger: + _model_ops._setup_logger(config, MagicMock()) + + assert mock_logger.call_args.kwargs["name"] == "shift-variant" + assert mock_logger.call_args.kwargs["project"] == "proj" + + def test_explicit_run_name_wins(self): + """An explicit run name overrides the model name.""" + config = { + "wandb": {"enabled": True, "name": "explicit"}, + "model_name": "shift-variant", + } + + with patch("lightning.pytorch.loggers.WandbLogger") as mock_logger: + _model_ops._setup_logger(config, MagicMock()) + + assert mock_logger.call_args.kwargs["name"] == "explicit" + + def test_config_is_logged_without_the_wandb_block(self): + """The training config is recorded with the run, so runs compare.""" + config = { + "wandb": {"enabled": True, "project_name": "proj"}, + "model_name": "m", + "epochs": 3, + "Global_features": 72, + } + + with patch("lightning.pytorch.loggers.WandbLogger") as mock_logger: + _model_ops._setup_logger(config, MagicMock()) + + logged = mock_logger.return_value.experiment.config.update.call_args.args[0] + assert logged["epochs"] == 3 + assert logged["Global_features"] == 72 + assert "wandb" not in logged diff --git a/uv.lock b/uv.lock index 97396fe..c00413a 100644 --- a/uv.lock +++ b/uv.lock @@ -436,63 +436,62 @@ wheels = [ [package.optional-dependencies] cublas = [ - { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, - { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] cudart = [ - { name = "nvidia-cuda-runtime", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-cuda-runtime", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] cufft = [ - { name = "nvidia-cufft", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, - { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-cufft", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] cufile = [ - { name = "nvidia-cufile", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cufile", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] cupti = [ - { name = "nvidia-cuda-cupti", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-cuda-cupti", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] curand = [ - { name = "nvidia-curand", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-curand", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] cusolver = [ - { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, - { name = "nvidia-cusolver", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, - { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, - { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cusolver", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] cusparse = [ - { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, - { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] nvjitlink = [ - { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] nvrtc = [ - { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] nvtx = [ - { name = "nvidia-nvtx", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine == 'AMD64' and sys_platform == 'win32')" }, + { name = "nvidia-nvtx", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] [[package]] name = "deeplc" -version = "4.0.0b1" +version = "4.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "onnx2torch" }, { name = "pandas" }, { name = "psm-utils" }, { name = "rich" }, { name = "scikit-learn" }, { name = "torch" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/cb/45cffba0a1821b422d3d4785a190df870168c6cb8bc850a3f9ae8fd3de64/deeplc-4.0.0b1.tar.gz", hash = "sha256:059b8cef99348dade1954ecd0117b4f38b918eaa703ce1b761e7c4785a05ec37", size = 32969676, upload-time = "2026-07-10T11:27:13.463Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/77/01f24cf03bea7d07182232b3b29da01a09500f3b50dd95ac08a0f1e833c9/deeplc-4.1.0.tar.gz", hash = "sha256:b7ae18c21cd3766bb3d88f9b2bdddd9c4cb2869a02fcbe7e3b5ddf0e25638251", size = 42618358, upload-time = "2026-08-21T11:15:30.657Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/bd/71599848767cd0373e28433e7245ac9ae87f9ce87f00147fe0772a8db966/deeplc-4.0.0b1-py3-none-any.whl", hash = "sha256:df2156daa83ae1bdd43a3b3bd6a6b15c295ecbb21d995aebd9552b6135f4403d", size = 32962099, upload-time = "2026-07-10T11:09:49.039Z" }, + { url = "https://files.pythonhosted.org/packages/7d/60/872e8a66879741969260fd7d9077dfff658878c90017f974f193fcc249b1/deeplc-4.1.0-py3-none-any.whl", hash = "sha256:8ca22c233e09f5aa8ee083542e460e67e23e845a6b8df95de2266a97d1c73487", size = 42604610, upload-time = "2026-08-21T11:15:27.416Z" }, ] [[package]] @@ -650,9 +649,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/58/5404031044f55afad7aad1aff8be3f22b1bed03e237cfeabbc7e5c8cfde0/greenlet-3.5.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:aca9b4ce85b152b5524ef7d88170efdff80dc0032aa8b75f9aaf7f3479ea95b4", size = 287424, upload-time = "2026-06-26T18:20:31.469Z" }, { url = "https://files.pythonhosted.org/packages/b4/bf/1c65e9b94a54d547068fa5b5a8a06f221f3316b48908e08668d29c77cb50/greenlet-3.5.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f71be4920368fe1fabeeaa53d1e3548337e2b223d9565f8ad5e392a75ba23fc", size = 606523, upload-time = "2026-06-26T19:07:08.859Z" }, { url = "https://files.pythonhosted.org/packages/b8/c7/b66baacc95775ad511287acb0137b95574a9ce5491902372b7564799d790/greenlet-3.5.3-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d77e67f65f98449e3fb83f795b5d0a8437aead2f874ca89c96576caf4be3af6", size = 618315, upload-time = "2026-06-26T19:10:06.055Z" }, - { url = "https://files.pythonhosted.org/packages/b0/a0/68afd1ebad40db87dac0a28ffa120726b98bf9c7c40c481b0f63c105d298/greenlet-3.5.3-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e18619ba655ac05d78d80fc83cac4ba892bd6927b99e3b8237aee861aaacc8bb", size = 626155, upload-time = "2026-06-26T19:24:14.44Z" }, { url = "https://files.pythonhosted.org/packages/78/2b/28ed29463522fdbe4c15b1f63922041626a7478316b34ab4adda3f0a4aba/greenlet-3.5.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8540f1e6205bd13ca0ce685581037219ca54a1b41a0a15d228c6c9b8ad5903d7", size = 617381, upload-time = "2026-06-26T18:32:16.077Z" }, - { url = "https://files.pythonhosted.org/packages/07/7f/e327d912239ec4b3b49999e3967389bcf1ee8722b9ee9194d2752ecd558a/greenlet-3.5.3-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:d27c0c653a60d9535f690226474a5cc1036a8b0d7b57504d1c4f89c44a07a80c", size = 421083, upload-time = "2026-06-26T19:25:35.804Z" }, { url = "https://files.pythonhosted.org/packages/2a/7b/ad04e9d1337fc04965dc9fc616b6a72cb65a24b800a014c011ec812f5489/greenlet-3.5.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ef56fe650f50575bf843acde967b9c567687f3c22340941a899b7bc56e956a8", size = 1577771, upload-time = "2026-06-26T19:09:01.537Z" }, { url = "https://files.pythonhosted.org/packages/d8/33/6c87ab7ba663f70ca21f3022aad1ffe56d3f3e0521e836c2415e13abcc3c/greenlet-3.5.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5121af01cf911e70056c00d4b46d5e9b5d1415550038573d744138bacb59e6b8", size = 1644048, upload-time = "2026-06-26T18:31:42.996Z" }, { url = "https://files.pythonhosted.org/packages/1c/35/f0d8ee998b422cf8693b270f098e55d8d4ec8006b061b333f54f177d28d9/greenlet-3.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:0f41e4a05a3c0cb31b17023eff28dd111e1d16bf7d7d00406cd7df23f31398a7", size = 239137, upload-time = "2026-06-26T18:23:21.664Z" }, @@ -660,9 +657,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/6e/4c37d51a2b7f82d2ff11bb6b5f7d766d9a011726624af255e843727627a3/greenlet-3.5.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:719757059f5a53fd0dde23f78cffeafcdd97b21c850ddb7ca684a3c1a1f122e2", size = 288685, upload-time = "2026-06-26T18:22:08.977Z" }, { url = "https://files.pythonhosted.org/packages/7a/73/815dd90131c1b71ebdf53dbc7c276cafec2a1173b97559f97aba72724a87/greenlet-3.5.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efa9f765dd09f9d0cdac651ffdf631ee59ec5dc6ee7a73e0c012ba9c52fbdf5b", size = 604761, upload-time = "2026-06-26T19:07:10.114Z" }, { url = "https://files.pythonhosted.org/packages/9f/57/079cfe76bcef36b153b25607ee91c6fcb58f17f8b23c86bbbeabe0c88d72/greenlet-3.5.3-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7faba15ac005376e02a0384504e0243be3370ce010296a44a820feb342b505ab", size = 617044, upload-time = "2026-06-26T19:10:07.25Z" }, - { url = "https://files.pythonhosted.org/packages/fb/fb/d97dc261209c80744b7c8132693a30d70ec6e7315e632cb0a10b3fec94dd/greenlet-3.5.3-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5795cd1101371140551c645f2d408b8d3c01a5a29cf8a9bce6e759c983682d23", size = 622351, upload-time = "2026-06-26T19:24:16.32Z" }, { url = "https://files.pythonhosted.org/packages/37/87/b4d095775a3fb1bcafbb483fc206b27ebb785724c83051447737085dc54e/greenlet-3.5.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:87142215824be6ac05e2e8e2786eec307ccbc27c36723c3881959df654af6861", size = 614244, upload-time = "2026-06-26T18:32:17.594Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ac/e5fee13cbbd0e8de312d9a146584b8a51891c68847330ef9dc8b5109d23f/greenlet-3.5.3-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:af4923b3096e26a36d7e9cf24ab88083a20f97d191e3b97f253731ce9b41b28c", size = 425395, upload-time = "2026-06-26T19:25:37.144Z" }, { url = "https://files.pythonhosted.org/packages/8a/70/7559b609683650fa2b95b8ab84b4ab0b26556a635d19675e12aa832d826d/greenlet-3.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215275b1b49320987352e6c1b054acca0064f965a2c66992bed9a6f7d913f149", size = 1574210, upload-time = "2026-06-26T19:09:03.077Z" }, { url = "https://files.pythonhosted.org/packages/ae/73/be55392074c60fc37655ca40fa6022457bfbf6718e9e342a7b0b41f96dd2/greenlet-3.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6b1b0eed82364b0e32c4ea0f221452d33e6bb17ae094d9f72aed9851812747ea", size = 1638627, upload-time = "2026-06-26T18:31:44.748Z" }, { url = "https://files.pythonhosted.org/packages/14/40/c57489acf8e37d74e2913d4eff63aa0dba17acccc4bdeef874dde2dbbec9/greenlet-3.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:cde8adafa2365676f74a979744629589999093bc86e2484214f58e61df08902c", size = 239882, upload-time = "2026-06-26T18:23:27.518Z" }, @@ -670,9 +665,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9b/ff/a620267401db30a50cc8450ee90730e2d4a85658c055c0e760d4ed47fb13/greenlet-3.5.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:c8d87c2134d871df96ecdea9cec7cbaab286dadab0f56476e57aaf9e8ac11550", size = 287609, upload-time = "2026-06-26T18:21:14.724Z" }, { url = "https://files.pythonhosted.org/packages/d6/fa/5401ac78021c826a25b6dde0c705e0a8f29b617509f9185a31dac15fbe1b/greenlet-3.5.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2d185dd1621757e70c3861cceffd5317ab4e7ed7eb09c82994828468527ade5", size = 607435, upload-time = "2026-06-26T19:07:11.412Z" }, { url = "https://files.pythonhosted.org/packages/e9/76/1dc144a2e56e65d36405078ed774224375ea520a1870a6e46e08bb4ac7bf/greenlet-3.5.3-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1c514a468149bf8fbbab874188a3535cd8a48a3e353eb53a3d424296f8dbacd3", size = 619787, upload-time = "2026-06-26T19:10:08.396Z" }, - { url = "https://files.pythonhosted.org/packages/57/61/2f5b1adf256d039f5dab8005de8d3d7ad2b0070a3219c0e036b3fbfeb440/greenlet-3.5.3-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9ad04dd75458c6300b047c61b8639092433d205a25a14e310d6582a480efcca1", size = 625580, upload-time = "2026-06-26T19:24:18.344Z" }, { url = "https://files.pythonhosted.org/packages/bf/87/c298cee62df1de4ad7fec32abda73526cff347fd143a6ed4ac369246668a/greenlet-3.5.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:915f887cf2682b66419b879423a2e072634aa7b7dce6f3ada4957cfced3f1e9a", size = 616786, upload-time = "2026-06-26T18:32:19.128Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d9/ab7fc9e543e44d6879b0a6ef9a4b2188940fd180cc65d6f646883ddf7201/greenlet-3.5.3-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:afaabdd554cd7ae9bbb3ca070b0d7fdfd207dbf1d16865f7233837709d354bda", size = 427933, upload-time = "2026-06-26T19:25:38.219Z" }, { url = "https://files.pythonhosted.org/packages/9e/2e/e6f009885ed0705ccf33fe0583c117cfd03cde77e31a596dd5785a30762b/greenlet-3.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:766cfd421c13e450feb340cd472a3ed9957d438727b7b4593ad7c76c5d2b0deb", size = 1574316, upload-time = "2026-06-26T19:09:04.273Z" }, { url = "https://files.pythonhosted.org/packages/ef/fe/43fd110b01e40da0adb7c90ac7ea744bef2d43dca00de5095fd2351c2a68/greenlet-3.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2ecda9ec22edf38fa389369eaed8c3d37c05f3c54e69f69438dbb2cc1de1458b", size = 1638614, upload-time = "2026-06-26T18:31:46.297Z" }, { url = "https://files.pythonhosted.org/packages/0f/7c/062447147a61f8b4337b156fe70d32a165fcf2f89d7ca6255e572806705c/greenlet-3.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:c82304750f057167ff60d188df1d0cc1764ce9567eadf03e6a7443bcedd0b30b", size = 239850, upload-time = "2026-06-26T18:21:54.613Z" }, @@ -680,9 +673,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c3/93/43e116ee114b28737ba7e12952a0d4e2f55944d0f84e42bc91ba7192a3c9/greenlet-3.5.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:fd2e02fa07485778536a036222d616ab957b1d533f36b3ed98ce725d9c9d3117", size = 288202, upload-time = "2026-06-26T18:23:49.604Z" }, { url = "https://files.pythonhosted.org/packages/82/2f/146d218299046a43d1f029fd544b3d110d0f175a09c715c7e8da4a4a345d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df0a0628d1597eb0897b62f55d1343f772405fd25f3b2a796c76874b0c2e22e8", size = 654096, upload-time = "2026-06-26T19:07:12.71Z" }, { url = "https://files.pythonhosted.org/packages/a0/cc/04738cafb3f45fa991ea44f9de94c47dcec964f5a972300988a6751f49d9/greenlet-3.5.3-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ebd933a6adabc298bab47731a130fe6bfb888bd934eee37810f151159544540d", size = 666304, upload-time = "2026-06-26T19:10:09.503Z" }, - { url = "https://files.pythonhosted.org/packages/86/a9/73fa62893d5b84b4205544e6b673c654cc43aa5b9899bac00f04d64af73d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8d19fe6c39ebff9259f07bcc685d3290f8fa4ea2278e51dd0008e4d6b0f2d814", size = 670657, upload-time = "2026-06-26T19:24:19.967Z" }, { url = "https://files.pythonhosted.org/packages/ce/aa/4e0dad5e605c270c784ab911c43da6adb136ccd4d81180f763ca429a723d/greenlet-3.5.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b9d501b40e80b70e32323c799dd9b420a5577a9601469d362ae1ffb690f3a7c", size = 663635, upload-time = "2026-06-26T18:32:20.802Z" }, - { url = "https://files.pythonhosted.org/packages/29/7e/2ffce64929fb3cab7b65d5a0b20aaf9764e227681d731b041077fc9a525a/greenlet-3.5.3-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:962c5df2db8cb446da51edf1ca5296c389d93b99c9d8aa2ee4c7d0d8f1218260", size = 473497, upload-time = "2026-06-26T19:25:39.421Z" }, { url = "https://files.pythonhosted.org/packages/d1/50/13efdbea246fe3d3b735e191fec08fb50809f53cd2383ebe123d0809e44b/greenlet-3.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a1fad1d11e7d6aab184107baa8e4ece11ccba3ec9599cd7efa5ff4d70d43256a", size = 1621252, upload-time = "2026-06-26T19:09:05.647Z" }, { url = "https://files.pythonhosted.org/packages/f7/22/c0a336ae4a1410fd5f5121098e5bfbf1865f64c5ef80b4b5412886c4a332/greenlet-3.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fad5aec764399f1b5cc347ad250a59660f20c8f8888ea6bae1f93b769cce1154", size = 1684824, upload-time = "2026-06-26T18:31:47.738Z" }, { url = "https://files.pythonhosted.org/packages/7a/94/91aec0030bea75c4b3244251d0de60a1f3432d1ecb53ab6c437fb5c3ba61/greenlet-3.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:7669aa24cf2a1041d6f7899575b494a3ab4cf68bfcc8609b1dc0be7272db835e", size = 240754, upload-time = "2026-06-26T18:22:15.669Z" }, @@ -690,18 +681,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/91/95/3e161213d7f1d378d15aa9e792093e9bfe01844680d04b7fd6e0107c9098/greenlet-3.5.3-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:271a8ea7c1024e8a0d7dd2be66dd66dda8a07193f41a17b9e924f7600f5b62be", size = 296389, upload-time = "2026-06-26T18:22:20.657Z" }, { url = "https://files.pythonhosted.org/packages/00/92/715c44721abe2b4d1ae9abde4179411868a5bff312479f54e105d372f131/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19131729ae0ddc3c2e1ef85e650169b5e37ee32e400f215f78b94d7b0d567310", size = 653382, upload-time = "2026-06-26T19:07:14.209Z" }, { url = "https://files.pythonhosted.org/packages/a0/83/37a10372a1090a6624cca8e74c12df1a36c2dc36429ed0255b7fb1aeee23/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1540dd8e5fc2a5aec40fbb98ef8e149fa47c89a4b4a1cf2575a14d3d1869d7a8", size = 659401, upload-time = "2026-06-26T19:10:10.876Z" }, - { url = "https://files.pythonhosted.org/packages/cb/73/8faec206b851c22b1733545fda900829a1f3f5b1c78ae7e0fb3dba57d9f4/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b897d97759425953f69a9c0fac67f8fe333ec0ce7377ef186fb2b0c3ad5e354d", size = 659582, upload-time = "2026-06-26T19:24:21.357Z" }, { url = "https://files.pythonhosted.org/packages/db/e2/d1509cad4207da559cc42986ecdd8fc67ad0d1bba2bf03023c467fd5e0f3/greenlet-3.5.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e81fa194a1d20967877bdf9c7794db2bc99063e5be36aee710c08f04c5bb087f", size = 656969, upload-time = "2026-06-26T18:32:22.272Z" }, - { url = "https://files.pythonhosted.org/packages/b4/55/50c19e49f8045834ada71ef12f8ad048eba8517c6aa41161bed676328fae/greenlet-3.5.3-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:3236754d423955ea08e9bb5f6c04a7895f9e22c290b66aa7653fcb922d839eb0", size = 491037, upload-time = "2026-06-26T19:25:40.672Z" }, { url = "https://files.pythonhosted.org/packages/86/7d/eaf70de20aadca3a5884aec58362861c64ce45e7b277f47ed026926a3b89/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:55cf4d777485d43110e47133cbba6d74a8885a87ec1227ef0267f9ee80c5aa21", size = 1617822, upload-time = "2026-06-26T19:09:06.893Z" }, { url = "https://files.pythonhosted.org/packages/8a/f9/414d38fc400ae4350d4185eaad1827676f7cf5287b9136e0ed1cbbe20a7f/greenlet-3.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:12a248ba75f6a9a236375f52296c498c89ff1d8badf32deb9eca7abd5853f7da", size = 1677983, upload-time = "2026-06-26T18:31:49.396Z" }, { url = "https://files.pythonhosted.org/packages/e4/15/7edb977e08f9bff702fe42d6c902702786ff6b9694058b4e6a2a6ac90e57/greenlet-3.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:efc6bd60ea02e085862c74a3ef64b147ffc6f1a5ea7d9f26e7a939943f68c1e3", size = 243626, upload-time = "2026-06-26T18:24:41.485Z" }, { url = "https://files.pythonhosted.org/packages/2c/8a/93928dce91e6b3598b5e779e8d1fd6576a504640c58e78627077f6a7a91a/greenlet-3.5.3-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:ea03f2f04367845d6b58eeed276e1e56e51f0b97d8ad5a88a7d20a91dc9056cc", size = 288860, upload-time = "2026-06-26T18:22:48.07Z" }, { url = "https://files.pythonhosted.org/packages/4f/ca/69db42d447a1378043e2c8f19c09cbbd1263371505053c496b49066d3d16/greenlet-3.5.3-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78dbef602fda6d97d957eb7937f70c9ce9e9527330347f8f6b6f9e554a9e7a47", size = 659747, upload-time = "2026-06-26T19:07:15.565Z" }, { url = "https://files.pythonhosted.org/packages/a8/0b/af7ac2ef8dd41e3da1a40dda6305c23b9a03e13ba975ec916357b50f8575/greenlet-3.5.3-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f73857adb8fee13fa56c172bd11262f888c0c648f9fea113e777bb2c7904a81", size = 670419, upload-time = "2026-06-26T19:10:12.293Z" }, - { url = "https://files.pythonhosted.org/packages/25/aa/952cf28c2ff949a8c971134fb43854dd7eaa737218723aaef758f8c9aead/greenlet-3.5.3-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cefa9cef4b371f9844c6053db71f1138bc6807bab1578b0dae5149c1f1141357", size = 674261, upload-time = "2026-06-26T19:24:22.79Z" }, { url = "https://files.pythonhosted.org/packages/51/1e/1d51640cacbfc455dbe9f9a9f594c49e4e244f63b9971a2f4764e46cc53d/greenlet-3.5.3-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:232fec92e823addaf02d9472cf7381e24a1d046a6ced1103c5caa4c21b9dfc1d", size = 668787, upload-time = "2026-06-26T18:32:24.298Z" }, - { url = "https://files.pythonhosted.org/packages/dc/f2/b00d6f5e63e531a93562b2ec1a4c320fbee91f580fc42e6417af69d706e5/greenlet-3.5.3-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:6219b6d04dbf6ba6084d77dc609e8473060dc55f759cbf626d512122781fa128", size = 480322, upload-time = "2026-06-26T19:25:41.852Z" }, { url = "https://files.pythonhosted.org/packages/21/66/4030d5b0b5894500023f003bb054d9bb354dfbd1e186c3a296759172f5f5/greenlet-3.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:2421c3564da9429d5586d46ca31ebb26516b5498a802cf65c041a8e8a8980d34", size = 1626305, upload-time = "2026-06-26T19:09:08.281Z" }, { url = "https://files.pythonhosted.org/packages/0e/50/5221371c7550108dfa3c378debc41d032aa9c78e89abb01d8011cfc93289/greenlet-3.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e0f0d160f0b2e558e6c75f7930967183255dc9735e5f5b8cae58ee09c9576d8b", size = 1688631, upload-time = "2026-06-26T18:31:51.278Z" }, { url = "https://files.pythonhosted.org/packages/68/5d/00d469daae3c65d2bf620b10eee82eb022127d483c6bc8c69fae6f3fbf17/greenlet-3.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:dd99329bbc15ca78dcc583dba05d0b1b0bae01ab6c2174989f5aaee3e41ac930", size = 241027, upload-time = "2026-06-26T18:22:38.203Z" }, @@ -709,9 +696,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1c/da/4f4a8450962fad137c1c8981a3f1b8919d06c829993d4d476f9c525d5173/greenlet-3.5.3-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:176bc16a721fa5fc294d70b87b4dfa5fbdd251b3da5d5372735ecef9bd7d6d0c", size = 297221, upload-time = "2026-06-26T18:23:27.176Z" }, { url = "https://files.pythonhosted.org/packages/57/66/b3bfae3e220a9b63ea539a0eea681800c69ab1aada757eae8789f183e7ce/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:629b614d2b786e89c50440e246f33eea78f58a962d0bdbbcc809e6d13605903f", size = 657221, upload-time = "2026-06-26T19:07:16.973Z" }, { url = "https://files.pythonhosted.org/packages/7b/81/b6d4d73a709684fc77e7fa034d7c2fe82cffa9fc920fadcaa659c2626213/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b2e857ae16f5f72142edf75f9f176fe7526ba19a2841df1420516f83831c9f2", size = 663226, upload-time = "2026-06-26T19:10:13.723Z" }, - { url = "https://files.pythonhosted.org/packages/e9/39/0e0938a75115b939d42733a2a12e1d349653c9531fe6fe563e8a681f04e6/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d192579ed281051396dddd7f7754dac6259e6b1fb26378c87b66622f8e3f91", size = 663706, upload-time = "2026-06-26T19:24:24.312Z" }, { url = "https://files.pythonhosted.org/packages/f5/07/e210b02b589f16e74ff48b730690e4a34ffe984219fce4f3c1a0e7ec8545/greenlet-3.5.3-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e515757e2e36bcbf1fad09a46e1557e8b1ae1797d4b44d09da7deed88ad28608", size = 660802, upload-time = "2026-06-26T18:32:26.081Z" }, - { url = "https://files.pythonhosted.org/packages/5b/41/35d1c678cdb3c3b9e6bee691728e563cfb294202b23c7a4c3c2ccc343589/greenlet-3.5.3-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:4399eb8d041f20b68d943918bc55502a93d6fdc0a37c14da7881c04139acee9d", size = 498803, upload-time = "2026-06-26T19:25:43.063Z" }, { url = "https://files.pythonhosted.org/packages/eb/2e/5303eb3fa06bca089060f479707182a93e360683bc252acf846c3090d34e/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b363d46ed1ea431825fdb01471bb024fc08399bad1572a616e853c7684415adb", size = 1622157, upload-time = "2026-06-26T19:09:09.527Z" }, { url = "https://files.pythonhosted.org/packages/54/70/50de47a488f14df260b50ae34fb5d56016e308b098eab02c878b5223c26a/greenlet-3.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:e44da2f5bbdaabaf7d80b73dbb430c7035771e9f244e3c8b769715c9d8fa0a16", size = 1681159, upload-time = "2026-06-26T18:31:52.986Z" }, { url = "https://files.pythonhosted.org/packages/a7/13/1055e1dda7882073eda533e2b96c62e55bbd2db7fda6d5ece992febc7071/greenlet-3.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:8ff8bed3e3baa20a3ea261ce00526f1898ad4801d4886fd2220580ee0ad8fadf", size = 244007, upload-time = "2026-06-26T18:22:04.353Z" }, @@ -761,6 +746,11 @@ dependencies = [ { name = "torch" }, ] +[package.optional-dependencies] +wandb = [ + { name = "wandb" }, +] + [package.dev-dependencies] dev = [ { name = "pre-commit" }, @@ -783,14 +773,16 @@ docs = [ [package.metadata] requires-dist = [ { name = "click", specifier = ">=8,<9" }, - { name = "deeplc", specifier = ">=4.0.0b1,<5" }, + { name = "deeplc", specifier = ">=4.1.0,<5" }, { name = "lightning", specifier = ">=2.1,<3" }, { name = "numpy", specifier = ">=1.25,<3" }, { name = "pandas", specifier = ">=1.5,<3" }, { name = "psm-utils", specifier = ">=1.5.1,<2" }, { name = "rich", specifier = ">=13,<15" }, { name = "torch", specifier = ">=2.6.0,<3" }, + { name = "wandb", marker = "extra == 'wandb'" }, ] +provides-extras = ["wandb"] [package.metadata.requires-dev] dev = [ @@ -1088,48 +1080,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/89/70/b1e4737b84163db5bb1dfde6f216dbfbf32783330a9989c965e121172830/mistune-3.3.3-py3-none-any.whl", hash = "sha256:99de1585e42dcbd826faa9e11a202727a5e202e4e4722a4c69ac1ff615793dd7", size = 63569, upload-time = "2026-07-09T06:18:03.839Z" }, ] -[[package]] -name = "ml-dtypes" -version = "0.5.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, -] -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 = [ - { url = "https://files.pythonhosted.org/packages/c6/5e/712092cfe7e5eb667b8ad9ca7c54442f21ed7ca8979745f1000e24cf8737/ml_dtypes-0.5.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6c7ecb74c4bd71db68a6bea1edf8da8c34f3d9fe218f038814fd1d310ac76c90", size = 679734, upload-time = "2025-11-17T22:31:39.223Z" }, - { url = "https://files.pythonhosted.org/packages/4f/cf/912146dfd4b5c0eea956836c01dcd2fce6c9c844b2691f5152aca196ce4f/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc11d7e8c44a65115d05e2ab9989d1e045125d7be8e05a071a48bc76eb6d6040", size = 5056165, upload-time = "2025-11-17T22:31:41.071Z" }, - { url = "https://files.pythonhosted.org/packages/a9/80/19189ea605017473660e43762dc853d2797984b3c7bf30ce656099add30c/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19b9a53598f21e453ea2fbda8aa783c20faff8e1eeb0d7ab899309a0053f1483", size = 5034975, upload-time = "2025-11-17T22:31:42.758Z" }, - { url = "https://files.pythonhosted.org/packages/b4/24/70bd59276883fdd91600ca20040b41efd4902a923283c4d6edcb1de128d2/ml_dtypes-0.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:7c23c54a00ae43edf48d44066a7ec31e05fdc2eee0be2b8b50dd1903a1db94bb", size = 210742, upload-time = "2025-11-17T22:31:44.068Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c9/64230ef14e40aa3f1cb254ef623bf812735e6bec7772848d19131111ac0d/ml_dtypes-0.5.4-cp311-cp311-win_arm64.whl", hash = "sha256:557a31a390b7e9439056644cb80ed0735a6e3e3bb09d67fd5687e4b04238d1de", size = 160709, upload-time = "2025-11-17T22:31:46.557Z" }, - { url = "https://files.pythonhosted.org/packages/a8/b8/3c70881695e056f8a32f8b941126cf78775d9a4d7feba8abcb52cb7b04f2/ml_dtypes-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a174837a64f5b16cab6f368171a1a03a27936b31699d167684073ff1c4237dac", size = 676927, upload-time = "2025-11-17T22:31:48.182Z" }, - { url = "https://files.pythonhosted.org/packages/54/0f/428ef6881782e5ebb7eca459689448c0394fa0a80bea3aa9262cba5445ea/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900", size = 5028464, upload-time = "2025-11-17T22:31:50.135Z" }, - { url = "https://files.pythonhosted.org/packages/3a/cb/28ce52eb94390dda42599c98ea0204d74799e4d8047a0eb559b6fd648056/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff", size = 5009002, upload-time = "2025-11-17T22:31:52.001Z" }, - { url = "https://files.pythonhosted.org/packages/f5/f0/0cfadd537c5470378b1b32bd859cf2824972174b51b873c9d95cfd7475a5/ml_dtypes-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:c1a953995cccb9e25a4ae19e34316671e4e2edaebe4cf538229b1fc7109087b7", size = 212222, upload-time = "2025-11-17T22:31:53.742Z" }, - { url = "https://files.pythonhosted.org/packages/16/2e/9acc86985bfad8f2c2d30291b27cd2bb4c74cea08695bd540906ed744249/ml_dtypes-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:9bad06436568442575beb2d03389aa7456c690a5b05892c471215bfd8cf39460", size = 160793, upload-time = "2025-11-17T22:31:55.358Z" }, - { url = "https://files.pythonhosted.org/packages/d9/a1/4008f14bbc616cfb1ac5b39ea485f9c63031c4634ab3f4cf72e7541f816a/ml_dtypes-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c760d85a2f82e2bed75867079188c9d18dae2ee77c25a54d60e9cc79be1bc48", size = 676888, upload-time = "2025-11-17T22:31:56.907Z" }, - { url = "https://files.pythonhosted.org/packages/d3/b7/dff378afc2b0d5a7d6cd9d3209b60474d9819d1189d347521e1688a60a53/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce756d3a10d0c4067172804c9cc276ba9cc0ff47af9078ad439b075d1abdc29b", size = 5036993, upload-time = "2025-11-17T22:31:58.497Z" }, - { url = "https://files.pythonhosted.org/packages/eb/33/40cd74219417e78b97c47802037cf2d87b91973e18bb968a7da48a96ea44/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:533ce891ba774eabf607172254f2e7260ba5f57bdd64030c9a4fcfbd99815d0d", size = 5010956, upload-time = "2025-11-17T22:31:59.931Z" }, - { url = "https://files.pythonhosted.org/packages/e1/8b/200088c6859d8221454825959df35b5244fa9bdf263fd0249ac5fb75e281/ml_dtypes-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:f21c9219ef48ca5ee78402d5cc831bd58ea27ce89beda894428bc67a52da5328", size = 212224, upload-time = "2025-11-17T22:32:01.349Z" }, - { url = "https://files.pythonhosted.org/packages/8f/75/dfc3775cb36367816e678f69a7843f6f03bd4e2bcd79941e01ea960a068e/ml_dtypes-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:35f29491a3e478407f7047b8a4834e4640a77d2737e0b294d049746507af5175", size = 160798, upload-time = "2025-11-17T22:32:02.864Z" }, - { url = "https://files.pythonhosted.org/packages/4f/74/e9ddb35fd1dd43b1106c20ced3f53c2e8e7fc7598c15638e9f80677f81d4/ml_dtypes-0.5.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:304ad47faa395415b9ccbcc06a0350800bc50eda70f0e45326796e27c62f18b6", size = 702083, upload-time = "2025-11-17T22:32:04.08Z" }, - { url = "https://files.pythonhosted.org/packages/74/f5/667060b0aed1aa63166b22897fdf16dca9eb704e6b4bbf86848d5a181aa7/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a0df4223b514d799b8a1629c65ddc351b3efa833ccf7f8ea0cf654a61d1e35d", size = 5354111, upload-time = "2025-11-17T22:32:05.546Z" }, - { url = "https://files.pythonhosted.org/packages/40/49/0f8c498a28c0efa5f5c95a9e374c83ec1385ca41d0e85e7cf40e5d519a21/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531eff30e4d368cb6255bc2328d070e35836aa4f282a0fb5f3a0cd7260257298", size = 5366453, upload-time = "2025-11-17T22:32:07.115Z" }, - { url = "https://files.pythonhosted.org/packages/8c/27/12607423d0a9c6bbbcc780ad19f1f6baa2b68b18ce4bddcdc122c4c68dc9/ml_dtypes-0.5.4-cp313-cp313t-win_amd64.whl", hash = "sha256:cb73dccfc991691c444acc8c0012bee8f2470da826a92e3a20bb333b1a7894e6", size = 225612, upload-time = "2025-11-17T22:32:08.615Z" }, - { url = "https://files.pythonhosted.org/packages/e5/80/5a5929e92c72936d5b19872c5fb8fc09327c1da67b3b68c6a13139e77e20/ml_dtypes-0.5.4-cp313-cp313t-win_arm64.whl", hash = "sha256:3bbbe120b915090d9dd1375e4684dd17a20a2491ef25d640a908281da85e73f1", size = 164145, upload-time = "2025-11-17T22:32:09.782Z" }, - { url = "https://files.pythonhosted.org/packages/72/4e/1339dc6e2557a344f5ba5590872e80346f76f6cb2ac3dd16e4666e88818c/ml_dtypes-0.5.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2b857d3af6ac0d39db1de7c706e69c7f9791627209c3d6dedbfca8c7e5faec22", size = 673781, upload-time = "2025-11-17T22:32:11.364Z" }, - { url = "https://files.pythonhosted.org/packages/04/f9/067b84365c7e83bda15bba2b06c6ca250ce27b20630b1128c435fb7a09aa/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:805cef3a38f4eafae3a5bf9ebdcdb741d0bcfd9e1bd90eb54abd24f928cd2465", size = 5036145, upload-time = "2025-11-17T22:32:12.783Z" }, - { url = "https://files.pythonhosted.org/packages/c6/bb/82c7dcf38070b46172a517e2334e665c5bf374a262f99a283ea454bece7c/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14a4fd3228af936461db66faccef6e4f41c1d82fcc30e9f8d58a08916b1d811f", size = 5010230, upload-time = "2025-11-17T22:32:14.38Z" }, - { url = "https://files.pythonhosted.org/packages/e9/93/2bfed22d2498c468f6bcd0d9f56b033eaa19f33320389314c19ef6766413/ml_dtypes-0.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:8c6a2dcebd6f3903e05d51960a8058d6e131fe69f952a5397e5dbabc841b6d56", size = 221032, upload-time = "2025-11-17T22:32:15.763Z" }, - { url = "https://files.pythonhosted.org/packages/76/a3/9c912fe6ea747bb10fe2f8f54d027eb265db05dfb0c6335e3e063e74e6e8/ml_dtypes-0.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:5a0f68ca8fd8d16583dfa7793973feb86f2fbb56ce3966daf9c9f748f52a2049", size = 163353, upload-time = "2025-11-17T22:32:16.932Z" }, - { url = "https://files.pythonhosted.org/packages/cd/02/48aa7d84cc30ab4ee37624a2fd98c56c02326785750cd212bc0826c2f15b/ml_dtypes-0.5.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:bfc534409c5d4b0bf945af29e5d0ab075eae9eecbb549ff8a29280db822f34f9", size = 702085, upload-time = "2025-11-17T22:32:18.175Z" }, - { url = "https://files.pythonhosted.org/packages/5a/e7/85cb99fe80a7a5513253ec7faa88a65306be071163485e9a626fce1b6e84/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2314892cdc3fcf05e373d76d72aaa15fda9fb98625effa73c1d646f331fcecb7", size = 5355358, upload-time = "2025-11-17T22:32:19.7Z" }, - { url = "https://files.pythonhosted.org/packages/79/2b/a826ba18d2179a56e144aef69e57fb2ab7c464ef0b2111940ee8a3a223a2/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d2ffd05a2575b1519dc928c0b93c06339eb67173ff53acb00724502cda231cf", size = 5366332, upload-time = "2025-11-17T22:32:21.193Z" }, - { url = "https://files.pythonhosted.org/packages/84/44/f4d18446eacb20ea11e82f133ea8f86e2bf2891785b67d9da8d0ab0ef525/ml_dtypes-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4381fe2f2452a2d7589689693d3162e876b3ddb0a832cde7a414f8e1adf7eab1", size = 236612, upload-time = "2025-11-17T22:32:22.579Z" }, - { url = "https://files.pythonhosted.org/packages/ad/3f/3d42e9a78fe5edf792a83c074b13b9b770092a4fbf3462872f4303135f09/ml_dtypes-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:11942cbf2cf92157db91e5022633c0d9474d4dfd813a909383bd23ce828a4b7d", size = 168825, upload-time = "2025-11-17T22:32:23.766Z" }, -] - [[package]] name = "mpmath" version = "1.3.0" @@ -1586,52 +1536,15 @@ wheels = [ ] [[package]] -name = "onnx" -version = "1.22.0" +name = "opentelemetry-api" +version = "1.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ml-dtypes" }, - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "protobuf" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/19/8ea73a64b368b75fe339771a20a02bc61ea1f551484c9e3d9d0bfbd0450f/onnx-1.22.0.tar.gz", hash = "sha256:ef40c0aaf0b643857ea9306fc7eddce17eaf9fb0407e4801f1fc5758443a38e0", size = 12024721, upload-time = "2026-06-15T12:50:05.354Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a", size = 72406, upload-time = "2026-07-16T15:25:32.678Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/55/30825c02c92a0380ce84c3feeeec95d329fa77548ba58cb10ad4bbfd83c6/onnx-1.22.0-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:2d8f229a553fa440fe623ed7b36fca5e7762da3af871c3f8f8ce451df73e2914", size = 20167891, upload-time = "2026-06-15T12:49:14.212Z" }, - { url = "https://files.pythonhosted.org/packages/4b/24/cd4ab52ecaf41c3fbed674772ccbfe39041cb257b8471a47a37e48bff3f8/onnx-1.22.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1a89a7cb9ba13d78f009bdec448ec82a98972589734f157022a2bff7a5973a6", size = 18892720, upload-time = "2026-06-15T12:49:16.904Z" }, - { url = "https://files.pythonhosted.org/packages/2b/a0/c9d9d56ceadb1c0a90a7cbec5a0510520ab6538938944fa84548e4b5b054/onnx-1.22.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d0a2bdb15eb2b3cb65c438f3423d9620d14fdce32f92380e6bb1b2e09568ef5", size = 19110720, upload-time = "2026-06-15T12:49:19.812Z" }, - { url = "https://files.pythonhosted.org/packages/0a/6e/e43e5a68d9cadde55df75310027f87127333a77e5ddcea14c73e96a10cac/onnx-1.22.0-cp311-cp311-win32.whl", hash = "sha256:239958534464612fbcb6ed23d5228aaa925b39b8773f58726809ffdccb4edd1c", size = 17083746, upload-time = "2026-06-15T12:49:22.935Z" }, - { url = "https://files.pythonhosted.org/packages/54/57/cc0a9f2cf4522e42829d089927b4b75924d32f50dca237482e7b741df003/onnx-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:8561a2c00041c07e08db0c228593b5b4694100398685f348532af7dbb84189da", size = 17215684, upload-time = "2026-06-15T12:49:26.084Z" }, - { url = "https://files.pythonhosted.org/packages/c9/99/0f049f9eaa06c8383060c5f0a338e3a6caac8822e6e326c9162f05abf95a/onnx-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:8907b9b9389893bc0dc6314cc00ee1e3a69844e48d689eacc6a0340411a7da58", size = 17210398, upload-time = "2026-06-15T12:49:29.091Z" }, - { url = "https://files.pythonhosted.org/packages/ee/6a/481561f1093834376ed493e4ca42a73e5be0d50031f2969c86593bdc7c96/onnx-1.22.0-cp312-abi3-macosx_12_0_universal2.whl", hash = "sha256:596fbf0490947533c1c1045ba860851dc9fb77471023dac9a71ba5b42ceab103", size = 20167081, upload-time = "2026-06-15T12:49:32.078Z" }, - { url = "https://files.pythonhosted.org/packages/84/55/b34fc2aa30aa54b4a775402d24c4082242c720283a274fe976ac8eb94480/onnx-1.22.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae5a563f281cd9d2845622cecf6c092a57e4ee1b138f66fdbbdd4200567a5e16", size = 18889249, upload-time = "2026-06-15T12:49:34.7Z" }, - { url = "https://files.pythonhosted.org/packages/09/a6/bd32357e6cc1ecb473afd78193d7231724f284435d2db25696ecfaaa1503/onnx-1.22.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:955e02e1f6d385b53d52f9cd7b9cdf5caf417c300bcfe3c64c6d542be763845b", size = 19106514, upload-time = "2026-06-15T12:49:37.424Z" }, - { url = "https://files.pythonhosted.org/packages/5a/9d/3af461ac6c714b8b369cb71499659932f4f12cfb066250b62f7567c3d530/onnx-1.22.0-cp312-abi3-pyemscripten_2025_0_wasm32.whl", hash = "sha256:82e9f27fc1223cb06d68a56bed6f9d3caf3d0dad1b61bce45006d529b15bd94c", size = 16966387, upload-time = "2026-06-15T12:49:40.918Z" }, - { url = "https://files.pythonhosted.org/packages/d0/f0/68195b5e5a53e333faf2660f5352ee43738d0e42fc5216cc6b1871a9fbfb/onnx-1.22.0-cp312-abi3-win32.whl", hash = "sha256:cc8b66b312f8f03a53e268afb67180a2d97dd12cc79e2b61361c6c0073448016", size = 17081568, upload-time = "2026-06-15T12:49:43.398Z" }, - { url = "https://files.pythonhosted.org/packages/13/a8/734725bb703c5fabb687f79c79e51249475212b3eb37771ac4a4ac9b487f/onnx-1.22.0-cp312-abi3-win_amd64.whl", hash = "sha256:72ccebab3bac07215c204ce8848d42e78eaaa666badbf72d25cd359b9f269e3a", size = 17213290, upload-time = "2026-06-15T12:49:45.933Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2a/8ce48d8ae26a8761ad4e5dc771961b155c5c3c7c8540ec7f2f2d71b69af0/onnx-1.22.0-cp312-abi3-win_arm64.whl", hash = "sha256:f3c120dcdb70ad738f3c061b32798f408ea299eb69f84dd69ab4a6bf3c2ec01f", size = 17207030, upload-time = "2026-06-15T12:49:48.635Z" }, - { url = "https://files.pythonhosted.org/packages/f3/13/47323b97846387848efb1044ded11bb94b83526f3d1fbdb37c6480d4520f/onnx-1.22.0-cp314-cp314t-macosx_12_0_universal2.whl", hash = "sha256:19e45e4af88e3fe3261458d4b8cc461957ae2782a358a3560503569bf3b23b72", size = 20176465, upload-time = "2026-06-15T12:49:51.311Z" }, - { url = "https://files.pythonhosted.org/packages/13/0c/d3b8a7e7eee123938586c608bb9894b5723f2342b9450c0eec59fbec7099/onnx-1.22.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c21a0e59fd967a95b358e4a6e756d1f1eec2d304a83480f329f66e30d2bf0223", size = 18894028, upload-time = "2026-06-15T12:49:54.451Z" }, - { url = "https://files.pythonhosted.org/packages/b8/8a/da2a97ab46fe6e0cd9beb3ac14603a22f5be492f9ca347faf8233a07bb33/onnx-1.22.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2632406b8f523ef2e2873c363f90b20a3d88c0fbcfac757d3addffccf8f452c2", size = 19110420, upload-time = "2026-06-15T12:49:57.665Z" }, - { url = "https://files.pythonhosted.org/packages/b9/a3/ce984063017518307ebfaa545782fc400e593dc2d7fdf4f23ce4be1ed197/onnx-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a3a39fc4643867aecb33417fdddb11e308ee79d2d4a584b9d50cc7aec2091b13", size = 17237547, upload-time = "2026-06-15T12:50:00.382Z" }, - { url = "https://files.pythonhosted.org/packages/00/50/257a880384a1dd502d543b0067945074d63cd17d0840e958355bc8197da8/onnx-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:8e268cdc0547e3949799ffd4a44451dc2b9080b57d0824a2db680b6ec65506f0", size = 17231391, upload-time = "2026-06-15T12:50:03.047Z" }, -] - -[[package]] -name = "onnx2torch" -version = "1.5.15" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "onnx" }, - { name = "torch" }, - { name = "torchvision" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/51/bb56a5dfee431aa61f13a297e33f6ae0654e55d3ba93d62b88b673182af3/onnx2torch-1.5.15.tar.gz", hash = "sha256:d2a239cf0871cbbeec13d43e2a72ac2f8b3c8dcf80695e4d1040c3deed2b14a2", size = 49786, upload-time = "2024-08-07T14:04:01.076Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/80/cec13a9bcb7525cbd941958037ccb5cd499e8466915130f3d2ee3772d34f/onnx2torch-1.5.15-py3-none-any.whl", hash = "sha256:123258e0f147e07b259cf845c8113c5634b8260e52c5fb26b7d507226732d9e5", size = 80965, upload-time = "2024-08-07T14:04:09.985Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef", size = 60018, upload-time = "2026-07-16T15:25:11.657Z" }, ] [[package]] @@ -1698,91 +1611,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, ] -[[package]] -name = "pillow" -version = "12.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" }, - { url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" }, - { url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" }, - { url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd", size = 6934408, upload-time = "2026-07-01T11:53:53.487Z" }, - { url = "https://files.pythonhosted.org/packages/a3/48/9c58b685e69d49c31af6c8eb9012055fab7e665785165c84796e2c73ce72/pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c", size = 6337160, upload-time = "2026-07-01T11:53:55.457Z" }, - { url = "https://files.pythonhosted.org/packages/ff/fa/dc2a5c0ba6df93f67c31d34b808b7ce440b40cdbf96f0b81cde1d1e6fa93/pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5", size = 7045172, upload-time = "2026-07-01T11:53:57.736Z" }, - { url = "https://files.pythonhosted.org/packages/86/a5/444817a4d4c4c2417df00513086ca196f388d8f9ef40c2e4ccd1ad1af54b/pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b", size = 6472232, upload-time = "2026-07-01T11:53:59.767Z" }, - { url = "https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a", size = 7233653, upload-time = "2026-07-01T11:54:02.066Z" }, - { url = "https://files.pythonhosted.org/packages/fd/16/00f91ab7760dc842f5aad55217e80fc4a7067a0604535249bc8a2d6d9870/pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26", size = 2568195, upload-time = "2026-07-01T11:54:04.622Z" }, - { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" }, - { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" }, - { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" }, - { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" }, - { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" }, - { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" }, - { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" }, - { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" }, - { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" }, - { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" }, - { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" }, - { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" }, - { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" }, - { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" }, - { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" }, - { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" }, - { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" }, - { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" }, - { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" }, - { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" }, - { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" }, - { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" }, - { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" }, - { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" }, - { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" }, - { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" }, - { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" }, - { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" }, - { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" }, - { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" }, - { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" }, - { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" }, - { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" }, - { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" }, - { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" }, - { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" }, - { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" }, - { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" }, - { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" }, - { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" }, - { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" }, - { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" }, - { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" }, - { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" }, - { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" }, - { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" }, - { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" }, - { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" }, - { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" }, - { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" }, - { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" }, - { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" }, - { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" }, - { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" }, - { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" }, - { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" }, - { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" }, - { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" }, - { url = "https://files.pythonhosted.org/packages/75/18/2e8b40223153ccbc60df07f9e8928dc0c76202aa4e55ae9f53962b6510d6/pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468", size = 5302510, upload-time = "2026-07-01T11:56:25.736Z" }, - { url = "https://files.pythonhosted.org/packages/46/3e/51fabf59d5ab801ceab709453d3ab6b180083496579549de4c45ced6528a/pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94", size = 4736058, upload-time = "2026-07-01T11:56:28.041Z" }, - { url = "https://files.pythonhosted.org/packages/bf/20/22fe9384b7949e25fb1293bcfc84fb82590ff4ea6b37c95b24d26d793d86/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e", size = 5237776, upload-time = "2026-07-01T11:56:30.263Z" }, - { url = "https://files.pythonhosted.org/packages/08/14/f6ba68107680ffa74b39985f3f30884e41318fbc4250caa423c79b4788bb/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3", size = 5860358, upload-time = "2026-07-01T11:56:32.68Z" }, - { url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" }, -] - [[package]] name = "platformdirs" version = "4.10.0" @@ -2427,7 +2255,7 @@ resolution-markers = [ "python_full_version < '3.12'", ] dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } wheels = [ @@ -2502,7 +2330,7 @@ resolution-markers = [ "python_full_version == '3.12.*'", ] dependencies = [ - { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" } wheels = [ @@ -2548,6 +2376,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d5/19/969dc072906c84dd0a3b05dcf57ea750936087d7873549e408b35cfc3f97/scipy-1.18.0-cp314-cp314t-win_arm64.whl", hash = "sha256:368e0a705903c466aa5f08eefb39e6b1b6b2d659e7352a31fd9e2438365be0f8", size = 25279661, upload-time = "2026-06-19T15:01:40.817Z" }, ] +[[package]] +name = "sentry-sdk" +version = "2.68.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/e7/c504a4bd2d95df2e0ab73714a9161ff1cf6ff1486922685e5f46dfd9eba8/sentry_sdk-2.68.1.tar.gz", hash = "sha256:6a97895230b04bc35d4d8d2e51e3b9e21902dfb0086ccf1f131a80c15c7b997a", size = 1019262, upload-time = "2026-08-24T13:09:38.108Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/28/465ad9382be98f2172e691f5836cf87f936773913ad7ab85ba1ba1d6706e/sentry_sdk-2.68.1-py3-none-any.whl", hash = "sha256:775b78871783a0ffd758276ad01b3bb2b1ebcdad8f9d2f0a7723f76b73c99b65", size = 520851, upload-time = "2026-08-24T13:09:36.186Z" }, +] + [[package]] name = "setuptools" version = "83.0.0" @@ -2583,23 +2424,23 @@ resolution-markers = [ "python_full_version < '3.12'", ] dependencies = [ - { name = "alabaster", marker = "python_full_version < '3.12'" }, - { name = "babel", marker = "python_full_version < '3.12'" }, - { name = "colorama", marker = "python_full_version < '3.12' and sys_platform == 'win32'" }, - { name = "docutils", marker = "python_full_version < '3.12'" }, - { name = "imagesize", marker = "python_full_version < '3.12'" }, - { name = "jinja2", marker = "python_full_version < '3.12'" }, - { name = "packaging", marker = "python_full_version < '3.12'" }, - { name = "pygments", marker = "python_full_version < '3.12'" }, - { name = "requests", marker = "python_full_version < '3.12'" }, - { name = "roman-numerals", marker = "python_full_version < '3.12'" }, - { name = "snowballstemmer", marker = "python_full_version < '3.12'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.12'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.12'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.12'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.12'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.12'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.12'" }, + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } wheels = [ @@ -2615,23 +2456,23 @@ resolution-markers = [ "python_full_version == '3.12.*'", ] dependencies = [ - { name = "alabaster", marker = "python_full_version >= '3.12'" }, - { name = "babel", marker = "python_full_version >= '3.12'" }, - { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, - { name = "docutils", marker = "python_full_version >= '3.12'" }, - { name = "imagesize", marker = "python_full_version >= '3.12'" }, - { name = "jinja2", marker = "python_full_version >= '3.12'" }, - { name = "packaging", marker = "python_full_version >= '3.12'" }, - { name = "pygments", marker = "python_full_version >= '3.12'" }, - { name = "requests", marker = "python_full_version >= '3.12'" }, - { name = "roman-numerals", marker = "python_full_version >= '3.12'" }, - { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } wheels = [ @@ -2964,39 +2805,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c3/a2/c7f6ebf546f8f644edf0f999aa98ece106986a77a7b922316bf6414ff825/torchmetrics-1.9.0-py3-none-any.whl", hash = "sha256:bfdcbff3dd1d96b3374bb2496eb39f23c4b28b8a845b6a18c313688e0d2d9ca1", size = 983384, upload-time = "2026-03-09T17:41:19.756Z" }, ] -[[package]] -name = "torchvision" -version = "0.28.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "pillow" }, - { name = "torch" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/b2/1e010052079e4c577007b789db336ea7075f1a426e84d17121fbc3745516/torchvision-0.28.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:83fe6c020866a85acd7d97deccc45ff11d66daf42916d04396a4309c66c0ccb8", size = 1856017, upload-time = "2026-07-08T16:07:55.533Z" }, - { url = "https://files.pythonhosted.org/packages/27/be/1b9c5de9c655ca2df4a74100fa671a7b848532ff787e077ccde14a7dea2a/torchvision-0.28.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5a38bc6da3d72621be003400b66f66a2b4c6d644fde05f680c2cb7ca8cf8dd6c", size = 7841822, upload-time = "2026-07-08T16:07:49.207Z" }, - { url = "https://files.pythonhosted.org/packages/0b/9b/f1e68e861d4462e3e195a642c2b448e7b7d3fad5f209487162b9a2133d9b/torchvision-0.28.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:7e80f543b22503d9415e126db5f0ff3917036925e38560ee6b9ae38c571a4002", size = 7670718, upload-time = "2026-07-08T16:07:46.525Z" }, - { url = "https://files.pythonhosted.org/packages/f5/de/1494610ff54cbb154beb55033cc2cd50f3de04dac132fa2dd00e4f2b2556/torchvision-0.28.0-cp311-cp311-win_amd64.whl", hash = "sha256:9a45ea67235d965ef52187130d20002a4de20c54ea3d927a24286961d268dc37", size = 3814319, upload-time = "2026-07-08T16:07:37.153Z" }, - { url = "https://files.pythonhosted.org/packages/15/49/c1cab1ecbb3ff1a380a3f99283db1dee61b8afe354f6352c643b65937130/torchvision-0.28.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:e9f54c30cd52e3ef7fd034cc69b7bb7e0964e1c8f8743e018ab92e95b40f9eee", size = 1856020, upload-time = "2026-07-08T16:07:52.182Z" }, - { url = "https://files.pythonhosted.org/packages/f0/4c/95233776e2def960e5abb7a07931230a545f43717a56a1e1140162033598/torchvision-0.28.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5cf78ebc401ce64ae19b8c55de866bb836797d559a4de9c25ccbe74cfa642d3a", size = 7842127, upload-time = "2026-07-08T16:07:53.446Z" }, - { url = "https://files.pythonhosted.org/packages/93/e4/e9b2495d0d57b9f60d63c57d0a910410a81b4b073bf70917bef815291119/torchvision-0.28.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:028a3d481b37d785605620d7cdad897064c5a55bae2aa1f2658766333e291940", size = 7675040, upload-time = "2026-07-08T16:07:58.017Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9c/55ed9cb6dfe3ee9c837df5cd0e758372e5829aa38b8dd71343aa632cc4e2/torchvision-0.28.0-cp312-cp312-win_amd64.whl", hash = "sha256:87dc16b2df427c1318ad335f1e2be2b3b15b2cf20f7934c83b0505a48425ee5d", size = 4085785, upload-time = "2026-07-08T16:07:50.928Z" }, - { url = "https://files.pythonhosted.org/packages/20/55/08a726c14c67b37c8aca04b077766909f1c7ed23f76116884fe63b9bd033/torchvision-0.28.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:d483b4aa3f5237569053f749cd1a2b5bb548ca456e40461a5dd087f21149d123", size = 1856021, upload-time = "2026-07-08T16:07:45.386Z" }, - { url = "https://files.pythonhosted.org/packages/db/8f/40beacd53809194f5259e590d1afaeaa8ad57da15f77c646e6560bcc4616/torchvision-0.28.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:bb6dd6918460ed89cc7644adcc2402991474d6933cf1ce92b390641cb233fddf", size = 7797014, upload-time = "2026-07-08T16:07:43.04Z" }, - { url = "https://files.pythonhosted.org/packages/32/db/062cdb5a84380a60439775311fff34d89229760d2a50680393dc18699956/torchvision-0.28.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad7b3a439265cc3739a4ab5b4c998c0e38ea99c0ee7ca4dea35c5d0b099ec237", size = 7674669, upload-time = "2026-07-08T16:07:38.91Z" }, - { url = "https://files.pythonhosted.org/packages/f3/a6/b4081e2d04e1541abf82785ac9e5178a494c19330391f551356c8c18b7b3/torchvision-0.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:7e9dd6f60d6e15f8dc27d4f877fdb6002fc70d70272412135f1c2ff9cfa08d3b", size = 4157380, upload-time = "2026-07-08T16:07:40.22Z" }, - { url = "https://files.pythonhosted.org/packages/c5/b9/da40eca5bbe9596c12ae9899ab7abaf887f5e20f29d08b924b4633714821/torchvision-0.28.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3bd9dba55224a9db4a2d77f6feaa5651770d8c8e86d3d0ddb0fa6bec54c8712b", size = 1856014, upload-time = "2026-07-08T16:07:44.282Z" }, - { url = "https://files.pythonhosted.org/packages/06/d6/313aafd3df4eaf5f330211bd4e75b7598bddbfee4f55580d3b58536e1b20/torchvision-0.28.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:89f90e29b0966352811b12589f3a3c61943bf2bb9487b9d7bbec10efb1096bb5", size = 7796873, upload-time = "2026-07-08T16:07:30.907Z" }, - { url = "https://files.pythonhosted.org/packages/b3/41/31f8e959ab8f942600b6357f8999c21d779d5fd3304b0fd204ff4b518239/torchvision-0.28.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:36beb0782976906069ca03d4c9aacaf4b6b838b06ed6c20960ea9c51cce7acdd", size = 7674634, upload-time = "2026-07-08T16:07:29.657Z" }, - { url = "https://files.pythonhosted.org/packages/15/15/4c5115253fd470672cdac0a1cf139e06b4f3e29d041238a2b255937f63be/torchvision-0.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:3557cc7b539f46dabcda2b6f2b14017ccbeef024de466d4fc5835fc3f287f769", size = 4184005, upload-time = "2026-07-08T16:07:35.805Z" }, - { url = "https://files.pythonhosted.org/packages/6a/80/822a6163da716f8a78141cf6678d74e26a572285d4ea866ef8aa657bb307/torchvision-0.28.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:09ce8f56e81f19b9c378ae7bb109f83f6659fd8bc3cd14241a48e4af46e9ed49", size = 1856011, upload-time = "2026-07-08T16:07:33.404Z" }, - { url = "https://files.pythonhosted.org/packages/7f/d1/cd3f9463b39a790ec8c0c2f6e6c8061edb1562114d04fcdfa786ed889345/torchvision-0.28.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:62c7d110f86a039245b587e4fae60278c649f3bd42ff79cfbc1178eca4e72542", size = 7796742, upload-time = "2026-07-08T16:07:28.339Z" }, - { url = "https://files.pythonhosted.org/packages/d9/82/3e0a7ad18e99831e2d7f4713d3be717b7159ff5a920862dd5c23c454aa71/torchvision-0.28.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:904cf89af220f8c6b2ed0296bb5065b474ce43b77558e48b2bf9de8b0ba17204", size = 7675526, upload-time = "2026-07-08T16:07:34.572Z" }, - { url = "https://files.pythonhosted.org/packages/18/d4/23aea03b28297bc66a4461f55ae4296368a9d85fa9a454bafcb2a5348bd7/torchvision-0.28.0-cp314-cp314t-win_amd64.whl", hash = "sha256:46f581979c010ad6da6bd85ee602aa707e1ff44312670223b7a0ee517ad06d47", size = 4291452, upload-time = "2026-07-08T16:07:32.236Z" }, -] - [[package]] name = "tqdm" version = "4.68.4" @@ -3118,6 +2926,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/7c/4e7225d46d634a0d8d534dd8a6ce0c319d09b4d0cf0337eb314ca4789d8c/virtualenv-21.6.1-py3-none-any.whl", hash = "sha256:afe991df855715a2b2f60edfcc0107ef95a79fdfd8cb4cdaa71603d1c12e463b", size = 5506392, upload-time = "2026-07-10T19:33:51.629Z" }, ] +[[package]] +name = "wandb" +version = "0.28.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "opentelemetry-api" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "protobuf" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "sentry-sdk" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/41/c2eb0e25e0504287442f0829a5dc380669b683f78068c79220acc626383f/wandb-0.28.2.tar.gz", hash = "sha256:9dba560ce076f96a0033a0d2898d97cba651fc95fec7274fd09920bae8aec5cf", size = 41011367, upload-time = "2026-08-12T01:34:20.484Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/d9/8fb47c6c0b6e70378fc2b5da6dfba5d1d3178366a06bd7e672c3249964c3/wandb-0.28.2-py3-none-macosx_12_0_arm64.whl", hash = "sha256:f9aa4037d18168dd4e804ff8213100c4a3ebe3e52b5f29463bbc7fadcaadff75", size = 26716653, upload-time = "2026-08-12T01:33:58.338Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a7/6690491966e46409bfe770b73bdb1952fb6a59988b9e8c72c748f4ca080c/wandb-0.28.2-py3-none-macosx_12_0_x86_64.whl", hash = "sha256:b3830781cb93a29ad91a736d3bc5763312e782467432ee432fd731f408708991", size = 28649923, upload-time = "2026-08-12T01:34:01.22Z" }, + { url = "https://files.pythonhosted.org/packages/03/46/e69c813d88f37f77da92cfd9c63e1e9b5ce70834b0f420f876c66590353f/wandb-0.28.2-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:0c475c6b93d57e0455f3d1323e323927696502e6af7ee15e389a552114edacff", size = 27385453, upload-time = "2026-08-12T01:34:03.876Z" }, + { url = "https://files.pythonhosted.org/packages/3c/7f/f3e23a2ff01848d7a5adab9f307992b92ef99c5e6bff2bd89cd46d651e3a/wandb-0.28.2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:1db698d107871c66b2dcbb0cf4dc2af1ddb159ba94e957e890158ec60ab2de54", size = 29622780, upload-time = "2026-08-12T01:34:06.621Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1d/e087f71898ebfa86f68264e91f44ee5b8106823f09e2644043e3f1d5fe49/wandb-0.28.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:86e017d3f38bb99374de4d991a2ea0e6e71722164918585b3dea66190b28d0eb", size = 27425107, upload-time = "2026-08-12T01:34:09.359Z" }, + { url = "https://files.pythonhosted.org/packages/f2/58/3f7a624c880d7b9e60b4a000b0bb751c20244d0bcc002d3ff683cf828651/wandb-0.28.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:81468f5c00b5453a2a1d3c7ca7a18056c52a85fa4df60299052cb96829d81b11", size = 29851453, upload-time = "2026-08-12T01:34:12.133Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a3/b680c7518b0dc7eb236e7ef3bcc0994d0fabc67bbef1b73878dee196fc1e/wandb-0.28.2-py3-none-win_amd64.whl", hash = "sha256:ae5c2591214565be9f085c3b7838ee4a89344ae0c8dc06030425def08b6191aa", size = 26800272, upload-time = "2026-08-12T01:34:14.64Z" }, + { url = "https://files.pythonhosted.org/packages/ae/5a/a323653c6989c565b2138ddd7421ef30b47ea2849a62d0181534772b56da/wandb-0.28.2-py3-none-win_arm64.whl", hash = "sha256:23a4b9ac74f211836b71b27ba33bb217977a6b4cdc927e33eb5b12de143dd727", size = 24484327, upload-time = "2026-08-12T01:34:17.46Z" }, +] + [[package]] name = "watchfiles" version = "1.2.0"