diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml deleted file mode 100644 index 9f0c164..0000000 --- a/.github/workflows/pages.yml +++ /dev/null @@ -1,42 +0,0 @@ -# Simple workflow for deploying static content to GitHub Pages -name: Deploy static content to Pages - -on: - # Runs on pushes targeting the default branch - push: - branches: ["main"] - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages -permissions: - contents: read - pages: write - id-token: write - -# Allow one concurrent deployment -concurrency: - group: "pages" - cancel-in-progress: true - -jobs: - # Single deploy job since we're just deploying - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Setup Pages - uses: actions/configure-pages@v2 - - name: Upload artifact - uses: actions/upload-pages-artifact@v1 - with: - # Upload entire repository - path: './docs/docs_build' - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v1 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..569c18d --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,47 @@ +name: Tests + +on: + push: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + # Ruff and mypy run inside every matrix entry rather than in a job of their own: + # mypy's result depends on the installed dependency versions, so a single + # environment cannot speak for the whole support range. + # + # The matrix lives in ci/matrix.json so that this workflow and + # `python scripts/run_matrix.py` cannot describe different environments. + matrix: + name: Load matrix + runs-on: ubuntu-latest + outputs: + include: ${{ steps.load.outputs.include }} + steps: + - uses: actions/checkout@v5 + - id: load + run: echo "include=$(jq -c '.include' ci/matrix.json)" >> "$GITHUB_OUTPUT" + + pytest: + name: ${{ matrix.name }} + needs: matrix + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.allow_failure == true }} + strategy: + fail-fast: false + matrix: + include: ${{ fromJson(needs.matrix.outputs.include) }} + + steps: + - uses: actions/checkout@v5 + - name: Install uv and Python ${{ matrix.python }} + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + python-version: ${{ matrix.python }} + # The same entry point a developer runs locally, so a green CI leg and a green + # local run mean the same thing. + - name: Run ${{ matrix.name }} + run: python scripts/run_matrix.py ${{ matrix.name }} --verbose diff --git a/.gitignore b/.gitignore index dbc56c4..33c931e 100644 --- a/.gitignore +++ b/.gitignore @@ -129,6 +129,9 @@ dmypy.json .pyre/ *.onnx *.falcon +*.fnnx +benchmark-results*.json +tmp.* test.ipynb falcon/dev_tmp.py Untitled.ipynb diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..223c17d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.analysis.typeCheckingMode": "off", +} \ No newline at end of file diff --git a/README.md b/README.md index 7bb5c64..baa7d13 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,7 @@ Falcon is a lightweight python library that allows to train production-ready mac - Simplicity: With Falcon, training a comprehensive Machine Learning pipeline is as easy as writing a single line of code. - Flexibility: Falcon offers a range of pre-set configurations, enabling swift interchangeability of internal components with just a minor parameter change. -- Extendability: Falcon's modular design, along with its extension registration procedure, allows seamless integration with virtually any framework. -- Portability: A standout feature of Falcon is its deep native support for [ONNX](https://onnx.ai/) models. This lets you export complex pipelines into a single ONNX graph, irrespective of the underlying frameworks. As a result, your model can be conveniently deployed on any platform or with almost any programming language, all without dependence on the training environment. - -## Future Developments 🔮 - -Falcon ML is under active development. We've already implemented a robust and production-ready core functionality, but there's much more to come. We plan to introduce many new features by the end of the year, so stay tuned! +- Portability: A standout feature of Falcon is its deep native support for [FNNX](https://github.com/BeastByteAI/FNNX)/[ONNX](https://onnx.ai/) models. This lets you export complex pipelines into a single production-ready file, irrespective of the underlying frameworks. As a result, your model can be conveniently deployed without any dependency on the training environment. ⭐ If you liked the project, please support us with a star! @@ -55,26 +50,14 @@ Latest version from [GitHub](https://github.com/OKUA1/falcon) pip install git+https://github.com/OKUA1/falcon ``` -Installing some of the dependencies on **Apple Silicon Macs** might not work, the workaround is to create an X86 environment using [Conda](https://docs.conda.io/en/latest/) +Optional extras add the FNNX runtime, the gradient boosting candidates and hyperparameter search. -```bash -conda create -n falcon_env -conda activate falcon_env -conda config --env --set subdir osx-64 -conda install python=3.9 -pip3 install falcon-ml +```bash +pip install "falcon-ml[runtime]" +pip install "falcon-ml[gbdt]" +pip install "falcon-ml[hpo]" ``` ## Documentation 📚 -You can find a more detailed guide as well as an API reference in our [official docs](https://beastbyteai.github.io/falcon/intro.html#). - -## Authors & Contributors ✨ - - - - - - - - -

Oleg Kostromin


Iryna Kondrashchenko


Marco Pasini

+The [user guide](docs/guide.md) covers configuration, evaluation, export and inference. + diff --git a/benchmarks/__init__.py b/benchmarks/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/benchmarks/__init__.py @@ -0,0 +1 @@ + diff --git a/benchmarks/run.py b/benchmarks/run.py new file mode 100644 index 0000000..937f19c --- /dev/null +++ b/benchmarks/run.py @@ -0,0 +1,571 @@ +from __future__ import annotations + +import argparse +import json +import logging +import math +import os +import statistics +import tempfile +import time +from collections.abc import Sequence +from dataclasses import dataclass +from pathlib import Path +from typing import Any, TypedDict, cast + +import numpy as np +import pandas as pd +from numpy import typing as npt +from sklearn import metrics +from sklearn.datasets import fetch_openml +from sklearn.model_selection import train_test_split + +from falcon import Predictor, RunConfig +from falcon.config import DATASET_AWARE_ORDERING_DEFAULT +from falcon.constants import TABULAR_CLASSIFICATION_TASK, TABULAR_REGRESSION_TASK +from falcon.runtime import Runtime + +logger = logging.getLogger("falcon.benchmarks") + + +@dataclass(frozen=True) +class BenchmarkDataset: + name: str + openml_id: int + task: str + + def __post_init__(self) -> None: + if not self.name: + raise ValueError("Dataset name must not be empty") + if isinstance(self.openml_id, bool) or self.openml_id < 1: + raise ValueError("OpenML dataset ID must be positive") + if self.task not in { + TABULAR_CLASSIFICATION_TASK, + TABULAR_REGRESSION_TASK, + }: + raise ValueError(f"Unknown benchmark task `{self.task}`") + + +DATASETS = ( + BenchmarkDataset("credit-g", 31, TABULAR_CLASSIFICATION_TASK), + BenchmarkDataset("segment", 36, TABULAR_CLASSIFICATION_TASK), + BenchmarkDataset("diabetes", 37, TABULAR_CLASSIFICATION_TASK), + BenchmarkDataset("spambase", 44, TABULAR_CLASSIFICATION_TASK), + BenchmarkDataset("vehicle", 54, TABULAR_CLASSIFICATION_TASK), + BenchmarkDataset("kc1", 1067, TABULAR_CLASSIFICATION_TASK), + BenchmarkDataset("phoneme", 1489, TABULAR_CLASSIFICATION_TASK), + BenchmarkDataset("adult", 1590, TABULAR_CLASSIFICATION_TASK), + BenchmarkDataset("cpu-act", 197, TABULAR_REGRESSION_TASK), + BenchmarkDataset("wine-quality", 287, TABULAR_REGRESSION_TASK), +) + +BenchmarkScalar = str | int | float | bool | None +BenchmarkResult = dict[str, BenchmarkScalar] + + +class BenchmarkReport(TypedDict): + schema_version: int + preset: str + dataset_aware_ordering: bool + compare_dataset_ordering: bool + ordering_gate_passed: bool | None + time_limit_seconds: float | None + random_state: int + test_size: float + inference_repeats: int + autogluon_baseline: str | None + results: list[BenchmarkResult] + + +def load_openml_dataset( + dataset: BenchmarkDataset, +) -> tuple[pd.DataFrame, pd.Series[Any]]: + features, targets = fetch_openml( + data_id=dataset.openml_id, + as_frame=True, + return_X_y=True, + parser="auto", + ) + if not isinstance(features, pd.DataFrame) or not isinstance(targets, pd.Series): + raise TypeError( + f"OpenML dataset {dataset.openml_id} did not return a single-target frame" + ) + + feature_names = [str(name) for name in features.columns] + if len(set(feature_names)) != len(feature_names): + raise ValueError( + f"OpenML dataset {dataset.openml_id} has duplicate feature names" + ) + normalized_features = features.copy() + normalized_features.columns = feature_names + normalized_targets = targets.copy() + normalized_targets.name = str(targets.name or "target") + return normalized_features, normalized_targets + + +def split_dataset( + dataset: BenchmarkDataset, + features: pd.DataFrame, + targets: pd.Series[Any], + *, + test_size: float, + random_state: int, +) -> tuple[pd.DataFrame, pd.DataFrame, pd.Series[Any], pd.Series[Any]]: + stratify = targets if dataset.task == TABULAR_CLASSIFICATION_TASK else None + train_features, test_features, train_targets, test_targets = train_test_split( + features, + targets, + test_size=test_size, + random_state=random_state, + stratify=stratify, + ) + return ( + train_features.reset_index(drop=True), + test_features.reset_index(drop=True), + train_targets.reset_index(drop=True), + test_targets.reset_index(drop=True), + ) + + +def score_predictions( + dataset: BenchmarkDataset, + targets: pd.Series[Any] | npt.ArrayLike, + predictions: npt.ArrayLike, +) -> tuple[str, float]: + if dataset.task == TABULAR_CLASSIFICATION_TASK: + score = metrics.balanced_accuracy_score( + np.asarray(targets).astype(np.str_), + np.asarray(predictions).reshape(-1).astype(np.str_), + ) + return "balanced_accuracy", float(score) + + score = metrics.root_mean_squared_error( + np.asarray(targets, dtype=np.float64), + np.asarray(predictions, dtype=np.float64).reshape(-1), + ) + return "rmse", float(score) + + +def measure_artifact_latency( + runtime: Runtime, + features: pd.DataFrame, + repeats: int, +) -> float: + runtime.predict(features) + durations: list[float] = [] + for _ in range(repeats): + started_at = time.perf_counter() + runtime.predict(features) + durations.append(time.perf_counter() - started_at) + return float(statistics.median(durations)) + + +def load_autogluon_predictor_type() -> type[Any] | None: + try: + from autogluon.tabular import TabularPredictor + except ModuleNotFoundError as error: + if error.name is not None and not error.name.startswith("autogluon"): + raise + return None + return TabularPredictor + + +def benchmark_autogluon( + predictor_type: type[Any], + dataset: BenchmarkDataset, + train_features: pd.DataFrame, + test_features: pd.DataFrame, + train_targets: pd.Series[Any], + test_targets: pd.Series[Any], + *, + workspace: Path, + time_limit: float | None, +) -> float: + label = "__falcon_benchmark_target__" + while label in train_features.columns: + label = f"_{label}" + train_data = train_features.copy() + train_data[label] = train_targets.to_numpy() + problem_type = "regression" + if dataset.task == TABULAR_CLASSIFICATION_TASK: + problem_type = "binary" if train_targets.nunique() == 2 else "multiclass" + predictor = predictor_type( + label=label, + problem_type=problem_type, + path=str(workspace / f"autogluon-{dataset.openml_id}"), + verbosity=0, + ) + fit_options: dict[str, Any] = { + "train_data": train_data, + "presets": "medium_quality", + } + if time_limit is not None: + fit_options["time_limit"] = time_limit + predictor.fit(**fit_options) + _, score = score_predictions( + dataset, test_targets, predictor.predict(test_features) + ) + return score + + +def benchmark_dataset( + dataset: BenchmarkDataset, + *, + workspace: Path, + preset: str, + time_limit: float | None, + random_state: int, + test_size: float, + inference_repeats: int, + autogluon_predictor_type: type[Any] | None, + dataset_aware_ordering: bool = DATASET_AWARE_ORDERING_DEFAULT, +) -> BenchmarkResult: + features, targets = load_openml_dataset(dataset) + train_features, test_features, train_targets, test_targets = split_dataset( + dataset, + features, + targets, + test_size=test_size, + random_state=random_state, + ) + + started_at = time.perf_counter() + predictor = Predictor( + dataset.task, + preset=preset, + config=RunConfig(dataset_aware_ordering=dataset_aware_ordering), + time_limit=time_limit, + random_state=random_state, + eval_strategy=None, + ).fit((train_features, train_targets)) + wall_time = time.perf_counter() - started_at + metric, score = score_predictions( + dataset, + test_targets, + predictor.predict(test_features), + ) + + artifact_path = workspace / f"falcon-{dataset.openml_id}.fnnx" + artifact = predictor.save(artifact_path) + runtime = Runtime(str(artifact_path)) + result: BenchmarkResult = { + "status": "ok", + "dataset": dataset.name, + "openml_id": dataset.openml_id, + "task": dataset.task, + "dataset_aware_ordering": dataset_aware_ordering, + "metric": metric, + "score": score, + "wall_time_seconds": float(wall_time), + "artifact_size_bytes": len(artifact), + "artifact_inference_latency_seconds": measure_artifact_latency( + runtime, + test_features, + inference_repeats, + ), + "train_rows": len(train_features), + "test_rows": len(test_features), + } + if autogluon_predictor_type is not None: + result["autogluon_score"] = benchmark_autogluon( + autogluon_predictor_type, + dataset, + train_features, + test_features, + train_targets, + test_targets, + workspace=workspace, + time_limit=time_limit, + ) + return result + + +def _relative_ordering_improvement( + task: str, + static_score: float, + dataset_aware_score: float, +) -> float: + improvement = dataset_aware_score - static_score + if task == TABULAR_REGRESSION_TASK: + improvement = -improvement + scale = max(abs(static_score), float(np.finfo(np.float64).eps)) + return improvement / scale + + +def benchmark_dataset_ordering( + dataset: BenchmarkDataset, + *, + workspace: Path, + preset: str, + time_limit: float | None, + random_state: int, + test_size: float, + inference_repeats: int, + autogluon_predictor_type: type[Any] | None, +) -> BenchmarkResult: + static_result = benchmark_dataset( + dataset, + workspace=workspace, + preset=preset, + time_limit=time_limit, + random_state=random_state, + test_size=test_size, + inference_repeats=inference_repeats, + autogluon_predictor_type=None, + dataset_aware_ordering=False, + ) + dataset_aware_result = benchmark_dataset( + dataset, + workspace=workspace, + preset=preset, + time_limit=time_limit, + random_state=random_state, + test_size=test_size, + inference_repeats=inference_repeats, + autogluon_predictor_type=autogluon_predictor_type, + dataset_aware_ordering=True, + ) + static_score = float(cast(float, static_result["score"])) + dataset_aware_score = float(cast(float, dataset_aware_result["score"])) + dataset_aware_result.update( + { + "static_score": static_score, + "dataset_aware_score": dataset_aware_score, + "ordering_relative_improvement": _relative_ordering_improvement( + dataset.task, + static_score, + dataset_aware_score, + ), + "static_wall_time_seconds": static_result["wall_time_seconds"], + } + ) + return dataset_aware_result + + +def ordering_quality_gate(results: Sequence[BenchmarkResult]) -> bool: + if not results or any(result.get("status") != "ok" for result in results): + return False + improvements: list[float] = [] + for result in results: + value = result.get("ordering_relative_improvement") + if ( + isinstance(value, bool) + or not isinstance(value, (int, float)) + or not math.isfinite(value) + ): + return False + improvements.append(float(value)) + return statistics.fmean(improvements) >= 0 + + +def _validate_run_settings( + datasets: Sequence[BenchmarkDataset], + preset: str, + time_limit: float | None, + test_size: float, + inference_repeats: int, + dataset_aware_ordering: bool, + compare_dataset_ordering: bool, +) -> None: + if not datasets: + raise ValueError("At least one dataset must be selected") + if not preset: + raise ValueError("preset must not be empty") + if time_limit is not None and (not math.isfinite(time_limit) or time_limit <= 0): + raise ValueError("time_limit must be a positive finite number") + if not math.isfinite(test_size) or not 0 < test_size < 1: + raise ValueError("test_size must be between zero and one") + if ( + isinstance(inference_repeats, bool) + or not isinstance(inference_repeats, int) + or inference_repeats < 1 + ): + raise ValueError("inference_repeats must be at least 1") + if not isinstance(dataset_aware_ordering, bool): + raise ValueError("dataset_aware_ordering must be a boolean") + if not isinstance(compare_dataset_ordering, bool): + raise ValueError("compare_dataset_ordering must be a boolean") + + +def _write_report(path: Path, report: BenchmarkReport) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + temporary_path = path.with_name(f".{path.name}.tmp") + temporary_path.write_text( + json.dumps(report, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + os.replace(temporary_path, path) + + +def run_benchmarks( + output_path: Path, + *, + datasets: Sequence[BenchmarkDataset] = DATASETS, + preset: str = "balanced", + time_limit: float | None = None, + random_state: int = 42, + test_size: float = 0.2, + inference_repeats: int = 5, + include_autogluon: bool = False, + dataset_aware_ordering: bool = DATASET_AWARE_ORDERING_DEFAULT, + compare_dataset_ordering: bool = False, +) -> BenchmarkReport: + _validate_run_settings( + datasets, + preset, + time_limit, + test_size, + inference_repeats, + dataset_aware_ordering, + compare_dataset_ordering, + ) + autogluon_predictor_type = ( + load_autogluon_predictor_type() if include_autogluon else None + ) + if include_autogluon and autogluon_predictor_type is None: + logger.warning("AutoGluon is not installed; its baseline will be skipped.") + report: BenchmarkReport = { + "schema_version": 2, + "preset": preset, + "dataset_aware_ordering": dataset_aware_ordering, + "compare_dataset_ordering": compare_dataset_ordering, + "ordering_gate_passed": None, + "time_limit_seconds": time_limit, + "random_state": random_state, + "test_size": test_size, + "inference_repeats": inference_repeats, + "autogluon_baseline": ( + "medium_quality" + if autogluon_predictor_type is not None + else "unavailable" + if include_autogluon + else None + ), + "results": [], + } + _write_report(output_path, report) + + with tempfile.TemporaryDirectory(prefix="falcon-benchmark-") as temporary_dir: + workspace = Path(temporary_dir) + for index, dataset in enumerate(datasets, start=1): + logger.info( + "Benchmarking %s (%d/%d)", + dataset.name, + index, + len(datasets), + ) + try: + benchmark = ( + benchmark_dataset_ordering + if compare_dataset_ordering + else benchmark_dataset + ) + benchmark_options: dict[str, Any] = { + "workspace": workspace, + "preset": preset, + "time_limit": time_limit, + "random_state": random_state, + "test_size": test_size, + "inference_repeats": inference_repeats, + "autogluon_predictor_type": autogluon_predictor_type, + } + if not compare_dataset_ordering: + benchmark_options["dataset_aware_ordering"] = dataset_aware_ordering + result = benchmark( + dataset, + **benchmark_options, + ) + except Exception as error: + logger.exception("Benchmark failed for %s", dataset.name) + result = { + "status": "failed", + "dataset": dataset.name, + "openml_id": dataset.openml_id, + "task": dataset.task, + "error": f"{type(error).__name__}: {error}", + } + report["results"].append(result) + if compare_dataset_ordering: + report["ordering_gate_passed"] = ordering_quality_gate( + report["results"] + ) + _write_report(output_path, report) + return report + + +def _positive_float(raw_value: str) -> float: + value = float(raw_value) + if not math.isfinite(value) or value <= 0: + raise argparse.ArgumentTypeError("expected a positive finite number") + return value + + +def _test_fraction(raw_value: str) -> float: + value = float(raw_value) + if not math.isfinite(value) or not 0 < value < 1: + raise argparse.ArgumentTypeError("expected a number between zero and one") + return value + + +def _positive_integer(raw_value: str) -> int: + value = int(raw_value) + if value < 1: + raise argparse.ArgumentTypeError("expected an integer of at least 1") + return value + + +def _argument_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Run the Falcon OpenML benchmark.") + parser.add_argument( + "--output", + type=Path, + default=Path("benchmark-results.json"), + ) + parser.add_argument( + "--preset", choices=("fast", "balanced", "best"), default="balanced" + ) + parser.add_argument("--time-limit", type=_positive_float) + parser.add_argument("--random-state", type=int, default=42) + parser.add_argument("--test-size", type=_test_fraction, default=0.2) + parser.add_argument("--inference-repeats", type=_positive_integer, default=5) + parser.add_argument("--autogluon", action="store_true") + parser.add_argument( + "--dataset-aware-ordering", + action=argparse.BooleanOptionalAction, + default=DATASET_AWARE_ORDERING_DEFAULT, + ) + parser.add_argument("--compare-dataset-ordering", action="store_true") + parser.add_argument( + "--dataset", + action="append", + choices=tuple(dataset.name for dataset in DATASETS), + ) + return parser + + +def main(argv: Sequence[str] | None = None) -> int: + arguments = _argument_parser().parse_args(argv) + logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s") + selected_names = set(arguments.dataset or ()) + datasets = ( + tuple(dataset for dataset in DATASETS if dataset.name in selected_names) + if selected_names + else DATASETS + ) + report = run_benchmarks( + arguments.output, + datasets=datasets, + preset=arguments.preset, + time_limit=arguments.time_limit, + random_state=arguments.random_state, + test_size=arguments.test_size, + inference_repeats=arguments.inference_repeats, + include_autogluon=arguments.autogluon, + dataset_aware_ordering=arguments.dataset_aware_ordering, + compare_dataset_ordering=arguments.compare_dataset_ordering, + ) + return int(any(result["status"] == "failed" for result in report["results"])) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/ci/matrix.json b/ci/matrix.json new file mode 100644 index 0000000..2b4ae42 --- /dev/null +++ b/ci/matrix.json @@ -0,0 +1,57 @@ +{ + "comment": "Test environments, shared by .github/workflows/tests.yml and scripts/run_matrix.py. Run one locally with `python scripts/run_matrix.py `. Python and dependency versions are not independent: an old interpreter cannot install the newest scientific stack, so each entry pairs them explicitly rather than forming a cross product.", + "include": [ + { + "name": "locked-3.10", + "python": "3.10", + "resolution": "locked", + "extras": ["runtime"] + }, + { + "name": "locked-3.11", + "python": "3.11", + "resolution": "locked", + "extras": ["runtime"] + }, + { + "name": "locked-3.12", + "python": "3.12", + "resolution": "locked", + "extras": ["runtime"] + }, + { + "name": "locked-3.13", + "python": "3.13", + "resolution": "locked", + "extras": ["runtime"] + }, + { + "name": "gbdt-3.12", + "python": "3.12", + "resolution": "locked", + "extras": ["runtime", "gbdt"] + }, + { + "name": "hpo-3.12", + "python": "3.12", + "resolution": "locked", + "extras": ["runtime", "hpo"], + "tests": ["tests/test_hpo.py", "tests/test_hpo_optional.py"] + }, + { + "name": "floor-3.10", + "python": "3.10", + "resolution": "lowest-direct", + "extras": ["runtime"], + "description": "Oldest dependencies the project claims to support, so the declared lower bounds are exercised rather than merely asserted." + }, + { + "name": "latest-3.13", + "python": "3.13", + "resolution": "highest", + "extras": ["runtime", "gbdt"], + "allow_failure": true, + "description": "Newest resolvable dependencies, ignoring the lock. Advisory: this is the leg that catches an upstream release breaking us before users do." + } + ] +} diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index d0c3cbf..0000000 --- a/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = source -BUILDDIR = build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/docs_build/.buildinfo b/docs/docs_build/.buildinfo deleted file mode 100644 index b8227e9..0000000 --- a/docs/docs_build/.buildinfo +++ /dev/null @@ -1,4 +0,0 @@ -# Sphinx build info version 1 -# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: 587d1e464de893d2cead4dc079d8c513 -tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/docs_build/.doctrees/abstract/index.doctree b/docs/docs_build/.doctrees/abstract/index.doctree deleted file mode 100644 index bc9570a..0000000 Binary files a/docs/docs_build/.doctrees/abstract/index.doctree and /dev/null differ diff --git a/docs/docs_build/.doctrees/abstract/learner.doctree b/docs/docs_build/.doctrees/abstract/learner.doctree deleted file mode 100644 index 4d15359..0000000 Binary files a/docs/docs_build/.doctrees/abstract/learner.doctree and /dev/null differ diff --git a/docs/docs_build/.doctrees/abstract/model.doctree b/docs/docs_build/.doctrees/abstract/model.doctree deleted file mode 100644 index 4e49f8a..0000000 Binary files a/docs/docs_build/.doctrees/abstract/model.doctree and /dev/null differ diff --git a/docs/docs_build/.doctrees/abstract/onnx.doctree b/docs/docs_build/.doctrees/abstract/onnx.doctree deleted file mode 100644 index fa91391..0000000 Binary files a/docs/docs_build/.doctrees/abstract/onnx.doctree and /dev/null differ diff --git a/docs/docs_build/.doctrees/abstract/optuna.doctree b/docs/docs_build/.doctrees/abstract/optuna.doctree deleted file mode 100644 index 7f1e4f6..0000000 Binary files a/docs/docs_build/.doctrees/abstract/optuna.doctree and /dev/null differ diff --git a/docs/docs_build/.doctrees/abstract/pipeline.doctree b/docs/docs_build/.doctrees/abstract/pipeline.doctree deleted file mode 100644 index 97d0839..0000000 Binary files a/docs/docs_build/.doctrees/abstract/pipeline.doctree and /dev/null differ diff --git a/docs/docs_build/.doctrees/abstract/pipeline_element.doctree b/docs/docs_build/.doctrees/abstract/pipeline_element.doctree deleted file mode 100644 index 0e8b9b8..0000000 Binary files a/docs/docs_build/.doctrees/abstract/pipeline_element.doctree and /dev/null differ diff --git a/docs/docs_build/.doctrees/abstract/processor.doctree b/docs/docs_build/.doctrees/abstract/processor.doctree deleted file mode 100644 index 30e9a9e..0000000 Binary files a/docs/docs_build/.doctrees/abstract/processor.doctree and /dev/null differ diff --git a/docs/docs_build/.doctrees/abstract/task_manager.doctree b/docs/docs_build/.doctrees/abstract/task_manager.doctree deleted file mode 100644 index 9092f69..0000000 Binary files a/docs/docs_build/.doctrees/abstract/task_manager.doctree and /dev/null differ diff --git a/docs/docs_build/.doctrees/api.doctree b/docs/docs_build/.doctrees/api.doctree deleted file mode 100644 index 74f3a74..0000000 Binary files a/docs/docs_build/.doctrees/api.doctree and /dev/null differ diff --git a/docs/docs_build/.doctrees/available_configurations.doctree b/docs/docs_build/.doctrees/available_configurations.doctree deleted file mode 100644 index c90f13a..0000000 Binary files a/docs/docs_build/.doctrees/available_configurations.doctree and /dev/null differ diff --git a/docs/docs_build/.doctrees/environment.pickle b/docs/docs_build/.doctrees/environment.pickle deleted file mode 100644 index 646d0e9..0000000 Binary files a/docs/docs_build/.doctrees/environment.pickle and /dev/null differ diff --git a/docs/docs_build/.doctrees/high_level_api.doctree b/docs/docs_build/.doctrees/high_level_api.doctree deleted file mode 100644 index 01c5ff3..0000000 Binary files a/docs/docs_build/.doctrees/high_level_api.doctree and /dev/null differ diff --git a/docs/docs_build/.doctrees/index.doctree b/docs/docs_build/.doctrees/index.doctree deleted file mode 100644 index 3b56d15..0000000 Binary files a/docs/docs_build/.doctrees/index.doctree and /dev/null differ diff --git a/docs/docs_build/.doctrees/intro.doctree b/docs/docs_build/.doctrees/intro.doctree deleted file mode 100644 index b1c08ca..0000000 Binary files a/docs/docs_build/.doctrees/intro.doctree and /dev/null differ diff --git a/docs/docs_build/.doctrees/registry.doctree b/docs/docs_build/.doctrees/registry.doctree deleted file mode 100644 index 2a89028..0000000 Binary files a/docs/docs_build/.doctrees/registry.doctree and /dev/null differ diff --git a/docs/docs_build/.doctrees/sklearn_api.doctree b/docs/docs_build/.doctrees/sklearn_api.doctree deleted file mode 100644 index 330915b..0000000 Binary files a/docs/docs_build/.doctrees/sklearn_api.doctree and /dev/null differ diff --git a/docs/docs_build/.doctrees/tabular/index.doctree b/docs/docs_build/.doctrees/tabular/index.doctree deleted file mode 100644 index f50636b..0000000 Binary files a/docs/docs_build/.doctrees/tabular/index.doctree and /dev/null differ diff --git a/docs/docs_build/.doctrees/tabular/learners/optuna_learner.doctree b/docs/docs_build/.doctrees/tabular/learners/optuna_learner.doctree deleted file mode 100644 index 08381a5..0000000 Binary files a/docs/docs_build/.doctrees/tabular/learners/optuna_learner.doctree and /dev/null differ diff --git a/docs/docs_build/.doctrees/tabular/learners/plain_learner.doctree b/docs/docs_build/.doctrees/tabular/learners/plain_learner.doctree deleted file mode 100644 index d2f4314..0000000 Binary files a/docs/docs_build/.doctrees/tabular/learners/plain_learner.doctree and /dev/null differ diff --git a/docs/docs_build/.doctrees/tabular/learners/super_learner.doctree b/docs/docs_build/.doctrees/tabular/learners/super_learner.doctree deleted file mode 100644 index 325b2b7..0000000 Binary files a/docs/docs_build/.doctrees/tabular/learners/super_learner.doctree and /dev/null differ diff --git a/docs/docs_build/.doctrees/tabular/models/hgbt_clf.doctree b/docs/docs_build/.doctrees/tabular/models/hgbt_clf.doctree deleted file mode 100644 index 2be8554..0000000 Binary files a/docs/docs_build/.doctrees/tabular/models/hgbt_clf.doctree and /dev/null differ diff --git a/docs/docs_build/.doctrees/tabular/models/hgbt_regr.doctree b/docs/docs_build/.doctrees/tabular/models/hgbt_regr.doctree deleted file mode 100644 index a9058e8..0000000 Binary files a/docs/docs_build/.doctrees/tabular/models/hgbt_regr.doctree and /dev/null differ diff --git a/docs/docs_build/.doctrees/tabular/models/stacking_clf.doctree b/docs/docs_build/.doctrees/tabular/models/stacking_clf.doctree deleted file mode 100644 index 7d0df9d..0000000 Binary files a/docs/docs_build/.doctrees/tabular/models/stacking_clf.doctree and /dev/null differ diff --git a/docs/docs_build/.doctrees/tabular/models/stacking_regr.doctree b/docs/docs_build/.doctrees/tabular/models/stacking_regr.doctree deleted file mode 100644 index e95a507..0000000 Binary files a/docs/docs_build/.doctrees/tabular/models/stacking_regr.doctree and /dev/null differ diff --git a/docs/docs_build/.doctrees/tabular/pipelines/simple_pipeline.doctree b/docs/docs_build/.doctrees/tabular/pipelines/simple_pipeline.doctree deleted file mode 100644 index 72f6197..0000000 Binary files a/docs/docs_build/.doctrees/tabular/pipelines/simple_pipeline.doctree and /dev/null differ diff --git a/docs/docs_build/.doctrees/tabular/processors/label_decoder.doctree b/docs/docs_build/.doctrees/tabular/processors/label_decoder.doctree deleted file mode 100644 index 8bf7c38..0000000 Binary files a/docs/docs_build/.doctrees/tabular/processors/label_decoder.doctree and /dev/null differ diff --git a/docs/docs_build/.doctrees/tabular/processors/mm_encoder.doctree b/docs/docs_build/.doctrees/tabular/processors/mm_encoder.doctree deleted file mode 100644 index 8bc37fa..0000000 Binary files a/docs/docs_build/.doctrees/tabular/processors/mm_encoder.doctree and /dev/null differ diff --git a/docs/docs_build/.doctrees/tabular/processors/scaler_and_encoder.doctree b/docs/docs_build/.doctrees/tabular/processors/scaler_and_encoder.doctree deleted file mode 100644 index f1593b9..0000000 Binary files a/docs/docs_build/.doctrees/tabular/processors/scaler_and_encoder.doctree and /dev/null differ diff --git a/docs/docs_build/.doctrees/tabular/tab_manager.doctree b/docs/docs_build/.doctrees/tabular/tab_manager.doctree deleted file mode 100644 index de2ed76..0000000 Binary files a/docs/docs_build/.doctrees/tabular/tab_manager.doctree and /dev/null differ diff --git a/docs/docs_build/_sources/abstract/index.rst.txt b/docs/docs_build/_sources/abstract/index.rst.txt deleted file mode 100644 index ca5fe09..0000000 --- a/docs/docs_build/_sources/abstract/index.rst.txt +++ /dev/null @@ -1,28 +0,0 @@ -Abstract -=================== - -.. currentmodule:: falcon.abstract - -.. autosummary:: - - TaskManager - Model - Pipeline - PipelineElement - Learner - Processor - ONNXConvertible - OptunaMixin - - -.. toctree:: - :hidden: - - task_manager - model - pipeline - pipeline_element - learner - processor - onnx - optuna \ No newline at end of file diff --git a/docs/docs_build/_sources/abstract/learner.rst.txt b/docs/docs_build/_sources/abstract/learner.rst.txt deleted file mode 100644 index f3c528c..0000000 --- a/docs/docs_build/_sources/abstract/learner.rst.txt +++ /dev/null @@ -1,8 +0,0 @@ -Learner -========================= - -.. autoclass:: falcon.abstract.Learner - :members: - :private-members: - :inherited-members: - :special-members: __init__ \ No newline at end of file diff --git a/docs/docs_build/_sources/abstract/model.rst.txt b/docs/docs_build/_sources/abstract/model.rst.txt deleted file mode 100644 index 8bf6a24..0000000 --- a/docs/docs_build/_sources/abstract/model.rst.txt +++ /dev/null @@ -1,8 +0,0 @@ -Model -========================= - -.. autoclass:: falcon.abstract.Model - :members: - :private-members: - :inherited-members: - :special-members: __init__ \ No newline at end of file diff --git a/docs/docs_build/_sources/abstract/onnx.rst.txt b/docs/docs_build/_sources/abstract/onnx.rst.txt deleted file mode 100644 index 5a39450..0000000 --- a/docs/docs_build/_sources/abstract/onnx.rst.txt +++ /dev/null @@ -1,8 +0,0 @@ -ONNXConvertible -========================= - -.. autoclass:: falcon.abstract.ONNXConvertible - :members: - :private-members: - :inherited-members: - :special-members: __init__ \ No newline at end of file diff --git a/docs/docs_build/_sources/abstract/optuna.rst.txt b/docs/docs_build/_sources/abstract/optuna.rst.txt deleted file mode 100644 index 7120332..0000000 --- a/docs/docs_build/_sources/abstract/optuna.rst.txt +++ /dev/null @@ -1,8 +0,0 @@ -OptunaMixin -========================= - -.. autoclass:: falcon.abstract.OptunaMixin - :members: - :private-members: - :inherited-members: - :special-members: __init__ \ No newline at end of file diff --git a/docs/docs_build/_sources/abstract/pipeline.rst.txt b/docs/docs_build/_sources/abstract/pipeline.rst.txt deleted file mode 100644 index daa4baf..0000000 --- a/docs/docs_build/_sources/abstract/pipeline.rst.txt +++ /dev/null @@ -1,8 +0,0 @@ -Pipeline -========================= - -.. autoclass:: falcon.abstract.Pipeline - :members: - :private-members: - :inherited-members: - :special-members: __init__ \ No newline at end of file diff --git a/docs/docs_build/_sources/abstract/pipeline_element.rst.txt b/docs/docs_build/_sources/abstract/pipeline_element.rst.txt deleted file mode 100644 index 0d79dcf..0000000 --- a/docs/docs_build/_sources/abstract/pipeline_element.rst.txt +++ /dev/null @@ -1,8 +0,0 @@ -PipelineElement -========================= - -.. autoclass:: falcon.abstract.PipelineElement - :members: - :private-members: - :inherited-members: - :special-members: __init__ \ No newline at end of file diff --git a/docs/docs_build/_sources/abstract/processor.rst.txt b/docs/docs_build/_sources/abstract/processor.rst.txt deleted file mode 100644 index 6eabcdf..0000000 --- a/docs/docs_build/_sources/abstract/processor.rst.txt +++ /dev/null @@ -1,8 +0,0 @@ -Processor -========================= - -.. autoclass:: falcon.abstract.Processor - :members: - :private-members: - :inherited-members: - :special-members: __init__ \ No newline at end of file diff --git a/docs/docs_build/_sources/abstract/task_manager.rst.txt b/docs/docs_build/_sources/abstract/task_manager.rst.txt deleted file mode 100644 index 04dcd22..0000000 --- a/docs/docs_build/_sources/abstract/task_manager.rst.txt +++ /dev/null @@ -1,8 +0,0 @@ -TaskManager -========================= - -.. autoclass:: falcon.abstract.TaskManager - :members: - :private-members: - :inherited-members: - :special-members: __init__ \ No newline at end of file diff --git a/docs/docs_build/_sources/api.rst.txt b/docs/docs_build/_sources/api.rst.txt deleted file mode 100644 index 529db7b..0000000 --- a/docs/docs_build/_sources/api.rst.txt +++ /dev/null @@ -1,10 +0,0 @@ -API reference -================================== - -.. toctree:: - high_level_api - sklearn_api - abstract/index - tabular/index - registry - available_configurations \ No newline at end of file diff --git a/docs/docs_build/_sources/available_configurations.rst.txt b/docs/docs_build/_sources/available_configurations.rst.txt deleted file mode 100644 index 12c6841..0000000 --- a/docs/docs_build/_sources/available_configurations.rst.txt +++ /dev/null @@ -1,70 +0,0 @@ -Available Configurations -============================== - -The tables below list both main and additional configurations that can be used. -Additional configurations should be used with caution as they may not be suitable for certain datasets. It is reccomended to always choose one of the main configurations. - -*********************************************************************************** -Configurations for tabular_regression/tabular_classification tasks -*********************************************************************************** - -.. list-table:: - :width: 100% - :widths: 18 12 70 - :header-rows: 1 - - * - Name - - Extension - - Description - * - SuperLearner - - -- - - | Uses :doc:`tabular/learners/super_learner` to build a stacking ensemble of base estimators. - | SuperLearner combines multiple individual estimators to make predictions with greater accuracy than any of the individual estimators alone. - | Additionaly, it learns to weigh the predictions of each individual model, optimizing the combination to maximize performance on the given task. - | SuperLearner is more suitable for smaller datasets, but the produced models tend to be relatively large. - * - OptunaLearner - - -- - - | Uses :doc:`tabular/learners/optuna_learner`. - | It builds a model and optimizes its hyperparameters using Optuna framework; :doc:`tabular/models/hgbt_clf`/:doc:`tabular/models/hgbt_regr` is used as a default model. - | Since OptunaLearner focuses on finetuning a single model, the produced model is not very large in size, but the optimization procedure can be very long. - * - PlainLearner - - -- - - | Uses :doc:`tabular/learners/plain_learner`. - | It builds a model using default hyperparameters; :doc:`tabular/models/hgbt_clf`/:doc:`tabular/models/hgbt_regr` is used as a default model. - | PlainLearner is very fast, thus it is a good choice for building initial baselines or automizing preprocessing steps. - -.. dropdown:: Additional configurations - - .. list-table:: - :width: 100% - :widths: 18 12 70 - :header-rows: 1 - - * - Name - - Extension - - Description - * - SuperLearner.mini - - -- - - | Uses :doc:`tabular/learners/super_learner` with a config for small datasets. - | The dataset is considered small when the number of cells after preprocessing [n_rows*n_columns] is < 80k. - * - SuperLearner.mid - - -- - - | Uses :doc:`tabular/learners/super_learner` with a config for mid datasets. - | The dataset is considered small when the number of cells after preprocessing [n_rows*n_columns] is < 4kk. - * - SuperLearner.large - - -- - - | Uses :doc:`tabular/learners/super_learner` with a config for large datasets. - | The dataset is considered small when the number of cells after preprocessing [n_rows*n_columns] is < 16kk. - * - SuperLearner.xlarge - - -- - - | Uses :doc:`tabular/learners/super_learner` with a config for x-large datasets. - | The dataset is considered small when the number of cells after preprocessing [n_rows*n_columns] is >= 16kk. - * - OptunaLearner.hgbt - - -- - - | Uses :doc:`tabular/learners/optuna_learner`. - | It builds a :doc:`tabular/models/hgbt_clf`/:doc:`tabular/models/hgbt_regr` model with hyperparameters optimized by Optuna framework. - * - PlainLearner.hgbt - - -- - - | Uses :doc:`tabular/learners/plain_learner`. - | It builds a :doc:`tabular/models/hgbt_clf`/:doc:`tabular/models/hgbt_regr` model with default hyperparameters. - diff --git a/docs/docs_build/_sources/high_level_api.rst.txt b/docs/docs_build/_sources/high_level_api.rst.txt deleted file mode 100644 index 15b97fe..0000000 --- a/docs/docs_build/_sources/high_level_api.rst.txt +++ /dev/null @@ -1,8 +0,0 @@ -High level API -======================== - -.. autofunction:: falcon.AutoML - -.. autofunction:: falcon.initialize - -.. autofunction:: falcon.run_model \ No newline at end of file diff --git a/docs/docs_build/_sources/index.rst.txt b/docs/docs_build/_sources/index.rst.txt deleted file mode 100644 index e83a8b3..0000000 --- a/docs/docs_build/_sources/index.rst.txt +++ /dev/null @@ -1,18 +0,0 @@ -.. Falcon documentation master file, created by - sphinx-quickstart on Thu Sep 1 12:30:41 2022. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to Falcon's documentation! -================================== - -.. toctree:: - intro - api - -.. Indices and tables -.. ================== - -.. * :ref:`genindex` -.. * :ref:`modindex` -.. * :ref:`search` diff --git a/docs/docs_build/_sources/intro.rst.txt b/docs/docs_build/_sources/intro.rst.txt deleted file mode 100644 index 0c23521..0000000 --- a/docs/docs_build/_sources/intro.rst.txt +++ /dev/null @@ -1,180 +0,0 @@ -Getting started -================================== - -**Train a powerful Machine Learning model in a single line of code with Falcon!** - -Falcon is a simple and lightweight AutoML library designed for people who want to train a model on a custom dataset in an instant even without specific data-science knowledge. Simply give Falcon your dataset and specify which feature you want the ML model to predict. Falcon will do the rest! - -Falcon allows the trained models to be immediately used in production by saving them in the widely used ONNX format. No need to write custom code to save complicated models to ONNX anymore! - -Installation -=================== - -Stable release from `PyPi `_ - -.. code-block:: bash - - pip install falcon-ml - -Latest version from `GitHub `_ - -.. code-block:: bash - - pip install git+https://github.com/OKUA1/falcon - -Installing some of the dependencies on **Apple Silicon Macs** might not work, the workaround is to create an X86 environment using `Conda `_ - -.. code-block:: bash - - conda create -n falcon_env - conda activate falcon_env - conda config --env --set subdir osx-64 - conda install python=3.9 - pip3 install falcon-ml - -Usage -================== - -Currently, Falcon supports only tabular datasets and two tasks: 'tabular_classification' and 'tabular_regression'. - -The easiest way to use the library is by using the highest level API as shown below: - -.. code-block:: python - - from falcon import AutoML - - AutoML(task = 'tabular_classification', train_data = 'titanic.csv') - - -This single line of code will read and prepare the dataset, scale/encode the features, encode the labels, train the model and save it as ONNX file for future inference. - -Additionally, it is also possible to explicitly specify the feature/target columns (otherwise the last column will be used as target and all other as features) and test data (otherwise 25% of training set will be kept) for evaluation report. - -.. code-block:: python - - from falcon import AutoML - - manager = AutoML(task = 'tabular_classification', train_data = 'titanic.csv', test_data = 'titanic_test.csv', features = ['sex', 'gender', 'class', 'age'], target = 'survived') - - -It is also possible to provide train/test data as a pandas dataframe, numpy array, or tuple containing X and y. In order to do that, simply pass the required object as an argument. This might be relevant in cases when custom data preparation is needed or data itself comes from non-conventional source. - -.. code-block:: python - - from falcon import AutoML - import pandas as pd - - df = pd.read_csv('titanic.csv') - X_test = pd.read_csv('X_test.csv') - y_test = pd.read_csv('y_test.csv') - - manager = AutoML(task = 'tabular_classification', train_data = df, test_data = (X_test, y_test), features = ['sex', 'gender', 'class', 'age'], target = 'survived') - - -While AutoML function enables extremely fast experimentation, it does not provide enough control over the training steps and might be not flexible enough for more advanced users. As an alternative, it is possible to use the relevant TaskManager class either directly or by using :code:`initialize` helper function. - -.. code-block:: python - - from falcon import initialize - import pandas as pd - - test_df = pd.read_csv('titanic_test.csv') - - manager = initialize(task='tabular_classification', data='titanic.csv') - manager.train(make_eval_subset = True) - manager.performance_summary(test_df) - - -When using :code:`initialize` function it is also possible to provide a custom configuration or even a custom pipeline. For more details please check the API reference section. - -Demo datasets -================== - -You can try out falcon using one of the built-in demo datasets. - -.. code-block:: python - - from falcon import AutoML - from falcon.datasets import load_churn_dataset, load_insurance_dataset # churn -> classification; insurance -> regression - - df = load_churn_dataset() - - AutoML(task = 'tabular_classification', train_data = df) - -Making predictions with trained models -============================================ - -There are 2 ways to make a prediction using a trained model. If the input/unlabeled data is available right away, the same manager object that was used for training the model can be used. -An important thing to notice is that the input data should have the same structure as the training set (the same number, order and type of the features). This is assumed by the model, but not explicitly checked during runtime. -The recommended approach is to provide the data as a numpy array. - -.. code-block:: python - - from falcon import AutoML - import pandas as pd - - df = pd.read_csv('training_data.csv') - manager = AutoML(task = 'tabular_classification', train_data = df) - - unlabeled_data = pd.read_csv('unlabeled_data.csv').to_numpy() - predictions = manager.predict(unlabeled_data) - print(predictions) - -While this solution is straight-forward, in real-world applications the new/unlabeled data is not always available right away. Therefore, it is desirable to train a model and reuse it in the future. - -One of the key features of falcon is native `ONNX `_ support. ONNX (Open Neural Network Exchange) is an open standard for representing machine learning algorithms. This means that once the model is exported to ONNX, it can be run on any platform with available ONNX implementation. -For example, `Microsoft ONNX Rutime (ORT) `_ is available for Python, C, C++, Java, JavaScript and multiple other languages which allows to run the model virtually everywhere. There are also alternative implementations, but there is a high chance they do not support all the required operators. - -In order to simplify the interaction with ONNX Runtime, falcon provides a `run_model` function that takes the path to the ONNX model, the input data as a numpy array and returns the predictions. - -.. code-block:: python - - from falcon import run_model - import pandas as pd - - unlabeled_data = pd.read_csv('unlabeled_data.csv').to_numpy() # ONLY NUMPY ARRAYS ARE ACCEPTED AS INPUT !!! - - predictions = run_model(model_path = "/path/to/model.onnx", X = unlabeled_data) - - print(predictions) - -Below is the complete example of model training and inference using the built-in datasets. - -.. code-block:: python - - ############################################ training.py ########################################################### - from falcon import AutoML - from falcon.datasets import load_churn_dataset - - df = load_churn_dataset(mode = "training") - AutoML(task = "tabular_classification", train_data = df) - # onnx model name will be printed after the training is done, use it instead of during infernce - - ############################################ inference.py ########################################################## - from falcon import run_model - from falcon.datasets import load_churn_dataset - - X = load_churn_dataset(mode = "inference") # for this example we are reusing training dataset but without labels - predictions = run_model(model_path = ".onnx", X = X) - print(predictions) - -Manually selecting a configuration -====================================== - -All of the examples in the previous sections demonstrated how to train falcon models using the default configuration. -However, there are several configurations available and it is easily possible to switch between them by providing a single additional argument. - -For tabular classification task, by default, falcon will use a :doc:`tabular/learners/super_learner` and the sub-configuration (e.g. list of base estimators) will be chosen automatically based on the dataset size. -But if we want to specify that a 'mini' sub-configuration of the learner is to be used, we can do it by adding `config = 'SuperLearner.mini'`. - -.. code-block:: python - - AutoML(task = "tabular_classification", train_data = df, config = 'SuperLearner.mini') # SuperLearner.mini config is used - -Similarly, instead of :doc:`tabular/learners/super_learner` which builds a stacking ensemble of base estimators, it is possible to use :doc:`tabular/learners/optuna_learner` which uses a single model and performs hyperparameter optimization using the Optuna framework. - -.. code-block:: python - - AutoML(task = "tabular_classification", train_data = df, config = 'OptunaLearner') # OptunaLearner config is used - -All the available configurations can be found :doc:`here`. \ No newline at end of file diff --git a/docs/docs_build/_sources/registry.rst.txt b/docs/docs_build/_sources/registry.rst.txt deleted file mode 100644 index 18ac0c3..0000000 --- a/docs/docs_build/_sources/registry.rst.txt +++ /dev/null @@ -1,6 +0,0 @@ -Task Registry -======================== - -.. autoclass:: falcon.task_configurations.TaskConfigurationRegistry - :members: - diff --git a/docs/docs_build/_sources/sklearn_api.rst.txt b/docs/docs_build/_sources/sklearn_api.rst.txt deleted file mode 100644 index c3e25a6..0000000 --- a/docs/docs_build/_sources/sklearn_api.rst.txt +++ /dev/null @@ -1,12 +0,0 @@ -Scikit-learn API -======================== - -.. autoclass:: falcon.sklapi.FalconTabularClassifier - :members: - :inherited-members: - :special-members: __init__ - -.. autoclass:: falcon.sklapi.FalconTabularRegressor - :members: - :inherited-members: - :special-members: __init__ \ No newline at end of file diff --git a/docs/docs_build/_sources/tabular/index.rst.txt b/docs/docs_build/_sources/tabular/index.rst.txt deleted file mode 100644 index 92370fe..0000000 --- a/docs/docs_build/_sources/tabular/index.rst.txt +++ /dev/null @@ -1,37 +0,0 @@ -Tabular -=================== - -.. currentmodule:: falcon.tabular - -.. autosummary:: - - TabularTaskManager - pipelines.SimpleTabularPipeline - processors.ScalerAndEncoder - processors.MultiModalEncoder - processors.LabelDecoder - learners.SuperLearner - learners.OptunaLearner - learners.PlainLearner - models.HistGradientBoostingClassifier - models.HistGradientBoostingRegressor - models.StackingClassifier - models.StackingRegressor - - - -.. toctree:: - :hidden: - - tab_manager - pipelines/simple_pipeline - processors/scaler_and_encoder - processors/mm_encoder - processors/label_decoder - learners/super_learner - learners/optuna_learner - learners/plain_learner - models/hgbt_clf - models/hgbt_regr - models/stacking_clf - models/stacking_regr diff --git a/docs/docs_build/_sources/tabular/learners/optuna_learner.rst.txt b/docs/docs_build/_sources/tabular/learners/optuna_learner.rst.txt deleted file mode 100644 index f3a5395..0000000 --- a/docs/docs_build/_sources/tabular/learners/optuna_learner.rst.txt +++ /dev/null @@ -1,8 +0,0 @@ -OptunaLearner -========================= - -.. autoclass:: falcon.tabular.learners.OptunaLearner - :members: - :inherited-members: - :special-members: __init__ - diff --git a/docs/docs_build/_sources/tabular/learners/plain_learner.rst.txt b/docs/docs_build/_sources/tabular/learners/plain_learner.rst.txt deleted file mode 100644 index e456d43..0000000 --- a/docs/docs_build/_sources/tabular/learners/plain_learner.rst.txt +++ /dev/null @@ -1,8 +0,0 @@ -PlainLearner -========================= - -.. autoclass:: falcon.tabular.learners.PlainLearner - :members: - :inherited-members: - :special-members: __init__ - diff --git a/docs/docs_build/_sources/tabular/learners/super_learner.rst.txt b/docs/docs_build/_sources/tabular/learners/super_learner.rst.txt deleted file mode 100644 index 4fcce8f..0000000 --- a/docs/docs_build/_sources/tabular/learners/super_learner.rst.txt +++ /dev/null @@ -1,8 +0,0 @@ -SuperLearner -========================= - -.. autoclass:: falcon.tabular.learners.SuperLearner - :members: - :inherited-members: - :special-members: __init__ - \ No newline at end of file diff --git a/docs/docs_build/_sources/tabular/models/hgbt_clf.rst.txt b/docs/docs_build/_sources/tabular/models/hgbt_clf.rst.txt deleted file mode 100644 index 21c44e0..0000000 --- a/docs/docs_build/_sources/tabular/models/hgbt_clf.rst.txt +++ /dev/null @@ -1,7 +0,0 @@ -HistGradientBoostingClassifier -================================================== - -.. autoclass:: falcon.tabular.models.HistGradientBoostingClassifier - :members: - :inherited-members: - :special-members: __init__ diff --git a/docs/docs_build/_sources/tabular/models/hgbt_regr.rst.txt b/docs/docs_build/_sources/tabular/models/hgbt_regr.rst.txt deleted file mode 100644 index 6bc487f..0000000 --- a/docs/docs_build/_sources/tabular/models/hgbt_regr.rst.txt +++ /dev/null @@ -1,8 +0,0 @@ -HistGradientBoostingRegressor -================================================== - -.. autoclass:: falcon.tabular.models.HistGradientBoostingRegressor - :members: - :inherited-members: - :special-members: __init__ - diff --git a/docs/docs_build/_sources/tabular/models/stacking_clf.rst.txt b/docs/docs_build/_sources/tabular/models/stacking_clf.rst.txt deleted file mode 100644 index 8275aac..0000000 --- a/docs/docs_build/_sources/tabular/models/stacking_clf.rst.txt +++ /dev/null @@ -1,7 +0,0 @@ -StackingClassifier -========================= - -.. autoclass:: falcon.tabular.models.StackingClassifier - :members: - :inherited-members: - :special-members: __init__ diff --git a/docs/docs_build/_sources/tabular/models/stacking_regr.rst.txt b/docs/docs_build/_sources/tabular/models/stacking_regr.rst.txt deleted file mode 100644 index 1ca541e..0000000 --- a/docs/docs_build/_sources/tabular/models/stacking_regr.rst.txt +++ /dev/null @@ -1,7 +0,0 @@ -StackingRegressor -========================= - -.. autoclass:: falcon.tabular.models.StackingRegressor - :members: - :inherited-members: - :special-members: __init__ diff --git a/docs/docs_build/_sources/tabular/pipelines/simple_pipeline.rst.txt b/docs/docs_build/_sources/tabular/pipelines/simple_pipeline.rst.txt deleted file mode 100644 index 44d953e..0000000 --- a/docs/docs_build/_sources/tabular/pipelines/simple_pipeline.rst.txt +++ /dev/null @@ -1,7 +0,0 @@ -SimpleTabularPipeline -========================= - -.. autoclass:: falcon.tabular.pipelines.SimpleTabularPipeline - :members: - :inherited-members: - :special-members: __init__ \ No newline at end of file diff --git a/docs/docs_build/_sources/tabular/processors/label_decoder.rst.txt b/docs/docs_build/_sources/tabular/processors/label_decoder.rst.txt deleted file mode 100644 index 1d9634f..0000000 --- a/docs/docs_build/_sources/tabular/processors/label_decoder.rst.txt +++ /dev/null @@ -1,8 +0,0 @@ -LabelDecoder -=================== - -.. autoclass:: falcon.tabular.processors.LabelDecoder - :members: - :private-members: - :inherited-members: - :special-members: __init__ \ No newline at end of file diff --git a/docs/docs_build/_sources/tabular/processors/mm_encoder.rst.txt b/docs/docs_build/_sources/tabular/processors/mm_encoder.rst.txt deleted file mode 100644 index 5599f06..0000000 --- a/docs/docs_build/_sources/tabular/processors/mm_encoder.rst.txt +++ /dev/null @@ -1,8 +0,0 @@ -MultiModalEncoder -=================== - -.. autoclass:: falcon.tabular.processors.MultiModalEncoder - :members: - :private-members: - :inherited-members: - :special-members: __init__ \ No newline at end of file diff --git a/docs/docs_build/_sources/tabular/processors/scaler_and_encoder.rst.txt b/docs/docs_build/_sources/tabular/processors/scaler_and_encoder.rst.txt deleted file mode 100644 index 870a261..0000000 --- a/docs/docs_build/_sources/tabular/processors/scaler_and_encoder.rst.txt +++ /dev/null @@ -1,8 +0,0 @@ -ScalerAndEncoder -=================== - -.. autoclass:: falcon.tabular.processors.ScalerAndEncoder - :members: - :private-members: - :inherited-members: - :special-members: __init__ \ No newline at end of file diff --git a/docs/docs_build/_sources/tabular/tab_manager.rst.txt b/docs/docs_build/_sources/tabular/tab_manager.rst.txt deleted file mode 100644 index 9616c2a..0000000 --- a/docs/docs_build/_sources/tabular/tab_manager.rst.txt +++ /dev/null @@ -1,8 +0,0 @@ -TabularTaskManager -=================== - -.. autoclass:: falcon.tabular.TabularTaskManager - :members: - :private-members: - :inherited-members: - :special-members: __init__ \ No newline at end of file diff --git a/docs/docs_build/_sphinx_design_static/design-style.4045f2051d55cab465a707391d5b2007.min.css b/docs/docs_build/_sphinx_design_static/design-style.4045f2051d55cab465a707391d5b2007.min.css deleted file mode 100644 index 3225661..0000000 --- a/docs/docs_build/_sphinx_design_static/design-style.4045f2051d55cab465a707391d5b2007.min.css +++ /dev/null @@ -1 +0,0 @@ -.sd-bg-primary{background-color:var(--sd-color-primary) !important}.sd-bg-text-primary{color:var(--sd-color-primary-text) !important}button.sd-bg-primary:focus,button.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}a.sd-bg-primary:focus,a.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}.sd-bg-secondary{background-color:var(--sd-color-secondary) !important}.sd-bg-text-secondary{color:var(--sd-color-secondary-text) !important}button.sd-bg-secondary:focus,button.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}a.sd-bg-secondary:focus,a.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}.sd-bg-success{background-color:var(--sd-color-success) !important}.sd-bg-text-success{color:var(--sd-color-success-text) !important}button.sd-bg-success:focus,button.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}a.sd-bg-success:focus,a.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}.sd-bg-info{background-color:var(--sd-color-info) !important}.sd-bg-text-info{color:var(--sd-color-info-text) !important}button.sd-bg-info:focus,button.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}a.sd-bg-info:focus,a.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}.sd-bg-warning{background-color:var(--sd-color-warning) !important}.sd-bg-text-warning{color:var(--sd-color-warning-text) !important}button.sd-bg-warning:focus,button.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}a.sd-bg-warning:focus,a.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}.sd-bg-danger{background-color:var(--sd-color-danger) !important}.sd-bg-text-danger{color:var(--sd-color-danger-text) !important}button.sd-bg-danger:focus,button.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}a.sd-bg-danger:focus,a.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}.sd-bg-light{background-color:var(--sd-color-light) !important}.sd-bg-text-light{color:var(--sd-color-light-text) !important}button.sd-bg-light:focus,button.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}a.sd-bg-light:focus,a.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}.sd-bg-muted{background-color:var(--sd-color-muted) !important}.sd-bg-text-muted{color:var(--sd-color-muted-text) !important}button.sd-bg-muted:focus,button.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}a.sd-bg-muted:focus,a.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}.sd-bg-dark{background-color:var(--sd-color-dark) !important}.sd-bg-text-dark{color:var(--sd-color-dark-text) !important}button.sd-bg-dark:focus,button.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}a.sd-bg-dark:focus,a.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}.sd-bg-black{background-color:var(--sd-color-black) !important}.sd-bg-text-black{color:var(--sd-color-black-text) !important}button.sd-bg-black:focus,button.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}a.sd-bg-black:focus,a.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}.sd-bg-white{background-color:var(--sd-color-white) !important}.sd-bg-text-white{color:var(--sd-color-white-text) !important}button.sd-bg-white:focus,button.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}a.sd-bg-white:focus,a.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}.sd-text-primary,.sd-text-primary>p{color:var(--sd-color-primary) !important}a.sd-text-primary:focus,a.sd-text-primary:hover{color:var(--sd-color-primary-highlight) !important}.sd-text-secondary,.sd-text-secondary>p{color:var(--sd-color-secondary) !important}a.sd-text-secondary:focus,a.sd-text-secondary:hover{color:var(--sd-color-secondary-highlight) !important}.sd-text-success,.sd-text-success>p{color:var(--sd-color-success) !important}a.sd-text-success:focus,a.sd-text-success:hover{color:var(--sd-color-success-highlight) !important}.sd-text-info,.sd-text-info>p{color:var(--sd-color-info) !important}a.sd-text-info:focus,a.sd-text-info:hover{color:var(--sd-color-info-highlight) !important}.sd-text-warning,.sd-text-warning>p{color:var(--sd-color-warning) !important}a.sd-text-warning:focus,a.sd-text-warning:hover{color:var(--sd-color-warning-highlight) !important}.sd-text-danger,.sd-text-danger>p{color:var(--sd-color-danger) !important}a.sd-text-danger:focus,a.sd-text-danger:hover{color:var(--sd-color-danger-highlight) !important}.sd-text-light,.sd-text-light>p{color:var(--sd-color-light) !important}a.sd-text-light:focus,a.sd-text-light:hover{color:var(--sd-color-light-highlight) !important}.sd-text-muted,.sd-text-muted>p{color:var(--sd-color-muted) !important}a.sd-text-muted:focus,a.sd-text-muted:hover{color:var(--sd-color-muted-highlight) !important}.sd-text-dark,.sd-text-dark>p{color:var(--sd-color-dark) !important}a.sd-text-dark:focus,a.sd-text-dark:hover{color:var(--sd-color-dark-highlight) !important}.sd-text-black,.sd-text-black>p{color:var(--sd-color-black) !important}a.sd-text-black:focus,a.sd-text-black:hover{color:var(--sd-color-black-highlight) !important}.sd-text-white,.sd-text-white>p{color:var(--sd-color-white) !important}a.sd-text-white:focus,a.sd-text-white:hover{color:var(--sd-color-white-highlight) !important}.sd-outline-primary{border-color:var(--sd-color-primary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-primary:focus,a.sd-outline-primary:hover{border-color:var(--sd-color-primary-highlight) !important}.sd-outline-secondary{border-color:var(--sd-color-secondary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-secondary:focus,a.sd-outline-secondary:hover{border-color:var(--sd-color-secondary-highlight) !important}.sd-outline-success{border-color:var(--sd-color-success) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-success:focus,a.sd-outline-success:hover{border-color:var(--sd-color-success-highlight) !important}.sd-outline-info{border-color:var(--sd-color-info) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-info:focus,a.sd-outline-info:hover{border-color:var(--sd-color-info-highlight) !important}.sd-outline-warning{border-color:var(--sd-color-warning) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-warning:focus,a.sd-outline-warning:hover{border-color:var(--sd-color-warning-highlight) !important}.sd-outline-danger{border-color:var(--sd-color-danger) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-danger:focus,a.sd-outline-danger:hover{border-color:var(--sd-color-danger-highlight) !important}.sd-outline-light{border-color:var(--sd-color-light) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-light:focus,a.sd-outline-light:hover{border-color:var(--sd-color-light-highlight) !important}.sd-outline-muted{border-color:var(--sd-color-muted) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-muted:focus,a.sd-outline-muted:hover{border-color:var(--sd-color-muted-highlight) !important}.sd-outline-dark{border-color:var(--sd-color-dark) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-dark:focus,a.sd-outline-dark:hover{border-color:var(--sd-color-dark-highlight) !important}.sd-outline-black{border-color:var(--sd-color-black) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-black:focus,a.sd-outline-black:hover{border-color:var(--sd-color-black-highlight) !important}.sd-outline-white{border-color:var(--sd-color-white) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-white:focus,a.sd-outline-white:hover{border-color:var(--sd-color-white-highlight) !important}.sd-bg-transparent{background-color:transparent !important}.sd-outline-transparent{border-color:transparent !important}.sd-text-transparent{color:transparent !important}.sd-p-0{padding:0 !important}.sd-pt-0,.sd-py-0{padding-top:0 !important}.sd-pr-0,.sd-px-0{padding-right:0 !important}.sd-pb-0,.sd-py-0{padding-bottom:0 !important}.sd-pl-0,.sd-px-0{padding-left:0 !important}.sd-p-1{padding:.25rem !important}.sd-pt-1,.sd-py-1{padding-top:.25rem !important}.sd-pr-1,.sd-px-1{padding-right:.25rem !important}.sd-pb-1,.sd-py-1{padding-bottom:.25rem !important}.sd-pl-1,.sd-px-1{padding-left:.25rem !important}.sd-p-2{padding:.5rem !important}.sd-pt-2,.sd-py-2{padding-top:.5rem !important}.sd-pr-2,.sd-px-2{padding-right:.5rem !important}.sd-pb-2,.sd-py-2{padding-bottom:.5rem !important}.sd-pl-2,.sd-px-2{padding-left:.5rem !important}.sd-p-3{padding:1rem !important}.sd-pt-3,.sd-py-3{padding-top:1rem !important}.sd-pr-3,.sd-px-3{padding-right:1rem !important}.sd-pb-3,.sd-py-3{padding-bottom:1rem !important}.sd-pl-3,.sd-px-3{padding-left:1rem !important}.sd-p-4{padding:1.5rem !important}.sd-pt-4,.sd-py-4{padding-top:1.5rem !important}.sd-pr-4,.sd-px-4{padding-right:1.5rem !important}.sd-pb-4,.sd-py-4{padding-bottom:1.5rem !important}.sd-pl-4,.sd-px-4{padding-left:1.5rem !important}.sd-p-5{padding:3rem !important}.sd-pt-5,.sd-py-5{padding-top:3rem !important}.sd-pr-5,.sd-px-5{padding-right:3rem !important}.sd-pb-5,.sd-py-5{padding-bottom:3rem !important}.sd-pl-5,.sd-px-5{padding-left:3rem !important}.sd-m-auto{margin:auto !important}.sd-mt-auto,.sd-my-auto{margin-top:auto !important}.sd-mr-auto,.sd-mx-auto{margin-right:auto !important}.sd-mb-auto,.sd-my-auto{margin-bottom:auto !important}.sd-ml-auto,.sd-mx-auto{margin-left:auto !important}.sd-m-0{margin:0 !important}.sd-mt-0,.sd-my-0{margin-top:0 !important}.sd-mr-0,.sd-mx-0{margin-right:0 !important}.sd-mb-0,.sd-my-0{margin-bottom:0 !important}.sd-ml-0,.sd-mx-0{margin-left:0 !important}.sd-m-1{margin:.25rem !important}.sd-mt-1,.sd-my-1{margin-top:.25rem !important}.sd-mr-1,.sd-mx-1{margin-right:.25rem !important}.sd-mb-1,.sd-my-1{margin-bottom:.25rem !important}.sd-ml-1,.sd-mx-1{margin-left:.25rem !important}.sd-m-2{margin:.5rem !important}.sd-mt-2,.sd-my-2{margin-top:.5rem !important}.sd-mr-2,.sd-mx-2{margin-right:.5rem !important}.sd-mb-2,.sd-my-2{margin-bottom:.5rem !important}.sd-ml-2,.sd-mx-2{margin-left:.5rem !important}.sd-m-3{margin:1rem !important}.sd-mt-3,.sd-my-3{margin-top:1rem !important}.sd-mr-3,.sd-mx-3{margin-right:1rem !important}.sd-mb-3,.sd-my-3{margin-bottom:1rem !important}.sd-ml-3,.sd-mx-3{margin-left:1rem !important}.sd-m-4{margin:1.5rem !important}.sd-mt-4,.sd-my-4{margin-top:1.5rem !important}.sd-mr-4,.sd-mx-4{margin-right:1.5rem !important}.sd-mb-4,.sd-my-4{margin-bottom:1.5rem !important}.sd-ml-4,.sd-mx-4{margin-left:1.5rem !important}.sd-m-5{margin:3rem !important}.sd-mt-5,.sd-my-5{margin-top:3rem !important}.sd-mr-5,.sd-mx-5{margin-right:3rem !important}.sd-mb-5,.sd-my-5{margin-bottom:3rem !important}.sd-ml-5,.sd-mx-5{margin-left:3rem !important}.sd-w-25{width:25% !important}.sd-w-50{width:50% !important}.sd-w-75{width:75% !important}.sd-w-100{width:100% !important}.sd-w-auto{width:auto !important}.sd-h-25{height:25% !important}.sd-h-50{height:50% !important}.sd-h-75{height:75% !important}.sd-h-100{height:100% !important}.sd-h-auto{height:auto !important}.sd-d-none{display:none !important}.sd-d-inline{display:inline !important}.sd-d-inline-block{display:inline-block !important}.sd-d-block{display:block !important}.sd-d-grid{display:grid !important}.sd-d-flex-row{display:-ms-flexbox !important;display:flex !important;flex-direction:row !important}.sd-d-flex-column{display:-ms-flexbox !important;display:flex !important;flex-direction:column !important}.sd-d-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}@media(min-width: 576px){.sd-d-sm-none{display:none !important}.sd-d-sm-inline{display:inline !important}.sd-d-sm-inline-block{display:inline-block !important}.sd-d-sm-block{display:block !important}.sd-d-sm-grid{display:grid !important}.sd-d-sm-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-sm-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 768px){.sd-d-md-none{display:none !important}.sd-d-md-inline{display:inline !important}.sd-d-md-inline-block{display:inline-block !important}.sd-d-md-block{display:block !important}.sd-d-md-grid{display:grid !important}.sd-d-md-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-md-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 992px){.sd-d-lg-none{display:none !important}.sd-d-lg-inline{display:inline !important}.sd-d-lg-inline-block{display:inline-block !important}.sd-d-lg-block{display:block !important}.sd-d-lg-grid{display:grid !important}.sd-d-lg-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-lg-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 1200px){.sd-d-xl-none{display:none !important}.sd-d-xl-inline{display:inline !important}.sd-d-xl-inline-block{display:inline-block !important}.sd-d-xl-block{display:block !important}.sd-d-xl-grid{display:grid !important}.sd-d-xl-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-xl-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}.sd-align-major-start{justify-content:flex-start !important}.sd-align-major-end{justify-content:flex-end !important}.sd-align-major-center{justify-content:center !important}.sd-align-major-justify{justify-content:space-between !important}.sd-align-major-spaced{justify-content:space-evenly !important}.sd-align-minor-start{align-items:flex-start !important}.sd-align-minor-end{align-items:flex-end !important}.sd-align-minor-center{align-items:center !important}.sd-align-minor-stretch{align-items:stretch !important}.sd-text-justify{text-align:justify !important}.sd-text-left{text-align:left !important}.sd-text-right{text-align:right !important}.sd-text-center{text-align:center !important}.sd-font-weight-light{font-weight:300 !important}.sd-font-weight-lighter{font-weight:lighter !important}.sd-font-weight-normal{font-weight:400 !important}.sd-font-weight-bold{font-weight:700 !important}.sd-font-weight-bolder{font-weight:bolder !important}.sd-font-italic{font-style:italic !important}.sd-text-decoration-none{text-decoration:none !important}.sd-text-lowercase{text-transform:lowercase !important}.sd-text-uppercase{text-transform:uppercase !important}.sd-text-capitalize{text-transform:capitalize !important}.sd-text-wrap{white-space:normal !important}.sd-text-nowrap{white-space:nowrap !important}.sd-text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sd-fs-1,.sd-fs-1>p{font-size:calc(1.375rem + 1.5vw) !important;line-height:unset !important}.sd-fs-2,.sd-fs-2>p{font-size:calc(1.325rem + 0.9vw) !important;line-height:unset !important}.sd-fs-3,.sd-fs-3>p{font-size:calc(1.3rem + 0.6vw) !important;line-height:unset !important}.sd-fs-4,.sd-fs-4>p{font-size:calc(1.275rem + 0.3vw) !important;line-height:unset !important}.sd-fs-5,.sd-fs-5>p{font-size:1.25rem !important;line-height:unset !important}.sd-fs-6,.sd-fs-6>p{font-size:1rem !important;line-height:unset !important}.sd-border-0{border:0 solid !important}.sd-border-top-0{border-top:0 solid !important}.sd-border-bottom-0{border-bottom:0 solid !important}.sd-border-right-0{border-right:0 solid !important}.sd-border-left-0{border-left:0 solid !important}.sd-border-1{border:1px solid !important}.sd-border-top-1{border-top:1px solid !important}.sd-border-bottom-1{border-bottom:1px solid !important}.sd-border-right-1{border-right:1px solid !important}.sd-border-left-1{border-left:1px solid !important}.sd-border-2{border:2px solid !important}.sd-border-top-2{border-top:2px solid !important}.sd-border-bottom-2{border-bottom:2px solid !important}.sd-border-right-2{border-right:2px solid !important}.sd-border-left-2{border-left:2px solid !important}.sd-border-3{border:3px solid !important}.sd-border-top-3{border-top:3px solid !important}.sd-border-bottom-3{border-bottom:3px solid !important}.sd-border-right-3{border-right:3px solid !important}.sd-border-left-3{border-left:3px solid !important}.sd-border-4{border:4px solid !important}.sd-border-top-4{border-top:4px solid !important}.sd-border-bottom-4{border-bottom:4px solid !important}.sd-border-right-4{border-right:4px solid !important}.sd-border-left-4{border-left:4px solid !important}.sd-border-5{border:5px solid !important}.sd-border-top-5{border-top:5px solid !important}.sd-border-bottom-5{border-bottom:5px solid !important}.sd-border-right-5{border-right:5px solid !important}.sd-border-left-5{border-left:5px solid !important}.sd-rounded-0{border-radius:0 !important}.sd-rounded-1{border-radius:.2rem !important}.sd-rounded-2{border-radius:.3rem !important}.sd-rounded-3{border-radius:.5rem !important}.sd-rounded-pill{border-radius:50rem !important}.sd-rounded-circle{border-radius:50% !important}.shadow-none{box-shadow:none !important}.sd-shadow-sm{box-shadow:0 .125rem .25rem var(--sd-color-shadow) !important}.sd-shadow-md{box-shadow:0 .5rem 1rem var(--sd-color-shadow) !important}.sd-shadow-lg{box-shadow:0 1rem 3rem var(--sd-color-shadow) !important}@keyframes sd-slide-from-left{0%{transform:translateX(-100%)}100%{transform:translateX(0)}}@keyframes sd-slide-from-right{0%{transform:translateX(200%)}100%{transform:translateX(0)}}@keyframes sd-grow100{0%{transform:scale(0);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50{0%{transform:scale(0.5);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50-rot20{0%{transform:scale(0.5) rotateZ(-20deg);opacity:.5}75%{transform:scale(1) rotateZ(5deg);opacity:1}95%{transform:scale(1) rotateZ(-1deg);opacity:1}100%{transform:scale(1) rotateZ(0);opacity:1}}.sd-animate-slide-from-left{animation:1s ease-out 0s 1 normal none running sd-slide-from-left}.sd-animate-slide-from-right{animation:1s ease-out 0s 1 normal none running sd-slide-from-right}.sd-animate-grow100{animation:1s ease-out 0s 1 normal none running sd-grow100}.sd-animate-grow50{animation:1s ease-out 0s 1 normal none running sd-grow50}.sd-animate-grow50-rot20{animation:1s ease-out 0s 1 normal none running sd-grow50-rot20}.sd-badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.sd-badge:empty{display:none}a.sd-badge{text-decoration:none}.sd-btn .sd-badge{position:relative;top:-1px}.sd-btn{background-color:transparent;border:1px solid transparent;border-radius:.25rem;cursor:pointer;display:inline-block;font-weight:400;font-size:1rem;line-height:1.5;padding:.375rem .75rem;text-align:center;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;vertical-align:middle;user-select:none;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none}.sd-btn:hover{text-decoration:none}@media(prefers-reduced-motion: reduce){.sd-btn{transition:none}}.sd-btn-primary,.sd-btn-outline-primary:hover,.sd-btn-outline-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-primary:hover,.sd-btn-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary-highlight) !important;border-color:var(--sd-color-primary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-primary{color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary,.sd-btn-outline-secondary:hover,.sd-btn-outline-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary:hover,.sd-btn-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary-highlight) !important;border-color:var(--sd-color-secondary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-secondary{color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success,.sd-btn-outline-success:hover,.sd-btn-outline-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success:hover,.sd-btn-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success-highlight) !important;border-color:var(--sd-color-success-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-success{color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info,.sd-btn-outline-info:hover,.sd-btn-outline-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info:hover,.sd-btn-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info-highlight) !important;border-color:var(--sd-color-info-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-info{color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning,.sd-btn-outline-warning:hover,.sd-btn-outline-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning:hover,.sd-btn-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning-highlight) !important;border-color:var(--sd-color-warning-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-warning{color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger,.sd-btn-outline-danger:hover,.sd-btn-outline-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger:hover,.sd-btn-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger-highlight) !important;border-color:var(--sd-color-danger-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-danger{color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light,.sd-btn-outline-light:hover,.sd-btn-outline-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light:hover,.sd-btn-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light-highlight) !important;border-color:var(--sd-color-light-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-light{color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted,.sd-btn-outline-muted:hover,.sd-btn-outline-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted:hover,.sd-btn-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted-highlight) !important;border-color:var(--sd-color-muted-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-muted{color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark,.sd-btn-outline-dark:hover,.sd-btn-outline-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark:hover,.sd-btn-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark-highlight) !important;border-color:var(--sd-color-dark-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-dark{color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black,.sd-btn-outline-black:hover,.sd-btn-outline-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black:hover,.sd-btn-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black-highlight) !important;border-color:var(--sd-color-black-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-black{color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white,.sd-btn-outline-white:hover,.sd-btn-outline-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white:hover,.sd-btn-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white-highlight) !important;border-color:var(--sd-color-white-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-white{color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.sd-hide-link-text{font-size:0}.sd-octicon,.sd-material-icon{display:inline-block;fill:currentColor;vertical-align:middle}.sd-avatar-xs{border-radius:50%;object-fit:cover;object-position:center;width:1rem;height:1rem}.sd-avatar-sm{border-radius:50%;object-fit:cover;object-position:center;width:3rem;height:3rem}.sd-avatar-md{border-radius:50%;object-fit:cover;object-position:center;width:5rem;height:5rem}.sd-avatar-lg{border-radius:50%;object-fit:cover;object-position:center;width:7rem;height:7rem}.sd-avatar-xl{border-radius:50%;object-fit:cover;object-position:center;width:10rem;height:10rem}.sd-avatar-inherit{border-radius:50%;object-fit:cover;object-position:center;width:inherit;height:inherit}.sd-avatar-initial{border-radius:50%;object-fit:cover;object-position:center;width:initial;height:initial}.sd-card{background-clip:border-box;background-color:var(--sd-color-card-background);border:1px solid var(--sd-color-card-border);border-radius:.25rem;color:var(--sd-color-card-text);display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;position:relative;word-wrap:break-word}.sd-card>hr{margin-left:0;margin-right:0}.sd-card-hover:hover{border-color:var(--sd-color-card-border-hover);transform:scale(1.01)}.sd-card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem 1rem}.sd-card-title{margin-bottom:.5rem}.sd-card-subtitle{margin-top:-0.25rem;margin-bottom:0}.sd-card-text:last-child{margin-bottom:0}.sd-card-link:hover{text-decoration:none}.sd-card-link+.card-link{margin-left:1rem}.sd-card-header{padding:.5rem 1rem;margin-bottom:0;background-color:var(--sd-color-card-header);border-bottom:1px solid var(--sd-color-card-border)}.sd-card-header:first-child{border-radius:calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0}.sd-card-footer{padding:.5rem 1rem;background-color:var(--sd-color-card-footer);border-top:1px solid var(--sd-color-card-border)}.sd-card-footer:last-child{border-radius:0 0 calc(0.25rem - 1px) calc(0.25rem - 1px)}.sd-card-header-tabs{margin-right:-0.5rem;margin-bottom:-0.5rem;margin-left:-0.5rem;border-bottom:0}.sd-card-header-pills{margin-right:-0.5rem;margin-left:-0.5rem}.sd-card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom,.sd-card-img-top{width:100%}.sd-card-img,.sd-card-img-top{border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom{border-bottom-left-radius:calc(0.25rem - 1px);border-bottom-right-radius:calc(0.25rem - 1px)}.sd-cards-carousel{width:100%;display:flex;flex-wrap:nowrap;-ms-flex-direction:row;flex-direction:row;overflow-x:hidden;scroll-snap-type:x mandatory}.sd-cards-carousel.sd-show-scrollbar{overflow-x:auto}.sd-cards-carousel:hover,.sd-cards-carousel:focus{overflow-x:auto}.sd-cards-carousel>.sd-card{flex-shrink:0;scroll-snap-align:start}.sd-cards-carousel>.sd-card:not(:last-child){margin-right:3px}.sd-card-cols-1>.sd-card{width:90%}.sd-card-cols-2>.sd-card{width:45%}.sd-card-cols-3>.sd-card{width:30%}.sd-card-cols-4>.sd-card{width:22.5%}.sd-card-cols-5>.sd-card{width:18%}.sd-card-cols-6>.sd-card{width:15%}.sd-card-cols-7>.sd-card{width:12.8571428571%}.sd-card-cols-8>.sd-card{width:11.25%}.sd-card-cols-9>.sd-card{width:10%}.sd-card-cols-10>.sd-card{width:9%}.sd-card-cols-11>.sd-card{width:8.1818181818%}.sd-card-cols-12>.sd-card{width:7.5%}.sd-container,.sd-container-fluid,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container-xl{margin-left:auto;margin-right:auto;padding-left:var(--sd-gutter-x, 0.75rem);padding-right:var(--sd-gutter-x, 0.75rem);width:100%}@media(min-width: 576px){.sd-container-sm,.sd-container{max-width:540px}}@media(min-width: 768px){.sd-container-md,.sd-container-sm,.sd-container{max-width:720px}}@media(min-width: 992px){.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:960px}}@media(min-width: 1200px){.sd-container-xl,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:1140px}}.sd-row{--sd-gutter-x: 1.5rem;--sd-gutter-y: 0;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-top:calc(var(--sd-gutter-y) * -1);margin-right:calc(var(--sd-gutter-x) * -0.5);margin-left:calc(var(--sd-gutter-x) * -0.5)}.sd-row>*{box-sizing:border-box;flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--sd-gutter-x) * 0.5);padding-left:calc(var(--sd-gutter-x) * 0.5);margin-top:var(--sd-gutter-y)}.sd-col{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-auto>*{flex:0 0 auto;width:auto}.sd-row-cols-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}@media(min-width: 576px){.sd-col-sm{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-sm-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-sm-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-sm-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-sm-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-sm-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-sm-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-sm-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-sm-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-sm-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-sm-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-sm-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-sm-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-sm-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 768px){.sd-col-md{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-md-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-md-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-md-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-md-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-md-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-md-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-md-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-md-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-md-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-md-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-md-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-md-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-md-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 992px){.sd-col-lg{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-lg-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-lg-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-lg-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-lg-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-lg-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-lg-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-lg-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-lg-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-lg-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-lg-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-lg-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-lg-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-lg-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 1200px){.sd-col-xl{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-xl-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-xl-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-xl-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-xl-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-xl-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-xl-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-xl-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-xl-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-xl-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-xl-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-xl-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-xl-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-xl-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}.sd-col-auto{flex:0 0 auto;-ms-flex:0 0 auto;width:auto}.sd-col-1{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}.sd-col-2{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-col-3{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-col-4{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-col-5{flex:0 0 auto;-ms-flex:0 0 auto;width:41.6666666667%}.sd-col-6{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-col-7{flex:0 0 auto;-ms-flex:0 0 auto;width:58.3333333333%}.sd-col-8{flex:0 0 auto;-ms-flex:0 0 auto;width:66.6666666667%}.sd-col-9{flex:0 0 auto;-ms-flex:0 0 auto;width:75%}.sd-col-10{flex:0 0 auto;-ms-flex:0 0 auto;width:83.3333333333%}.sd-col-11{flex:0 0 auto;-ms-flex:0 0 auto;width:91.6666666667%}.sd-col-12{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-g-0,.sd-gy-0{--sd-gutter-y: 0}.sd-g-0,.sd-gx-0{--sd-gutter-x: 0}.sd-g-1,.sd-gy-1{--sd-gutter-y: 0.25rem}.sd-g-1,.sd-gx-1{--sd-gutter-x: 0.25rem}.sd-g-2,.sd-gy-2{--sd-gutter-y: 0.5rem}.sd-g-2,.sd-gx-2{--sd-gutter-x: 0.5rem}.sd-g-3,.sd-gy-3{--sd-gutter-y: 1rem}.sd-g-3,.sd-gx-3{--sd-gutter-x: 1rem}.sd-g-4,.sd-gy-4{--sd-gutter-y: 1.5rem}.sd-g-4,.sd-gx-4{--sd-gutter-x: 1.5rem}.sd-g-5,.sd-gy-5{--sd-gutter-y: 3rem}.sd-g-5,.sd-gx-5{--sd-gutter-x: 3rem}@media(min-width: 576px){.sd-col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-sm-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-sm-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-sm-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-sm-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-sm-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-sm-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-sm-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-sm-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-sm-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-sm-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-sm-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-sm-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-sm-0,.sd-gy-sm-0{--sd-gutter-y: 0}.sd-g-sm-0,.sd-gx-sm-0{--sd-gutter-x: 0}.sd-g-sm-1,.sd-gy-sm-1{--sd-gutter-y: 0.25rem}.sd-g-sm-1,.sd-gx-sm-1{--sd-gutter-x: 0.25rem}.sd-g-sm-2,.sd-gy-sm-2{--sd-gutter-y: 0.5rem}.sd-g-sm-2,.sd-gx-sm-2{--sd-gutter-x: 0.5rem}.sd-g-sm-3,.sd-gy-sm-3{--sd-gutter-y: 1rem}.sd-g-sm-3,.sd-gx-sm-3{--sd-gutter-x: 1rem}.sd-g-sm-4,.sd-gy-sm-4{--sd-gutter-y: 1.5rem}.sd-g-sm-4,.sd-gx-sm-4{--sd-gutter-x: 1.5rem}.sd-g-sm-5,.sd-gy-sm-5{--sd-gutter-y: 3rem}.sd-g-sm-5,.sd-gx-sm-5{--sd-gutter-x: 3rem}}@media(min-width: 768px){.sd-col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-md-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-md-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-md-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-md-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-md-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-md-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-md-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-md-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-md-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-md-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-md-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-md-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-md-0,.sd-gy-md-0{--sd-gutter-y: 0}.sd-g-md-0,.sd-gx-md-0{--sd-gutter-x: 0}.sd-g-md-1,.sd-gy-md-1{--sd-gutter-y: 0.25rem}.sd-g-md-1,.sd-gx-md-1{--sd-gutter-x: 0.25rem}.sd-g-md-2,.sd-gy-md-2{--sd-gutter-y: 0.5rem}.sd-g-md-2,.sd-gx-md-2{--sd-gutter-x: 0.5rem}.sd-g-md-3,.sd-gy-md-3{--sd-gutter-y: 1rem}.sd-g-md-3,.sd-gx-md-3{--sd-gutter-x: 1rem}.sd-g-md-4,.sd-gy-md-4{--sd-gutter-y: 1.5rem}.sd-g-md-4,.sd-gx-md-4{--sd-gutter-x: 1.5rem}.sd-g-md-5,.sd-gy-md-5{--sd-gutter-y: 3rem}.sd-g-md-5,.sd-gx-md-5{--sd-gutter-x: 3rem}}@media(min-width: 992px){.sd-col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-lg-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-lg-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-lg-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-lg-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-lg-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-lg-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-lg-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-lg-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-lg-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-lg-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-lg-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-lg-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-lg-0,.sd-gy-lg-0{--sd-gutter-y: 0}.sd-g-lg-0,.sd-gx-lg-0{--sd-gutter-x: 0}.sd-g-lg-1,.sd-gy-lg-1{--sd-gutter-y: 0.25rem}.sd-g-lg-1,.sd-gx-lg-1{--sd-gutter-x: 0.25rem}.sd-g-lg-2,.sd-gy-lg-2{--sd-gutter-y: 0.5rem}.sd-g-lg-2,.sd-gx-lg-2{--sd-gutter-x: 0.5rem}.sd-g-lg-3,.sd-gy-lg-3{--sd-gutter-y: 1rem}.sd-g-lg-3,.sd-gx-lg-3{--sd-gutter-x: 1rem}.sd-g-lg-4,.sd-gy-lg-4{--sd-gutter-y: 1.5rem}.sd-g-lg-4,.sd-gx-lg-4{--sd-gutter-x: 1.5rem}.sd-g-lg-5,.sd-gy-lg-5{--sd-gutter-y: 3rem}.sd-g-lg-5,.sd-gx-lg-5{--sd-gutter-x: 3rem}}@media(min-width: 1200px){.sd-col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-xl-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-xl-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-xl-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-xl-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-xl-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-xl-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-xl-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-xl-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-xl-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-xl-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-xl-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-xl-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-xl-0,.sd-gy-xl-0{--sd-gutter-y: 0}.sd-g-xl-0,.sd-gx-xl-0{--sd-gutter-x: 0}.sd-g-xl-1,.sd-gy-xl-1{--sd-gutter-y: 0.25rem}.sd-g-xl-1,.sd-gx-xl-1{--sd-gutter-x: 0.25rem}.sd-g-xl-2,.sd-gy-xl-2{--sd-gutter-y: 0.5rem}.sd-g-xl-2,.sd-gx-xl-2{--sd-gutter-x: 0.5rem}.sd-g-xl-3,.sd-gy-xl-3{--sd-gutter-y: 1rem}.sd-g-xl-3,.sd-gx-xl-3{--sd-gutter-x: 1rem}.sd-g-xl-4,.sd-gy-xl-4{--sd-gutter-y: 1.5rem}.sd-g-xl-4,.sd-gx-xl-4{--sd-gutter-x: 1.5rem}.sd-g-xl-5,.sd-gy-xl-5{--sd-gutter-y: 3rem}.sd-g-xl-5,.sd-gx-xl-5{--sd-gutter-x: 3rem}}.sd-flex-row-reverse{flex-direction:row-reverse !important}details.sd-dropdown{position:relative}details.sd-dropdown .sd-summary-title{font-weight:700;padding-right:3em !important;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;user-select:none}details.sd-dropdown:hover{cursor:pointer}details.sd-dropdown .sd-summary-content{cursor:default}details.sd-dropdown summary{list-style:none;padding:1em}details.sd-dropdown summary .sd-octicon.no-title{vertical-align:middle}details.sd-dropdown[open] summary .sd-octicon.no-title{visibility:hidden}details.sd-dropdown summary::-webkit-details-marker{display:none}details.sd-dropdown summary:focus{outline:none}details.sd-dropdown .sd-summary-icon{margin-right:.5em}details.sd-dropdown .sd-summary-icon svg{opacity:.8}details.sd-dropdown summary:hover .sd-summary-up svg,details.sd-dropdown summary:hover .sd-summary-down svg{opacity:1;transform:scale(1.1)}details.sd-dropdown .sd-summary-up svg,details.sd-dropdown .sd-summary-down svg{display:block;opacity:.6}details.sd-dropdown .sd-summary-up,details.sd-dropdown .sd-summary-down{pointer-events:none;position:absolute;right:1em;top:1em}details.sd-dropdown[open]>.sd-summary-title .sd-summary-down{visibility:hidden}details.sd-dropdown:not([open])>.sd-summary-title .sd-summary-up{visibility:hidden}details.sd-dropdown:not([open]).sd-card{border:none}details.sd-dropdown:not([open])>.sd-card-header{border:1px solid var(--sd-color-card-border);border-radius:.25rem}details.sd-dropdown.sd-fade-in[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out;animation:sd-fade-in .5s ease-in-out}details.sd-dropdown.sd-fade-in-slide-down[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out}.sd-col>.sd-dropdown{width:100%}.sd-summary-content>.sd-tab-set:first-child{margin-top:0}@keyframes sd-fade-in{0%{opacity:0}100%{opacity:1}}@keyframes sd-slide-down{0%{transform:translate(0, -10px)}100%{transform:translate(0, 0)}}.sd-tab-set{border-radius:.125rem;display:flex;flex-wrap:wrap;margin:1em 0;position:relative}.sd-tab-set>input{opacity:0;position:absolute}.sd-tab-set>input:checked+label{border-color:var(--sd-color-tabs-underline-active);color:var(--sd-color-tabs-label-active)}.sd-tab-set>input:checked+label+.sd-tab-content{display:block}.sd-tab-set>input:not(:checked)+label:hover{color:var(--sd-color-tabs-label-hover);border-color:var(--sd-color-tabs-underline-hover)}.sd-tab-set>input:focus+label{outline-style:auto}.sd-tab-set>input:not(.focus-visible)+label{outline:none;-webkit-tap-highlight-color:transparent}.sd-tab-set>label{border-bottom:.125rem solid transparent;margin-bottom:0;color:var(--sd-color-tabs-label-inactive);border-color:var(--sd-color-tabs-underline-inactive);cursor:pointer;font-size:var(--sd-fontsize-tabs-label);font-weight:700;padding:1em 1.25em .5em;transition:color 250ms;width:auto;z-index:1}html .sd-tab-set>label:hover{color:var(--sd-color-tabs-label-active)}.sd-col>.sd-tab-set{width:100%}.sd-tab-content{box-shadow:0 -0.0625rem var(--sd-color-tabs-overline),0 .0625rem var(--sd-color-tabs-underline);display:none;order:99;padding-bottom:.75rem;padding-top:.75rem;width:100%}.sd-tab-content>:first-child{margin-top:0 !important}.sd-tab-content>:last-child{margin-bottom:0 !important}.sd-tab-content>.sd-tab-set{margin:0}.sd-sphinx-override,.sd-sphinx-override *{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.sd-sphinx-override p{margin-top:0}:root{--sd-color-primary: #007bff;--sd-color-secondary: #6c757d;--sd-color-success: #28a745;--sd-color-info: #17a2b8;--sd-color-warning: #f0b37e;--sd-color-danger: #dc3545;--sd-color-light: #f8f9fa;--sd-color-muted: #6c757d;--sd-color-dark: #212529;--sd-color-black: black;--sd-color-white: white;--sd-color-primary-highlight: #0069d9;--sd-color-secondary-highlight: #5c636a;--sd-color-success-highlight: #228e3b;--sd-color-info-highlight: #148a9c;--sd-color-warning-highlight: #cc986b;--sd-color-danger-highlight: #bb2d3b;--sd-color-light-highlight: #d3d4d5;--sd-color-muted-highlight: #5c636a;--sd-color-dark-highlight: #1c1f23;--sd-color-black-highlight: black;--sd-color-white-highlight: #d9d9d9;--sd-color-primary-text: #fff;--sd-color-secondary-text: #fff;--sd-color-success-text: #fff;--sd-color-info-text: #fff;--sd-color-warning-text: #212529;--sd-color-danger-text: #fff;--sd-color-light-text: #212529;--sd-color-muted-text: #fff;--sd-color-dark-text: #fff;--sd-color-black-text: #fff;--sd-color-white-text: #212529;--sd-color-shadow: rgba(0, 0, 0, 0.15);--sd-color-card-border: rgba(0, 0, 0, 0.125);--sd-color-card-border-hover: hsla(231, 99%, 66%, 1);--sd-color-card-background: transparent;--sd-color-card-text: inherit;--sd-color-card-header: transparent;--sd-color-card-footer: transparent;--sd-color-tabs-label-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-hover: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-inactive: hsl(0, 0%, 66%);--sd-color-tabs-underline-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-underline-hover: rgba(178, 206, 245, 0.62);--sd-color-tabs-underline-inactive: transparent;--sd-color-tabs-overline: rgb(222, 222, 222);--sd-color-tabs-underline: rgb(222, 222, 222);--sd-fontsize-tabs-label: 1rem} diff --git a/docs/docs_build/_sphinx_design_static/design-tabs.js b/docs/docs_build/_sphinx_design_static/design-tabs.js deleted file mode 100644 index 36b38cf..0000000 --- a/docs/docs_build/_sphinx_design_static/design-tabs.js +++ /dev/null @@ -1,27 +0,0 @@ -var sd_labels_by_text = {}; - -function ready() { - const li = document.getElementsByClassName("sd-tab-label"); - for (const label of li) { - syncId = label.getAttribute("data-sync-id"); - if (syncId) { - label.onclick = onLabelClick; - if (!sd_labels_by_text[syncId]) { - sd_labels_by_text[syncId] = []; - } - sd_labels_by_text[syncId].push(label); - } - } -} - -function onLabelClick() { - // Activate other inputs with the same sync id. - syncId = this.getAttribute("data-sync-id"); - for (label of sd_labels_by_text[syncId]) { - if (label === this) continue; - label.previousElementSibling.checked = true; - } - window.localStorage.setItem("sphinx-design-last-tab", syncId); -} - -document.addEventListener("DOMContentLoaded", ready, false); diff --git a/docs/docs_build/_static/_sphinx_javascript_frameworks_compat.js b/docs/docs_build/_static/_sphinx_javascript_frameworks_compat.js deleted file mode 100644 index 8549469..0000000 --- a/docs/docs_build/_static/_sphinx_javascript_frameworks_compat.js +++ /dev/null @@ -1,134 +0,0 @@ -/* - * _sphinx_javascript_frameworks_compat.js - * ~~~~~~~~~~ - * - * Compatability shim for jQuery and underscores.js. - * - * WILL BE REMOVED IN Sphinx 6.0 - * xref RemovedInSphinx60Warning - * - */ - -/** - * select a different prefix for underscore - */ -$u = _.noConflict(); - - -/** - * small helper function to urldecode strings - * - * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL - */ -jQuery.urldecode = function(x) { - if (!x) { - return x - } - return decodeURIComponent(x.replace(/\+/g, ' ')); -}; - -/** - * small helper function to urlencode strings - */ -jQuery.urlencode = encodeURIComponent; - -/** - * This function returns the parsed url parameters of the - * current request. Multiple values per key are supported, - * it will always return arrays of strings for the value parts. - */ -jQuery.getQueryParameters = function(s) { - if (typeof s === 'undefined') - s = document.location.search; - var parts = s.substr(s.indexOf('?') + 1).split('&'); - var result = {}; - for (var i = 0; i < parts.length; i++) { - var tmp = parts[i].split('=', 2); - var key = jQuery.urldecode(tmp[0]); - var value = jQuery.urldecode(tmp[1]); - if (key in result) - result[key].push(value); - else - result[key] = [value]; - } - return result; -}; - -/** - * highlight a given string on a jquery object by wrapping it in - * span elements with the given class name. - */ -jQuery.fn.highlightText = function(text, className) { - function highlight(node, addItems) { - if (node.nodeType === 3) { - var val = node.nodeValue; - var pos = val.toLowerCase().indexOf(text); - if (pos >= 0 && - !jQuery(node.parentNode).hasClass(className) && - !jQuery(node.parentNode).hasClass("nohighlight")) { - var span; - var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); - if (isInSVG) { - span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); - } else { - span = document.createElement("span"); - span.className = className; - } - span.appendChild(document.createTextNode(val.substr(pos, text.length))); - node.parentNode.insertBefore(span, node.parentNode.insertBefore( - document.createTextNode(val.substr(pos + text.length)), - node.nextSibling)); - node.nodeValue = val.substr(0, pos); - if (isInSVG) { - var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); - var bbox = node.parentElement.getBBox(); - rect.x.baseVal.value = bbox.x; - rect.y.baseVal.value = bbox.y; - rect.width.baseVal.value = bbox.width; - rect.height.baseVal.value = bbox.height; - rect.setAttribute('class', className); - addItems.push({ - "parent": node.parentNode, - "target": rect}); - } - } - } - else if (!jQuery(node).is("button, select, textarea")) { - jQuery.each(node.childNodes, function() { - highlight(this, addItems); - }); - } - } - var addItems = []; - var result = this.each(function() { - highlight(this, addItems); - }); - for (var i = 0; i < addItems.length; ++i) { - jQuery(addItems[i].parent).before(addItems[i].target); - } - return result; -}; - -/* - * backward compatibility for jQuery.browser - * This will be supported until firefox bug is fixed. - */ -if (!jQuery.browser) { - jQuery.uaMatch = function(ua) { - ua = ua.toLowerCase(); - - var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || - /(webkit)[ \/]([\w.]+)/.exec(ua) || - /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || - /(msie) ([\w.]+)/.exec(ua) || - ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || - []; - - return { - browser: match[ 1 ] || "", - version: match[ 2 ] || "0" - }; - }; - jQuery.browser = {}; - jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; -} diff --git a/docs/docs_build/_static/basic.css b/docs/docs_build/_static/basic.css deleted file mode 100644 index eeb0519..0000000 --- a/docs/docs_build/_static/basic.css +++ /dev/null @@ -1,899 +0,0 @@ -/* - * basic.css - * ~~~~~~~~~ - * - * Sphinx stylesheet -- basic theme. - * - * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * - */ - -/* -- main layout ----------------------------------------------------------- */ - -div.clearer { - clear: both; -} - -div.section::after { - display: block; - content: ''; - clear: left; -} - -/* -- relbar ---------------------------------------------------------------- */ - -div.related { - width: 100%; - font-size: 90%; -} - -div.related h3 { - display: none; -} - -div.related ul { - margin: 0; - padding: 0 0 0 10px; - list-style: none; -} - -div.related li { - display: inline; -} - -div.related li.right { - float: right; - margin-right: 5px; -} - -/* -- sidebar --------------------------------------------------------------- */ - -div.sphinxsidebarwrapper { - padding: 10px 5px 0 10px; -} - -div.sphinxsidebar { - float: left; - width: 230px; - margin-left: -100%; - font-size: 90%; - word-wrap: break-word; - overflow-wrap : break-word; -} - -div.sphinxsidebar ul { - list-style: none; -} - -div.sphinxsidebar ul ul, -div.sphinxsidebar ul.want-points { - margin-left: 20px; - list-style: square; -} - -div.sphinxsidebar ul ul { - margin-top: 0; - margin-bottom: 0; -} - -div.sphinxsidebar form { - margin-top: 10px; -} - -div.sphinxsidebar input { - border: 1px solid #98dbcc; - font-family: sans-serif; - font-size: 1em; -} - -div.sphinxsidebar #searchbox form.search { - overflow: hidden; -} - -div.sphinxsidebar #searchbox input[type="text"] { - float: left; - width: 80%; - padding: 0.25em; - box-sizing: border-box; -} - -div.sphinxsidebar #searchbox input[type="submit"] { - float: left; - width: 20%; - border-left: none; - padding: 0.25em; - box-sizing: border-box; -} - - -img { - border: 0; - max-width: 100%; -} - -/* -- search page ----------------------------------------------------------- */ - -ul.search { - margin: 10px 0 0 20px; - padding: 0; -} - -ul.search li { - padding: 5px 0 5px 20px; - background-image: url(file.png); - background-repeat: no-repeat; - background-position: 0 7px; -} - -ul.search li a { - font-weight: bold; -} - -ul.search li p.context { - color: #888; - margin: 2px 0 0 30px; - text-align: left; -} - -ul.keywordmatches li.goodmatch a { - font-weight: bold; -} - -/* -- index page ------------------------------------------------------------ */ - -table.contentstable { - width: 90%; - margin-left: auto; - margin-right: auto; -} - -table.contentstable p.biglink { - line-height: 150%; -} - -a.biglink { - font-size: 1.3em; -} - -span.linkdescr { - font-style: italic; - padding-top: 5px; - font-size: 90%; -} - -/* -- general index --------------------------------------------------------- */ - -table.indextable { - width: 100%; -} - -table.indextable td { - text-align: left; - vertical-align: top; -} - -table.indextable ul { - margin-top: 0; - margin-bottom: 0; - list-style-type: none; -} - -table.indextable > tbody > tr > td > ul { - padding-left: 0em; -} - -table.indextable tr.pcap { - height: 10px; -} - -table.indextable tr.cap { - margin-top: 10px; - background-color: #f2f2f2; -} - -img.toggler { - margin-right: 3px; - margin-top: 3px; - cursor: pointer; -} - -div.modindex-jumpbox { - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; - margin: 1em 0 1em 0; - padding: 0.4em; -} - -div.genindex-jumpbox { - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; - margin: 1em 0 1em 0; - padding: 0.4em; -} - -/* -- domain module index --------------------------------------------------- */ - -table.modindextable td { - padding: 2px; - border-collapse: collapse; -} - -/* -- general body styles --------------------------------------------------- */ - -div.body { - min-width: 360px; - max-width: 800px; -} - -div.body p, div.body dd, div.body li, div.body blockquote { - -moz-hyphens: auto; - -ms-hyphens: auto; - -webkit-hyphens: auto; - hyphens: auto; -} - -a.headerlink { - visibility: hidden; -} -a.brackets:before, -span.brackets > a:before{ - content: "["; -} - -a.brackets:after, -span.brackets > a:after { - content: "]"; -} - - -h1:hover > a.headerlink, -h2:hover > a.headerlink, -h3:hover > a.headerlink, -h4:hover > a.headerlink, -h5:hover > a.headerlink, -h6:hover > a.headerlink, -dt:hover > a.headerlink, -caption:hover > a.headerlink, -p.caption:hover > a.headerlink, -div.code-block-caption:hover > a.headerlink { - visibility: visible; -} - -div.body p.caption { - text-align: inherit; -} - -div.body td { - text-align: left; -} - -.first { - margin-top: 0 !important; -} - -p.rubric { - margin-top: 30px; - font-weight: bold; -} - -img.align-left, figure.align-left, .figure.align-left, object.align-left { - clear: left; - float: left; - margin-right: 1em; -} - -img.align-right, figure.align-right, .figure.align-right, object.align-right { - clear: right; - float: right; - margin-left: 1em; -} - -img.align-center, figure.align-center, .figure.align-center, object.align-center { - display: block; - margin-left: auto; - margin-right: auto; -} - -img.align-default, figure.align-default, .figure.align-default { - display: block; - margin-left: auto; - margin-right: auto; -} - -.align-left { - text-align: left; -} - -.align-center { - text-align: center; -} - -.align-default { - text-align: center; -} - -.align-right { - text-align: right; -} - -/* -- sidebars -------------------------------------------------------------- */ - -div.sidebar, -aside.sidebar { - margin: 0 0 0.5em 1em; - border: 1px solid #ddb; - padding: 7px; - background-color: #ffe; - width: 40%; - float: right; - clear: right; - overflow-x: auto; -} - -p.sidebar-title { - font-weight: bold; -} -div.admonition, div.topic, blockquote { - clear: left; -} - -/* -- topics ---------------------------------------------------------------- */ -div.topic { - border: 1px solid #ccc; - padding: 7px; - margin: 10px 0 10px 0; -} - -p.topic-title { - font-size: 1.1em; - font-weight: bold; - margin-top: 10px; -} - -/* -- admonitions ----------------------------------------------------------- */ - -div.admonition { - margin-top: 10px; - margin-bottom: 10px; - padding: 7px; -} - -div.admonition dt { - font-weight: bold; -} - -p.admonition-title { - margin: 0px 10px 5px 0px; - font-weight: bold; -} - -div.body p.centered { - text-align: center; - margin-top: 25px; -} - -/* -- content of sidebars/topics/admonitions -------------------------------- */ - -div.sidebar > :last-child, -aside.sidebar > :last-child, -div.topic > :last-child, -div.admonition > :last-child { - margin-bottom: 0; -} - -div.sidebar::after, -aside.sidebar::after, -div.topic::after, -div.admonition::after, -blockquote::after { - display: block; - content: ''; - clear: both; -} - -/* -- tables ---------------------------------------------------------------- */ - -table.docutils { - margin-top: 10px; - margin-bottom: 10px; - border: 0; - border-collapse: collapse; -} - -table.align-center { - margin-left: auto; - margin-right: auto; -} - -table.align-default { - margin-left: auto; - margin-right: auto; -} - -table caption span.caption-number { - font-style: italic; -} - -table caption span.caption-text { -} - -table.docutils td, table.docutils th { - padding: 1px 8px 1px 5px; - border-top: 0; - border-left: 0; - border-right: 0; - border-bottom: 1px solid #aaa; -} - -th { - text-align: left; - padding-right: 5px; -} - -table.citation { - border-left: solid 1px gray; - margin-left: 1px; -} - -table.citation td { - border-bottom: none; -} - -th > :first-child, -td > :first-child { - margin-top: 0px; -} - -th > :last-child, -td > :last-child { - margin-bottom: 0px; -} - -/* -- figures --------------------------------------------------------------- */ - -div.figure, figure { - margin: 0.5em; - padding: 0.5em; -} - -div.figure p.caption, figcaption { - padding: 0.3em; -} - -div.figure p.caption span.caption-number, -figcaption span.caption-number { - font-style: italic; -} - -div.figure p.caption span.caption-text, -figcaption span.caption-text { -} - -/* -- field list styles ----------------------------------------------------- */ - -table.field-list td, table.field-list th { - border: 0 !important; -} - -.field-list ul { - margin: 0; - padding-left: 1em; -} - -.field-list p { - margin: 0; -} - -.field-name { - -moz-hyphens: manual; - -ms-hyphens: manual; - -webkit-hyphens: manual; - hyphens: manual; -} - -/* -- hlist styles ---------------------------------------------------------- */ - -table.hlist { - margin: 1em 0; -} - -table.hlist td { - vertical-align: top; -} - -/* -- object description styles --------------------------------------------- */ - -.sig { - font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; -} - -.sig-name, code.descname { - background-color: transparent; - font-weight: bold; -} - -.sig-name { - font-size: 1.1em; -} - -code.descname { - font-size: 1.2em; -} - -.sig-prename, code.descclassname { - background-color: transparent; -} - -.optional { - font-size: 1.3em; -} - -.sig-paren { - font-size: larger; -} - -.sig-param.n { - font-style: italic; -} - -/* C++ specific styling */ - -.sig-inline.c-texpr, -.sig-inline.cpp-texpr { - font-family: unset; -} - -.sig.c .k, .sig.c .kt, -.sig.cpp .k, .sig.cpp .kt { - color: #0033B3; -} - -.sig.c .m, -.sig.cpp .m { - color: #1750EB; -} - -.sig.c .s, .sig.c .sc, -.sig.cpp .s, .sig.cpp .sc { - color: #067D17; -} - - -/* -- other body styles ----------------------------------------------------- */ - -ol.arabic { - list-style: decimal; -} - -ol.loweralpha { - list-style: lower-alpha; -} - -ol.upperalpha { - list-style: upper-alpha; -} - -ol.lowerroman { - list-style: lower-roman; -} - -ol.upperroman { - list-style: upper-roman; -} - -:not(li) > ol > li:first-child > :first-child, -:not(li) > ul > li:first-child > :first-child { - margin-top: 0px; -} - -:not(li) > ol > li:last-child > :last-child, -:not(li) > ul > li:last-child > :last-child { - margin-bottom: 0px; -} - -ol.simple ol p, -ol.simple ul p, -ul.simple ol p, -ul.simple ul p { - margin-top: 0; -} - -ol.simple > li:not(:first-child) > p, -ul.simple > li:not(:first-child) > p { - margin-top: 0; -} - -ol.simple p, -ul.simple p { - margin-bottom: 0; -} -dl.footnote > dt, -dl.citation > dt { - float: left; - margin-right: 0.5em; -} - -dl.footnote > dd, -dl.citation > dd { - margin-bottom: 0em; -} - -dl.footnote > dd:after, -dl.citation > dd:after { - content: ""; - clear: both; -} - -dl.field-list { - display: grid; - grid-template-columns: fit-content(30%) auto; -} - -dl.field-list > dt { - font-weight: bold; - word-break: break-word; - padding-left: 0.5em; - padding-right: 5px; -} -dl.field-list > dt:after { - content: ":"; -} - - -dl.field-list > dd { - padding-left: 0.5em; - margin-top: 0em; - margin-left: 0em; - margin-bottom: 0em; -} - -dl { - margin-bottom: 15px; -} - -dd > :first-child { - margin-top: 0px; -} - -dd ul, dd table { - margin-bottom: 10px; -} - -dd { - margin-top: 3px; - margin-bottom: 10px; - margin-left: 30px; -} - -dl > dd:last-child, -dl > dd:last-child > :last-child { - margin-bottom: 0; -} - -dt:target, span.highlighted { - background-color: #fbe54e; -} - -rect.highlighted { - fill: #fbe54e; -} - -dl.glossary dt { - font-weight: bold; - font-size: 1.1em; -} - -.versionmodified { - font-style: italic; -} - -.system-message { - background-color: #fda; - padding: 5px; - border: 3px solid red; -} - -.footnote:target { - background-color: #ffa; -} - -.line-block { - display: block; - margin-top: 1em; - margin-bottom: 1em; -} - -.line-block .line-block { - margin-top: 0; - margin-bottom: 0; - margin-left: 1.5em; -} - -.guilabel, .menuselection { - font-family: sans-serif; -} - -.accelerator { - text-decoration: underline; -} - -.classifier { - font-style: oblique; -} - -.classifier:before { - font-style: normal; - margin: 0 0.5em; - content: ":"; - display: inline-block; -} - -abbr, acronym { - border-bottom: dotted 1px; - cursor: help; -} - -/* -- code displays --------------------------------------------------------- */ - -pre { - overflow: auto; - overflow-y: hidden; /* fixes display issues on Chrome browsers */ -} - -pre, div[class*="highlight-"] { - clear: both; -} - -span.pre { - -moz-hyphens: none; - -ms-hyphens: none; - -webkit-hyphens: none; - hyphens: none; - white-space: nowrap; -} - -div[class*="highlight-"] { - margin: 1em 0; -} - -td.linenos pre { - border: 0; - background-color: transparent; - color: #aaa; -} - -table.highlighttable { - display: block; -} - -table.highlighttable tbody { - display: block; -} - -table.highlighttable tr { - display: flex; -} - -table.highlighttable td { - margin: 0; - padding: 0; -} - -table.highlighttable td.linenos { - padding-right: 0.5em; -} - -table.highlighttable td.code { - flex: 1; - overflow: hidden; -} - -.highlight .hll { - display: block; -} - -div.highlight pre, -table.highlighttable pre { - margin: 0; -} - -div.code-block-caption + div { - margin-top: 0; -} - -div.code-block-caption { - margin-top: 1em; - padding: 2px 5px; - font-size: small; -} - -div.code-block-caption code { - background-color: transparent; -} - -table.highlighttable td.linenos, -span.linenos, -div.highlight span.gp { /* gp: Generic.Prompt */ - user-select: none; - -webkit-user-select: text; /* Safari fallback only */ - -webkit-user-select: none; /* Chrome/Safari */ - -moz-user-select: none; /* Firefox */ - -ms-user-select: none; /* IE10+ */ -} - -div.code-block-caption span.caption-number { - padding: 0.1em 0.3em; - font-style: italic; -} - -div.code-block-caption span.caption-text { -} - -div.literal-block-wrapper { - margin: 1em 0; -} - -code.xref, a code { - background-color: transparent; - font-weight: bold; -} - -h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { - background-color: transparent; -} - -.viewcode-link { - float: right; -} - -.viewcode-back { - float: right; - font-family: sans-serif; -} - -div.viewcode-block:target { - margin: -1px -10px; - padding: 0 10px; -} - -/* -- math display ---------------------------------------------------------- */ - -img.math { - vertical-align: middle; -} - -div.body div.math p { - text-align: center; -} - -span.eqno { - float: right; -} - -span.eqno a.headerlink { - position: absolute; - z-index: 1; -} - -div.math:hover a.headerlink { - visibility: visible; -} - -/* -- printout stylesheet --------------------------------------------------- */ - -@media print { - div.document, - div.documentwrapper, - div.bodywrapper { - margin: 0 !important; - width: 100%; - } - - div.sphinxsidebar, - div.related, - div.footer, - #top-link { - display: none; - } -} \ No newline at end of file diff --git a/docs/docs_build/_static/css/badge_only.css b/docs/docs_build/_static/css/badge_only.css deleted file mode 100644 index c718cee..0000000 --- a/docs/docs_build/_static/css/badge_only.css +++ /dev/null @@ -1 +0,0 @@ -.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-style:normal;font-weight:400;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#FontAwesome) format("svg")}.fa:before{font-family:FontAwesome;font-style:normal;font-weight:400;line-height:1}.fa:before,a .fa{text-decoration:inherit}.fa:before,a .fa,li .fa{display:inline-block}li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before,.icon-book:before{content:"\f02d"}.fa-caret-down:before,.icon-caret-down:before{content:"\f0d7"}.fa-caret-up:before,.icon-caret-up:before{content:"\f0d8"}.fa-caret-left:before,.icon-caret-left:before{content:"\f0d9"}.fa-caret-right:before,.icon-caret-right:before{content:"\f0da"}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60}.rst-versions .rst-current-version:after{clear:both;content:"";display:block}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}} \ No newline at end of file diff --git a/docs/docs_build/_static/css/fonts/Roboto-Slab-Bold.woff b/docs/docs_build/_static/css/fonts/Roboto-Slab-Bold.woff deleted file mode 100644 index 6cb6000..0000000 Binary files a/docs/docs_build/_static/css/fonts/Roboto-Slab-Bold.woff and /dev/null differ diff --git a/docs/docs_build/_static/css/fonts/Roboto-Slab-Bold.woff2 b/docs/docs_build/_static/css/fonts/Roboto-Slab-Bold.woff2 deleted file mode 100644 index 7059e23..0000000 Binary files a/docs/docs_build/_static/css/fonts/Roboto-Slab-Bold.woff2 and /dev/null differ diff --git a/docs/docs_build/_static/css/fonts/Roboto-Slab-Regular.woff b/docs/docs_build/_static/css/fonts/Roboto-Slab-Regular.woff deleted file mode 100644 index f815f63..0000000 Binary files a/docs/docs_build/_static/css/fonts/Roboto-Slab-Regular.woff and /dev/null differ diff --git a/docs/docs_build/_static/css/fonts/Roboto-Slab-Regular.woff2 b/docs/docs_build/_static/css/fonts/Roboto-Slab-Regular.woff2 deleted file mode 100644 index f2c76e5..0000000 Binary files a/docs/docs_build/_static/css/fonts/Roboto-Slab-Regular.woff2 and /dev/null differ diff --git a/docs/docs_build/_static/css/fonts/fontawesome-webfont.eot b/docs/docs_build/_static/css/fonts/fontawesome-webfont.eot deleted file mode 100644 index e9f60ca..0000000 Binary files a/docs/docs_build/_static/css/fonts/fontawesome-webfont.eot and /dev/null differ diff --git a/docs/docs_build/_static/css/fonts/fontawesome-webfont.svg b/docs/docs_build/_static/css/fonts/fontawesome-webfont.svg deleted file mode 100644 index 855c845..0000000 --- a/docs/docs_build/_static/css/fonts/fontawesome-webfont.svg +++ /dev/null @@ -1,2671 +0,0 @@ - - - - -Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 - By ,,, -Copyright Dave Gandy 2016. All rights reserved. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/docs_build/_static/css/fonts/fontawesome-webfont.ttf b/docs/docs_build/_static/css/fonts/fontawesome-webfont.ttf deleted file mode 100644 index 35acda2..0000000 Binary files a/docs/docs_build/_static/css/fonts/fontawesome-webfont.ttf and /dev/null differ diff --git a/docs/docs_build/_static/css/fonts/fontawesome-webfont.woff b/docs/docs_build/_static/css/fonts/fontawesome-webfont.woff deleted file mode 100644 index 400014a..0000000 Binary files a/docs/docs_build/_static/css/fonts/fontawesome-webfont.woff and /dev/null differ diff --git a/docs/docs_build/_static/css/fonts/fontawesome-webfont.woff2 b/docs/docs_build/_static/css/fonts/fontawesome-webfont.woff2 deleted file mode 100644 index 4d13fc6..0000000 Binary files a/docs/docs_build/_static/css/fonts/fontawesome-webfont.woff2 and /dev/null differ diff --git a/docs/docs_build/_static/css/fonts/lato-bold-italic.woff b/docs/docs_build/_static/css/fonts/lato-bold-italic.woff deleted file mode 100644 index 88ad05b..0000000 Binary files a/docs/docs_build/_static/css/fonts/lato-bold-italic.woff and /dev/null differ diff --git a/docs/docs_build/_static/css/fonts/lato-bold-italic.woff2 b/docs/docs_build/_static/css/fonts/lato-bold-italic.woff2 deleted file mode 100644 index c4e3d80..0000000 Binary files a/docs/docs_build/_static/css/fonts/lato-bold-italic.woff2 and /dev/null differ diff --git a/docs/docs_build/_static/css/fonts/lato-bold.woff b/docs/docs_build/_static/css/fonts/lato-bold.woff deleted file mode 100644 index c6dff51..0000000 Binary files a/docs/docs_build/_static/css/fonts/lato-bold.woff and /dev/null differ diff --git a/docs/docs_build/_static/css/fonts/lato-bold.woff2 b/docs/docs_build/_static/css/fonts/lato-bold.woff2 deleted file mode 100644 index bb19504..0000000 Binary files a/docs/docs_build/_static/css/fonts/lato-bold.woff2 and /dev/null differ diff --git a/docs/docs_build/_static/css/fonts/lato-normal-italic.woff b/docs/docs_build/_static/css/fonts/lato-normal-italic.woff deleted file mode 100644 index 76114bc..0000000 Binary files a/docs/docs_build/_static/css/fonts/lato-normal-italic.woff and /dev/null differ diff --git a/docs/docs_build/_static/css/fonts/lato-normal-italic.woff2 b/docs/docs_build/_static/css/fonts/lato-normal-italic.woff2 deleted file mode 100644 index 3404f37..0000000 Binary files a/docs/docs_build/_static/css/fonts/lato-normal-italic.woff2 and /dev/null differ diff --git a/docs/docs_build/_static/css/fonts/lato-normal.woff b/docs/docs_build/_static/css/fonts/lato-normal.woff deleted file mode 100644 index ae1307f..0000000 Binary files a/docs/docs_build/_static/css/fonts/lato-normal.woff and /dev/null differ diff --git a/docs/docs_build/_static/css/fonts/lato-normal.woff2 b/docs/docs_build/_static/css/fonts/lato-normal.woff2 deleted file mode 100644 index 3bf9843..0000000 Binary files a/docs/docs_build/_static/css/fonts/lato-normal.woff2 and /dev/null differ diff --git a/docs/docs_build/_static/css/theme.css b/docs/docs_build/_static/css/theme.css deleted file mode 100644 index 09a1af8..0000000 --- a/docs/docs_build/_static/css/theme.css +++ /dev/null @@ -1,4 +0,0 @@ -html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden],audio:not([controls]){display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;text-decoration:none}ins,mark{color:#000}mark{background:#ff0;font-style:italic;font-weight:700}.rst-content code,.rst-content tt,code,kbd,pre,samp{font-family:monospace,serif;_font-family:courier new,monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:after,q:before{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}dl,ol,ul{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure,form{margin:0}label{cursor:pointer}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type=button],input[type=reset],input[type=submit]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}textarea{resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none!important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{body,html,section{background:none!important}*{box-shadow:none!important;text-shadow:none!important;filter:none!important;-ms-filter:none!important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}.rst-content .toctree-wrapper>p.caption,h2,h3,p{orphans:3;widows:3}.rst-content .toctree-wrapper>p.caption,h2,h3{page-break-after:avoid}}.btn,.fa:before,.icon:before,.rst-content .admonition,.rst-content .admonition-title:before,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .code-block-caption .headerlink:before,.rst-content .danger,.rst-content .eqno .headerlink:before,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-alert,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before,input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select,textarea{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}/*! - * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome - * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */@font-face{font-family:FontAwesome;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713);src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix&v=4.7.0) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#fontawesomeregular) format("svg");font-weight:400;font-style:normal}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:.14286em;text-align:center}.fa-li.fa-lg{left:-1.85714em}.fa-border{padding:.2em .25em .15em;border:.08em solid #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa-pull-left.icon,.fa.fa-pull-left,.rst-content .code-block-caption .fa-pull-left.headerlink,.rst-content .eqno .fa-pull-left.headerlink,.rst-content .fa-pull-left.admonition-title,.rst-content code.download span.fa-pull-left:first-child,.rst-content dl dt .fa-pull-left.headerlink,.rst-content h1 .fa-pull-left.headerlink,.rst-content h2 .fa-pull-left.headerlink,.rst-content h3 .fa-pull-left.headerlink,.rst-content h4 .fa-pull-left.headerlink,.rst-content h5 .fa-pull-left.headerlink,.rst-content h6 .fa-pull-left.headerlink,.rst-content p .fa-pull-left.headerlink,.rst-content table>caption .fa-pull-left.headerlink,.rst-content tt.download span.fa-pull-left:first-child,.wy-menu-vertical li.current>a button.fa-pull-left.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-left.toctree-expand,.wy-menu-vertical li button.fa-pull-left.toctree-expand{margin-right:.3em}.fa-pull-right.icon,.fa.fa-pull-right,.rst-content .code-block-caption .fa-pull-right.headerlink,.rst-content .eqno .fa-pull-right.headerlink,.rst-content .fa-pull-right.admonition-title,.rst-content code.download span.fa-pull-right:first-child,.rst-content dl dt .fa-pull-right.headerlink,.rst-content h1 .fa-pull-right.headerlink,.rst-content h2 .fa-pull-right.headerlink,.rst-content h3 .fa-pull-right.headerlink,.rst-content h4 .fa-pull-right.headerlink,.rst-content h5 .fa-pull-right.headerlink,.rst-content h6 .fa-pull-right.headerlink,.rst-content p .fa-pull-right.headerlink,.rst-content table>caption .fa-pull-right.headerlink,.rst-content tt.download span.fa-pull-right:first-child,.wy-menu-vertical li.current>a button.fa-pull-right.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-right.toctree-expand,.wy-menu-vertical li button.fa-pull-right.toctree-expand{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.pull-left.icon,.rst-content .code-block-caption .pull-left.headerlink,.rst-content .eqno .pull-left.headerlink,.rst-content .pull-left.admonition-title,.rst-content code.download span.pull-left:first-child,.rst-content dl dt .pull-left.headerlink,.rst-content h1 .pull-left.headerlink,.rst-content h2 .pull-left.headerlink,.rst-content h3 .pull-left.headerlink,.rst-content h4 .pull-left.headerlink,.rst-content h5 .pull-left.headerlink,.rst-content h6 .pull-left.headerlink,.rst-content p .pull-left.headerlink,.rst-content table>caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.wy-menu-vertical li.current>a button.pull-left.toctree-expand,.wy-menu-vertical li.on a button.pull-left.toctree-expand,.wy-menu-vertical li button.pull-left.toctree-expand{margin-right:.3em}.fa.pull-right,.pull-right.icon,.rst-content .code-block-caption .pull-right.headerlink,.rst-content .eqno .pull-right.headerlink,.rst-content .pull-right.admonition-title,.rst-content code.download span.pull-right:first-child,.rst-content dl dt .pull-right.headerlink,.rst-content h1 .pull-right.headerlink,.rst-content h2 .pull-right.headerlink,.rst-content h3 .pull-right.headerlink,.rst-content h4 .pull-right.headerlink,.rst-content h5 .pull-right.headerlink,.rst-content h6 .pull-right.headerlink,.rst-content p .pull-right.headerlink,.rst-content table>caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.wy-menu-vertical li.current>a button.pull-right.toctree-expand,.wy-menu-vertical li.on a button.pull-right.toctree-expand,.wy-menu-vertical li button.pull-right.toctree-expand{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);-ms-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scaleY(-1);-ms-transform:scaleY(-1);transform:scaleY(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before,.icon-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-close:before,.fa-remove:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-cog:before,.fa-gear:before{content:""}.fa-trash-o:before{content:""}.fa-home:before,.icon-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before,.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-repeat:before,.fa-rotate-right:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before,.icon-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:""}.fa-check-circle:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before,.rst-content .admonition-title:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before,.icon-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-exclamation-triangle:before,.fa-warning:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-cogs:before,.fa-gears:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before,.icon-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before,.icon-circle-arrow-left:before{content:""}.fa-arrow-circle-right:before,.icon-circle-arrow-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before,.icon-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-floppy-o:before,.fa-save:before{content:""}.fa-square:before{content:""}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before,.icon-caret-down:before,.wy-dropdown .caret:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-sort:before,.fa-unsorted:before{content:""}.fa-sort-desc:before,.fa-sort-down:before{content:""}.fa-sort-asc:before,.fa-sort-up:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-gavel:before,.fa-legal:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-bolt:before,.fa-flash:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-clipboard:before,.fa-paste:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-chain-broken:before,.fa-unlink:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:""}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:""}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:""}.fa-eur:before,.fa-euro:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-inr:before,.fa-rupee:before{content:""}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:""}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:""}.fa-krw:before,.fa-won:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before,.icon-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-try:before,.fa-turkish-lira:before{content:""}.fa-plus-square-o:before,.wy-menu-vertical li button.toctree-expand:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-bank:before,.fa-institution:before,.fa-university:before{content:""}.fa-graduation-cap:before,.fa-mortar-board:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:""}.fa-file-archive-o:before,.fa-file-zip-o:before{content:""}.fa-file-audio-o:before,.fa-file-sound-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-rebel:before,.fa-resistance:before{content:""}.fa-empire:before,.fa-ge:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-hacker-news:before,.fa-y-combinator-square:before,.fa-yc-square:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-paper-plane:before,.fa-send:before{content:""}.fa-paper-plane-o:before,.fa-send-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-bed:before,.fa-hotel:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-y-combinator:before,.fa-yc:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery-full:before,.fa-battery:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-paper-o:before,.fa-hand-stop-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-television:before,.fa-tv:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""}.fa-gitlab:before,.icon-gitlab:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpforms:before{content:""}.fa-envira:before{content:""}.fa-universal-access:before{content:""}.fa-wheelchair-alt:before{content:""}.fa-question-circle-o:before{content:""}.fa-blind:before{content:""}.fa-audio-description:before{content:""}.fa-volume-control-phone:before{content:""}.fa-braille:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before{content:""}.fa-deaf:before,.fa-deafness:before,.fa-hard-of-hearing:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-sign-language:before,.fa-signing:before{content:""}.fa-low-vision:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-pied-piper:before{content:""}.fa-first-order:before{content:""}.fa-yoast:before{content:""}.fa-themeisle:before{content:""}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:""}.fa-fa:before,.fa-font-awesome:before{content:""}.fa-handshake-o:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-o:before{content:""}.fa-linode:before{content:""}.fa-address-book:before{content:""}.fa-address-book-o:before{content:""}.fa-address-card:before,.fa-vcard:before{content:""}.fa-address-card-o:before,.fa-vcard-o:before{content:""}.fa-user-circle:before{content:""}.fa-user-circle-o:before{content:""}.fa-user-o:before{content:""}.fa-id-badge:before{content:""}.fa-drivers-license:before,.fa-id-card:before{content:""}.fa-drivers-license-o:before,.fa-id-card-o:before{content:""}.fa-quora:before{content:""}.fa-free-code-camp:before{content:""}.fa-telegram:before{content:""}.fa-thermometer-4:before,.fa-thermometer-full:before,.fa-thermometer:before{content:""}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:""}.fa-thermometer-2:before,.fa-thermometer-half:before{content:""}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:""}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:""}.fa-shower:before{content:""}.fa-bath:before,.fa-bathtub:before,.fa-s15:before{content:""}.fa-podcast:before{content:""}.fa-window-maximize:before{content:""}.fa-window-minimize:before{content:""}.fa-window-restore:before{content:""}.fa-times-rectangle:before,.fa-window-close:before{content:""}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:""}.fa-bandcamp:before{content:""}.fa-grav:before{content:""}.fa-etsy:before{content:""}.fa-imdb:before{content:""}.fa-ravelry:before{content:""}.fa-eercast:before{content:""}.fa-microchip:before{content:""}.fa-snowflake-o:before{content:""}.fa-superpowers:before{content:""}.fa-wpexplorer:before{content:""}.fa-meetup:before{content:""}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{font-family:inherit}.fa:before,.icon:before,.rst-content .admonition-title:before,.rst-content .code-block-caption .headerlink:before,.rst-content .eqno .headerlink:before,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before{font-family:FontAwesome;display:inline-block;font-style:normal;font-weight:400;line-height:1;text-decoration:inherit}.rst-content .code-block-caption a .headerlink,.rst-content .eqno a .headerlink,.rst-content a .admonition-title,.rst-content code.download a span:first-child,.rst-content dl dt a .headerlink,.rst-content h1 a .headerlink,.rst-content h2 a .headerlink,.rst-content h3 a .headerlink,.rst-content h4 a .headerlink,.rst-content h5 a .headerlink,.rst-content h6 a .headerlink,.rst-content p.caption a .headerlink,.rst-content p a .headerlink,.rst-content table>caption a .headerlink,.rst-content tt.download a span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li a button.toctree-expand,a .fa,a .icon,a .rst-content .admonition-title,a .rst-content .code-block-caption .headerlink,a .rst-content .eqno .headerlink,a .rst-content code.download span:first-child,a .rst-content dl dt .headerlink,a .rst-content h1 .headerlink,a .rst-content h2 .headerlink,a .rst-content h3 .headerlink,a .rst-content h4 .headerlink,a .rst-content h5 .headerlink,a .rst-content h6 .headerlink,a .rst-content p.caption .headerlink,a .rst-content p .headerlink,a .rst-content table>caption .headerlink,a .rst-content tt.download span:first-child,a .wy-menu-vertical li button.toctree-expand{display:inline-block;text-decoration:inherit}.btn .fa,.btn .icon,.btn .rst-content .admonition-title,.btn .rst-content .code-block-caption .headerlink,.btn .rst-content .eqno .headerlink,.btn .rst-content code.download span:first-child,.btn .rst-content dl dt .headerlink,.btn .rst-content h1 .headerlink,.btn .rst-content h2 .headerlink,.btn .rst-content h3 .headerlink,.btn .rst-content h4 .headerlink,.btn .rst-content h5 .headerlink,.btn .rst-content h6 .headerlink,.btn .rst-content p .headerlink,.btn .rst-content table>caption .headerlink,.btn .rst-content tt.download span:first-child,.btn .wy-menu-vertical li.current>a button.toctree-expand,.btn .wy-menu-vertical li.on a button.toctree-expand,.btn .wy-menu-vertical li button.toctree-expand,.nav .fa,.nav .icon,.nav .rst-content .admonition-title,.nav .rst-content .code-block-caption .headerlink,.nav .rst-content .eqno .headerlink,.nav .rst-content code.download span:first-child,.nav .rst-content dl dt .headerlink,.nav .rst-content h1 .headerlink,.nav .rst-content h2 .headerlink,.nav .rst-content h3 .headerlink,.nav .rst-content h4 .headerlink,.nav .rst-content h5 .headerlink,.nav .rst-content h6 .headerlink,.nav .rst-content p .headerlink,.nav .rst-content table>caption .headerlink,.nav .rst-content tt.download span:first-child,.nav .wy-menu-vertical li.current>a button.toctree-expand,.nav .wy-menu-vertical li.on a button.toctree-expand,.nav .wy-menu-vertical li button.toctree-expand,.rst-content .btn .admonition-title,.rst-content .code-block-caption .btn .headerlink,.rst-content .code-block-caption .nav .headerlink,.rst-content .eqno .btn .headerlink,.rst-content .eqno .nav .headerlink,.rst-content .nav .admonition-title,.rst-content code.download .btn span:first-child,.rst-content code.download .nav span:first-child,.rst-content dl dt .btn .headerlink,.rst-content dl dt .nav .headerlink,.rst-content h1 .btn .headerlink,.rst-content h1 .nav .headerlink,.rst-content h2 .btn .headerlink,.rst-content h2 .nav .headerlink,.rst-content h3 .btn .headerlink,.rst-content h3 .nav .headerlink,.rst-content h4 .btn .headerlink,.rst-content h4 .nav .headerlink,.rst-content h5 .btn .headerlink,.rst-content h5 .nav .headerlink,.rst-content h6 .btn .headerlink,.rst-content h6 .nav .headerlink,.rst-content p .btn .headerlink,.rst-content p .nav .headerlink,.rst-content table>caption .btn .headerlink,.rst-content table>caption .nav .headerlink,.rst-content tt.download .btn span:first-child,.rst-content tt.download .nav span:first-child,.wy-menu-vertical li .btn button.toctree-expand,.wy-menu-vertical li.current>a .btn button.toctree-expand,.wy-menu-vertical li.current>a .nav button.toctree-expand,.wy-menu-vertical li .nav button.toctree-expand,.wy-menu-vertical li.on a .btn button.toctree-expand,.wy-menu-vertical li.on a .nav button.toctree-expand{display:inline}.btn .fa-large.icon,.btn .fa.fa-large,.btn .rst-content .code-block-caption .fa-large.headerlink,.btn .rst-content .eqno .fa-large.headerlink,.btn .rst-content .fa-large.admonition-title,.btn .rst-content code.download span.fa-large:first-child,.btn .rst-content dl dt .fa-large.headerlink,.btn .rst-content h1 .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.btn .rst-content p .fa-large.headerlink,.btn .rst-content table>caption .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.btn .wy-menu-vertical li button.fa-large.toctree-expand,.nav .fa-large.icon,.nav .fa.fa-large,.nav .rst-content .code-block-caption .fa-large.headerlink,.nav .rst-content .eqno .fa-large.headerlink,.nav .rst-content .fa-large.admonition-title,.nav .rst-content code.download span.fa-large:first-child,.nav .rst-content dl dt .fa-large.headerlink,.nav .rst-content h1 .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.nav .rst-content p .fa-large.headerlink,.nav .rst-content table>caption .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.nav .wy-menu-vertical li button.fa-large.toctree-expand,.rst-content .btn .fa-large.admonition-title,.rst-content .code-block-caption .btn .fa-large.headerlink,.rst-content .code-block-caption .nav .fa-large.headerlink,.rst-content .eqno .btn .fa-large.headerlink,.rst-content .eqno .nav .fa-large.headerlink,.rst-content .nav .fa-large.admonition-title,.rst-content code.download .btn span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.rst-content dl dt .btn .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.rst-content p .btn .fa-large.headerlink,.rst-content p .nav .fa-large.headerlink,.rst-content table>caption .btn .fa-large.headerlink,.rst-content table>caption .nav .fa-large.headerlink,.rst-content tt.download .btn span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.wy-menu-vertical li .btn button.fa-large.toctree-expand,.wy-menu-vertical li .nav button.fa-large.toctree-expand{line-height:.9em}.btn .fa-spin.icon,.btn .fa.fa-spin,.btn .rst-content .code-block-caption .fa-spin.headerlink,.btn .rst-content .eqno .fa-spin.headerlink,.btn .rst-content .fa-spin.admonition-title,.btn .rst-content code.download span.fa-spin:first-child,.btn .rst-content dl dt .fa-spin.headerlink,.btn .rst-content h1 .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.btn .rst-content p .fa-spin.headerlink,.btn .rst-content table>caption .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.btn .wy-menu-vertical li button.fa-spin.toctree-expand,.nav .fa-spin.icon,.nav .fa.fa-spin,.nav .rst-content .code-block-caption .fa-spin.headerlink,.nav .rst-content .eqno .fa-spin.headerlink,.nav .rst-content .fa-spin.admonition-title,.nav .rst-content code.download span.fa-spin:first-child,.nav .rst-content dl dt .fa-spin.headerlink,.nav .rst-content h1 .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.nav .rst-content p .fa-spin.headerlink,.nav .rst-content table>caption .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.nav .wy-menu-vertical li button.fa-spin.toctree-expand,.rst-content .btn .fa-spin.admonition-title,.rst-content .code-block-caption .btn .fa-spin.headerlink,.rst-content .code-block-caption .nav .fa-spin.headerlink,.rst-content .eqno .btn .fa-spin.headerlink,.rst-content .eqno .nav .fa-spin.headerlink,.rst-content .nav .fa-spin.admonition-title,.rst-content code.download .btn span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.rst-content dl dt .btn .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.rst-content p .btn .fa-spin.headerlink,.rst-content p .nav .fa-spin.headerlink,.rst-content table>caption .btn .fa-spin.headerlink,.rst-content table>caption .nav .fa-spin.headerlink,.rst-content tt.download .btn span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.wy-menu-vertical li .btn button.fa-spin.toctree-expand,.wy-menu-vertical li .nav button.fa-spin.toctree-expand{display:inline-block}.btn.fa:before,.btn.icon:before,.rst-content .btn.admonition-title:before,.rst-content .code-block-caption .btn.headerlink:before,.rst-content .eqno .btn.headerlink:before,.rst-content code.download span.btn:first-child:before,.rst-content dl dt .btn.headerlink:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content p .btn.headerlink:before,.rst-content table>caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.wy-menu-vertical li button.btn.toctree-expand:before{opacity:.5;-webkit-transition:opacity .05s ease-in;-moz-transition:opacity .05s ease-in;transition:opacity .05s ease-in}.btn.fa:hover:before,.btn.icon:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content .code-block-caption .btn.headerlink:hover:before,.rst-content .eqno .btn.headerlink:hover:before,.rst-content code.download span.btn:first-child:hover:before,.rst-content dl dt .btn.headerlink:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content p .btn.headerlink:hover:before,.rst-content table>caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.wy-menu-vertical li button.btn.toctree-expand:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .icon:before,.btn-mini .rst-content .admonition-title:before,.btn-mini .rst-content .code-block-caption .headerlink:before,.btn-mini .rst-content .eqno .headerlink:before,.btn-mini .rst-content code.download span:first-child:before,.btn-mini .rst-content dl dt .headerlink:before,.btn-mini .rst-content h1 .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.btn-mini .rst-content p .headerlink:before,.btn-mini .rst-content table>caption .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.btn-mini .wy-menu-vertical li button.toctree-expand:before,.rst-content .btn-mini .admonition-title:before,.rst-content .code-block-caption .btn-mini .headerlink:before,.rst-content .eqno .btn-mini .headerlink:before,.rst-content code.download .btn-mini span:first-child:before,.rst-content dl dt .btn-mini .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.rst-content p .btn-mini .headerlink:before,.rst-content table>caption .btn-mini .headerlink:before,.rst-content tt.download .btn-mini span:first-child:before,.wy-menu-vertical li .btn-mini button.toctree-expand:before{font-size:14px;vertical-align:-15%}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.wy-alert{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.rst-content .admonition-title,.wy-alert-title{font-weight:700;display:block;color:#fff;background:#6ab0de;padding:6px 12px;margin:-12px -12px 12px}.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.admonition,.rst-content .wy-alert-danger.admonition-todo,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.wy-alert.wy-alert-danger{background:#fdf3f2}.rst-content .danger .admonition-title,.rst-content .danger .wy-alert-title,.rst-content .error .admonition-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.rst-content .wy-alert-danger.admonition .admonition-title,.rst-content .wy-alert-danger.admonition .wy-alert-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.wy-alert.wy-alert-danger .wy-alert-title{background:#f29f97}.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .warning,.rst-content .wy-alert-warning.admonition,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.note,.rst-content .wy-alert-warning.seealso,.rst-content .wy-alert-warning.tip,.wy-alert.wy-alert-warning{background:#ffedcc}.rst-content .admonition-todo .admonition-title,.rst-content .admonition-todo .wy-alert-title,.rst-content .attention .admonition-title,.rst-content .attention .wy-alert-title,.rst-content .caution .admonition-title,.rst-content .caution .wy-alert-title,.rst-content .warning .admonition-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.admonition .admonition-title,.rst-content .wy-alert-warning.admonition .wy-alert-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.wy-alert.wy-alert-warning .wy-alert-title{background:#f0b37e}.rst-content .note,.rst-content .seealso,.rst-content .wy-alert-info.admonition,.rst-content .wy-alert-info.admonition-todo,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.wy-alert.wy-alert-info{background:#e7f2fa}.rst-content .note .admonition-title,.rst-content .note .wy-alert-title,.rst-content .seealso .admonition-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .admonition-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.rst-content .wy-alert-info.admonition .admonition-title,.rst-content .wy-alert-info.admonition .wy-alert-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.wy-alert.wy-alert-info .wy-alert-title{background:#6ab0de}.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.admonition,.rst-content .wy-alert-success.admonition-todo,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.warning,.wy-alert.wy-alert-success{background:#dbfaf4}.rst-content .hint .admonition-title,.rst-content .hint .wy-alert-title,.rst-content .important .admonition-title,.rst-content .important .wy-alert-title,.rst-content .tip .admonition-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .admonition-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.rst-content .wy-alert-success.admonition .admonition-title,.rst-content .wy-alert-success.admonition .wy-alert-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.wy-alert.wy-alert-success .wy-alert-title{background:#1abc9c}.rst-content .wy-alert-neutral.admonition,.rst-content .wy-alert-neutral.admonition-todo,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.wy-alert.wy-alert-neutral{background:#f3f6f6}.rst-content .wy-alert-neutral.admonition-todo .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.rst-content .wy-alert-neutral.admonition .admonition-title,.rst-content .wy-alert-neutral.admonition .wy-alert-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.wy-alert.wy-alert-neutral .wy-alert-title{color:#404040;background:#e1e4e5}.rst-content .wy-alert-neutral.admonition-todo a,.rst-content .wy-alert-neutral.admonition a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.wy-alert.wy-alert-neutral a{color:#2980b9}.rst-content .admonition-todo p:last-child,.rst-content .admonition p:last-child,.rst-content .attention p:last-child,.rst-content .caution p:last-child,.rst-content .danger p:last-child,.rst-content .error p:last-child,.rst-content .hint p:last-child,.rst-content .important p:last-child,.rst-content .note p:last-child,.rst-content .seealso p:last-child,.rst-content .tip p:last-child,.rst-content .warning p:last-child,.wy-alert p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0;left:0;z-index:600}.wy-tray-container li{display:block;width:300px;background:transparent;color:#fff;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,.1);padding:0 24px;min-width:20%;opacity:0;height:0;line-height:56px;overflow:hidden;-webkit-transition:all .3s ease-in;-moz-transition:all .3s ease-in;transition:all .3s ease-in}.wy-tray-container li.wy-tray-item-success{background:#27ae60}.wy-tray-container li.wy-tray-item-info{background:#2980b9}.wy-tray-container li.wy-tray-item-warning{background:#e67e22}.wy-tray-container li.wy-tray-item-danger{background:#e74c3c}.wy-tray-container li.on{opacity:1;height:56px}@media screen and (max-width:768px){.wy-tray-container{bottom:auto;top:0;width:100%}.wy-tray-container li{width:100%}}button{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;cursor:pointer;line-height:normal;-webkit-appearance:button;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}.btn{display:inline-block;border-radius:2px;line-height:normal;white-space:nowrap;text-align:center;cursor:pointer;font-size:100%;padding:6px 12px 8px;color:#fff;border:1px solid rgba(0,0,0,.1);background-color:#27ae60;text-decoration:none;font-weight:400;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 2px -1px hsla(0,0%,100%,.5),inset 0 -2px 0 0 rgba(0,0,0,.1);outline-none:false;vertical-align:middle;*display:inline;zoom:1;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all .1s linear;-moz-transition:all .1s linear;transition:all .1s linear}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;outline:0}.btn:active{box-shadow:inset 0 -1px 0 0 rgba(0,0,0,.05),inset 0 2px 0 0 rgba(0,0,0,.1);padding:8px 12px 6px}.btn:visited{color:#fff}.btn-disabled,.btn-disabled:active,.btn-disabled:focus,.btn-disabled:hover,.btn:disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980b9!important}.btn-info:hover{background-color:#2e8ece!important}.btn-neutral{background-color:#f3f6f6!important;color:#404040!important}.btn-neutral:hover{background-color:#e5ebeb!important;color:#404040}.btn-neutral:visited{color:#404040!important}.btn-success{background-color:#27ae60!important}.btn-success:hover{background-color:#295!important}.btn-danger{background-color:#e74c3c!important}.btn-danger:hover{background-color:#ea6153!important}.btn-warning{background-color:#e67e22!important}.btn-warning:hover{background-color:#e98b39!important}.btn-invert{background-color:#222}.btn-invert:hover{background-color:#2f2f2f!important}.btn-link{background-color:transparent!important;color:#2980b9;box-shadow:none;border-color:transparent!important}.btn-link:active,.btn-link:hover{background-color:transparent!important;color:#409ad5!important;box-shadow:none}.btn-link:visited{color:#9b59b6}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:after,.wy-btn-group:before{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown-active .wy-dropdown-menu{display:block}.wy-dropdown-menu{position:absolute;left:0;display:none;float:left;top:100%;min-width:100%;background:#fcfcfc;z-index:100;border:1px solid #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,0,0,.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px;cursor:pointer}.wy-dropdown-menu>dd>a:hover{background:#2980b9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:1px solid #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type=search]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-up .wy-dropdown-menu{bottom:100%;top:auto;left:auto;right:0}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980b9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;left:auto;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned .wy-help-inline,.wy-form-aligned input,.wy-form-aligned label,.wy-form-aligned select,.wy-form-aligned textarea{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:6px 12px 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:6px}fieldset{margin:0}fieldset,legend{border:0;padding:0}legend{width:100%;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label,legend{display:block}label{margin:0 0 .3125em;color:#333;font-size:90%}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.wy-control-group{margin-bottom:24px;max-width:1200px;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:after,.wy-control-group:before{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-required>label:after{content:" *";color:#e74c3c}.wy-control-group .wy-form-full,.wy-control-group .wy-form-halves,.wy-control-group .wy-form-thirds{padding-bottom:12px}.wy-control-group .wy-form-full input[type=color],.wy-control-group .wy-form-full input[type=date],.wy-control-group .wy-form-full input[type=datetime-local],.wy-control-group .wy-form-full input[type=datetime],.wy-control-group .wy-form-full input[type=email],.wy-control-group .wy-form-full input[type=month],.wy-control-group .wy-form-full input[type=number],.wy-control-group .wy-form-full input[type=password],.wy-control-group .wy-form-full input[type=search],.wy-control-group .wy-form-full input[type=tel],.wy-control-group .wy-form-full input[type=text],.wy-control-group .wy-form-full input[type=time],.wy-control-group .wy-form-full input[type=url],.wy-control-group .wy-form-full input[type=week],.wy-control-group .wy-form-full select,.wy-control-group .wy-form-halves input[type=color],.wy-control-group .wy-form-halves input[type=date],.wy-control-group .wy-form-halves input[type=datetime-local],.wy-control-group .wy-form-halves input[type=datetime],.wy-control-group .wy-form-halves input[type=email],.wy-control-group .wy-form-halves input[type=month],.wy-control-group .wy-form-halves input[type=number],.wy-control-group .wy-form-halves input[type=password],.wy-control-group .wy-form-halves input[type=search],.wy-control-group .wy-form-halves input[type=tel],.wy-control-group .wy-form-halves input[type=text],.wy-control-group .wy-form-halves input[type=time],.wy-control-group .wy-form-halves input[type=url],.wy-control-group .wy-form-halves input[type=week],.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds input[type=color],.wy-control-group .wy-form-thirds input[type=date],.wy-control-group .wy-form-thirds input[type=datetime-local],.wy-control-group .wy-form-thirds input[type=datetime],.wy-control-group .wy-form-thirds input[type=email],.wy-control-group .wy-form-thirds input[type=month],.wy-control-group .wy-form-thirds input[type=number],.wy-control-group .wy-form-thirds input[type=password],.wy-control-group .wy-form-thirds input[type=search],.wy-control-group .wy-form-thirds input[type=tel],.wy-control-group .wy-form-thirds input[type=text],.wy-control-group .wy-form-thirds input[type=time],.wy-control-group .wy-form-thirds input[type=url],.wy-control-group .wy-form-thirds input[type=week],.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full{float:left;display:block;width:100%;margin-right:0}.wy-control-group .wy-form-full:last-child{margin-right:0}.wy-control-group .wy-form-halves{float:left;display:block;margin-right:2.35765%;width:48.82117%}.wy-control-group .wy-form-halves:last-child,.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(odd){clear:left}.wy-control-group .wy-form-thirds{float:left;display:block;margin-right:2.35765%;width:31.76157%}.wy-control-group .wy-form-thirds:last-child,.wy-control-group .wy-form-thirds:nth-of-type(3n){margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n+1){clear:left}.wy-control-group.wy-control-group-no-input .wy-control,.wy-control-no-input{margin:6px 0 0;font-size:90%}.wy-control-no-input{display:inline-block}.wy-control-group.fluid-input input[type=color],.wy-control-group.fluid-input input[type=date],.wy-control-group.fluid-input input[type=datetime-local],.wy-control-group.fluid-input input[type=datetime],.wy-control-group.fluid-input input[type=email],.wy-control-group.fluid-input input[type=month],.wy-control-group.fluid-input input[type=number],.wy-control-group.fluid-input input[type=password],.wy-control-group.fluid-input input[type=search],.wy-control-group.fluid-input input[type=tel],.wy-control-group.fluid-input input[type=text],.wy-control-group.fluid-input input[type=time],.wy-control-group.fluid-input input[type=url],.wy-control-group.fluid-input input[type=week]{width:100%}.wy-form-message-inline{padding-left:.3em;color:#666;font-size:90%}.wy-form-message{display:block;color:#999;font-size:70%;margin-top:.3125em;font-style:italic}.wy-form-message p{font-size:inherit;font-style:italic;margin-bottom:6px}.wy-form-message p:last-child{margin-bottom:0}input{line-height:normal}input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;*overflow:visible}input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}input[type=datetime-local]{padding:.34375em .625em}input[disabled]{cursor:default}input[type=checkbox],input[type=radio]{padding:0;margin-right:.3125em;*height:13px;*width:13px}input[type=checkbox],input[type=radio],input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}input[type=color]:focus,input[type=date]:focus,input[type=datetime-local]:focus,input[type=datetime]:focus,input[type=email]:focus,input[type=month]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=time]:focus,input[type=url]:focus,input[type=week]:focus{outline:0;outline:thin dotted\9;border-color:#333}input.no-focus:focus{border-color:#ccc!important}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:thin dotted #333;outline:1px auto #129fea}input[type=color][disabled],input[type=date][disabled],input[type=datetime-local][disabled],input[type=datetime][disabled],input[type=email][disabled],input[type=month][disabled],input[type=number][disabled],input[type=password][disabled],input[type=search][disabled],input[type=tel][disabled],input[type=text][disabled],input[type=time][disabled],input[type=url][disabled],input[type=week][disabled]{cursor:not-allowed;background-color:#fafafa}input:focus:invalid,select:focus:invalid,textarea:focus:invalid{color:#e74c3c;border:1px solid #e74c3c}input:focus:invalid:focus,select:focus:invalid:focus,textarea:focus:invalid:focus{border-color:#e74c3c}input[type=checkbox]:focus:invalid:focus,input[type=file]:focus:invalid:focus,input[type=radio]:focus:invalid:focus{outline-color:#e74c3c}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif}select,textarea{padding:.5em .625em;display:inline-block;border:1px solid #ccc;font-size:80%;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}input[readonly],select[disabled],select[readonly],textarea[disabled],textarea[readonly]{cursor:not-allowed;background-color:#fafafa}input[type=checkbox][disabled],input[type=radio][disabled]{cursor:not-allowed}.wy-checkbox,.wy-radio{margin:6px 0;color:#404040;display:block}.wy-checkbox input,.wy-radio input{vertical-align:baseline}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap;padding:6px}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{line-height:27px;padding:0 8px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:1px solid #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-switch{position:relative;display:block;height:24px;margin-top:12px;cursor:pointer}.wy-switch:before{left:0;top:0;width:36px;height:12px;background:#ccc}.wy-switch:after,.wy-switch:before{position:absolute;content:"";display:block;border-radius:4px;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch:after{width:18px;height:18px;background:#999;left:-3px;top:-3px}.wy-switch span{position:absolute;left:48px;display:block;font-size:12px;color:#ccc;line-height:1}.wy-switch.active:before{background:#1e8449}.wy-switch.active:after{left:24px;background:#27ae60}.wy-switch.disabled{cursor:not-allowed;opacity:.8}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error>label{color:#e74c3c}.wy-control-group.wy-control-group-error input[type=color],.wy-control-group.wy-control-group-error input[type=date],.wy-control-group.wy-control-group-error input[type=datetime-local],.wy-control-group.wy-control-group-error input[type=datetime],.wy-control-group.wy-control-group-error input[type=email],.wy-control-group.wy-control-group-error input[type=month],.wy-control-group.wy-control-group-error input[type=number],.wy-control-group.wy-control-group-error input[type=password],.wy-control-group.wy-control-group-error input[type=search],.wy-control-group.wy-control-group-error input[type=tel],.wy-control-group.wy-control-group-error input[type=text],.wy-control-group.wy-control-group-error input[type=time],.wy-control-group.wy-control-group-error input[type=url],.wy-control-group.wy-control-group-error input[type=week],.wy-control-group.wy-control-group-error textarea{border:1px solid #e74c3c}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:.5em .625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27ae60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#e74c3c}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#e67e22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980b9}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}@media only screen and (max-width:480px){.wy-form button[type=submit]{margin:.7em 0 0}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=text],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week],.wy-form label{margin-bottom:.3em;display:block}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-control{margin:1.5em 0 0}.wy-form-message,.wy-form-message-inline,.wy-form .wy-help-inline{display:block;font-size:80%;padding:6px 0}}@media screen and (max-width:768px){.tablet-hide{display:none}}@media screen and (max-width:480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.rst-content table.docutils,.rst-content table.field-list,.wy-table{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.rst-content table.docutils caption,.rst-content table.field-list caption,.wy-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.rst-content table.docutils td,.rst-content table.docutils th,.rst-content table.field-list td,.rst-content table.field-list th,.wy-table td,.wy-table th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.rst-content table.docutils td:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list td:first-child,.rst-content table.field-list th:first-child,.wy-table td:first-child,.wy-table th:first-child{border-left-width:0}.rst-content table.docutils thead,.rst-content table.field-list thead,.wy-table thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.rst-content table.docutils thead th,.rst-content table.field-list thead th,.wy-table thead th{font-weight:700;border-bottom:2px solid #e1e4e5}.rst-content table.docutils td,.rst-content table.field-list td,.wy-table td{background-color:transparent;vertical-align:middle}.rst-content table.docutils td p,.rst-content table.field-list td p,.wy-table td p{line-height:18px}.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child,.wy-table td p:last-child{margin-bottom:0}.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min,.wy-table .wy-table-cell-min{width:1%;padding-right:0}.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:grey;font-size:90%}.wy-table-tertiary{color:grey;font-size:80%}.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td,.wy-table-backed,.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td{background-color:#f3f6f6}.rst-content table.docutils,.wy-table-bordered-all{border:1px solid #e1e4e5}.rst-content table.docutils td,.wy-table-bordered-all td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.rst-content table.docutils tbody>tr:last-child td,.wy-table-bordered-all tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0!important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}a{color:#2980b9;text-decoration:none;cursor:pointer}a:hover{color:#3091d1}a:visited{color:#9b59b6}html{height:100%}body,html{overflow-x:hidden}body{font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-weight:400;color:#404040;min-height:100%;background:#edf0f2}.wy-text-left{text-align:left}.wy-text-center{text-align:center}.wy-text-right{text-align:right}.wy-text-large{font-size:120%}.wy-text-normal{font-size:100%}.wy-text-small,small{font-size:80%}.wy-text-strike{text-decoration:line-through}.wy-text-warning{color:#e67e22!important}a.wy-text-warning:hover{color:#eb9950!important}.wy-text-info{color:#2980b9!important}a.wy-text-info:hover{color:#409ad5!important}.wy-text-success{color:#27ae60!important}a.wy-text-success:hover{color:#36d278!important}.wy-text-danger{color:#e74c3c!important}a.wy-text-danger:hover{color:#ed7669!important}.wy-text-neutral{color:#404040!important}a.wy-text-neutral:hover{color:#595959!important}.rst-content .toctree-wrapper>p.caption,h1,h2,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif}p{line-height:24px;font-size:16px;margin:0 0 24px}h1{font-size:175%}.rst-content .toctree-wrapper>p.caption,h2{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}hr{display:block;height:1px;border:0;border-top:1px solid #e1e4e5;margin:24px 0;padding:0}.rst-content code,.rst-content tt,code{white-space:nowrap;max-width:100%;background:#fff;border:1px solid #e1e4e5;font-size:75%;padding:0 5px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#e74c3c;overflow-x:auto}.rst-content tt.code-large,code.code-large{font-size:90%}.rst-content .section ul,.rst-content .toctree-wrapper ul,.rst-content section ul,.wy-plain-list-disc,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.rst-content .section ul li,.rst-content .toctree-wrapper ul li,.rst-content section ul li,.wy-plain-list-disc li,article ul li{list-style:disc;margin-left:24px}.rst-content .section ul li p:last-child,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li p:last-child,.rst-content .toctree-wrapper ul li ul,.rst-content section ul li p:last-child,.rst-content section ul li ul,.wy-plain-list-disc li p:last-child,.wy-plain-list-disc li ul,article ul li p:last-child,article ul li ul{margin-bottom:0}.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,.rst-content section ul li li,.wy-plain-list-disc li li,article ul li li{list-style:circle}.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,.rst-content section ul li li li,.wy-plain-list-disc li li li,article ul li li li{list-style:square}.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,.rst-content section ul li ol li,.wy-plain-list-disc li ol li,article ul li ol li{list-style:decimal}.rst-content .section ol,.rst-content .section ol.arabic,.rst-content .toctree-wrapper ol,.rst-content .toctree-wrapper ol.arabic,.rst-content section ol,.rst-content section ol.arabic,.wy-plain-list-decimal,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.rst-content .section ol.arabic li,.rst-content .section ol li,.rst-content .toctree-wrapper ol.arabic li,.rst-content .toctree-wrapper ol li,.rst-content section ol.arabic li,.rst-content section ol li,.wy-plain-list-decimal li,article ol li{list-style:decimal;margin-left:24px}.rst-content .section ol.arabic li ul,.rst-content .section ol li p:last-child,.rst-content .section ol li ul,.rst-content .toctree-wrapper ol.arabic li ul,.rst-content .toctree-wrapper ol li p:last-child,.rst-content .toctree-wrapper ol li ul,.rst-content section ol.arabic li ul,.rst-content section ol li p:last-child,.rst-content section ol li ul,.wy-plain-list-decimal li p:last-child,.wy-plain-list-decimal li ul,article ol li p:last-child,article ol li ul{margin-bottom:0}.rst-content .section ol.arabic li ul li,.rst-content .section ol li ul li,.rst-content .toctree-wrapper ol.arabic li ul li,.rst-content .toctree-wrapper ol li ul li,.rst-content section ol.arabic li ul li,.rst-content section ol li ul li,.wy-plain-list-decimal li ul li,article ol li ul li{list-style:disc}.wy-breadcrumbs{*zoom:1}.wy-breadcrumbs:after,.wy-breadcrumbs:before{display:table;content:""}.wy-breadcrumbs:after{clear:both}.wy-breadcrumbs>li{display:inline-block;padding-top:5px}.wy-breadcrumbs>li.wy-breadcrumbs-aside{float:right}.rst-content .wy-breadcrumbs>li code,.rst-content .wy-breadcrumbs>li tt,.wy-breadcrumbs>li .rst-content tt,.wy-breadcrumbs>li code{all:inherit;color:inherit}.breadcrumb-item:before{content:"/";color:#bbb;font-size:13px;padding:0 6px 0 3px}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width:480px){.wy-breadcrumbs-extra,.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}html{font-size:16px}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:after,.wy-menu-horiz:before{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz li,.wy-menu-horiz ul{display:inline-block}.wy-menu-horiz li:hover{background:hsla(0,0%,100%,.1)}.wy-menu-horiz li.divide-left{border-left:1px solid #404040}.wy-menu-horiz li.divide-right{border-right:1px solid #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical{width:300px}.wy-menu-vertical header,.wy-menu-vertical p.caption{color:#55a5d9;height:32px;line-height:32px;padding:0 1.618em;margin:12px 0 0;display:block;font-weight:700;text-transform:uppercase;font-size:85%;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:1px solid #404040}.wy-menu-vertical li.divide-bottom{border-bottom:1px solid #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:grey;border-right:1px solid #c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.rst-content .wy-menu-vertical li tt,.wy-menu-vertical li .rst-content tt,.wy-menu-vertical li code{border:none;background:inherit;color:inherit;padding-left:0;padding-right:0}.wy-menu-vertical li button.toctree-expand{display:block;float:left;margin-left:-1.2em;line-height:18px;color:#4d4d4d;border:none;background:none;padding:0}.wy-menu-vertical li.current>a,.wy-menu-vertical li.on a{color:#404040;font-weight:700;position:relative;background:#fcfcfc;border:none;padding:.4045em 1.618em}.wy-menu-vertical li.current>a:hover,.wy-menu-vertical li.on a:hover{background:#fcfcfc}.wy-menu-vertical li.current>a:hover button.toctree-expand,.wy-menu-vertical li.on a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand{display:block;line-height:18px;color:#333}.wy-menu-vertical li.toctree-l1.current>a{border-bottom:1px solid #c9c9c9;border-top:1px solid #c9c9c9}.wy-menu-vertical .toctree-l1.current .toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .toctree-l11>ul{display:none}.wy-menu-vertical .toctree-l1.current .current.toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .current.toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .current.toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .current.toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .current.toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .current.toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .current.toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .current.toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .current.toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .current.toctree-l11>ul{display:block}.wy-menu-vertical li.toctree-l3,.wy-menu-vertical li.toctree-l4{font-size:.9em}.wy-menu-vertical li.toctree-l2 a,.wy-menu-vertical li.toctree-l3 a,.wy-menu-vertical li.toctree-l4 a,.wy-menu-vertical li.toctree-l5 a,.wy-menu-vertical li.toctree-l6 a,.wy-menu-vertical li.toctree-l7 a,.wy-menu-vertical li.toctree-l8 a,.wy-menu-vertical li.toctree-l9 a,.wy-menu-vertical li.toctree-l10 a{color:#404040}.wy-menu-vertical li.toctree-l2 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l3 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l4 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l5 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l6 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l7 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l8 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l9 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l10 a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a,.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a,.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a,.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a,.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a,.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a,.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a,.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{display:block}.wy-menu-vertical li.toctree-l2.current>a{padding:.4045em 2.427em}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{padding:.4045em 1.618em .4045em 4.045em}.wy-menu-vertical li.toctree-l3.current>a{padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{padding:.4045em 1.618em .4045em 5.663em}.wy-menu-vertical li.toctree-l4.current>a{padding:.4045em 5.663em}.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a{padding:.4045em 1.618em .4045em 7.281em}.wy-menu-vertical li.toctree-l5.current>a{padding:.4045em 7.281em}.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a{padding:.4045em 1.618em .4045em 8.899em}.wy-menu-vertical li.toctree-l6.current>a{padding:.4045em 8.899em}.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a{padding:.4045em 1.618em .4045em 10.517em}.wy-menu-vertical li.toctree-l7.current>a{padding:.4045em 10.517em}.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a{padding:.4045em 1.618em .4045em 12.135em}.wy-menu-vertical li.toctree-l8.current>a{padding:.4045em 12.135em}.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a{padding:.4045em 1.618em .4045em 13.753em}.wy-menu-vertical li.toctree-l9.current>a{padding:.4045em 13.753em}.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a{padding:.4045em 1.618em .4045em 15.371em}.wy-menu-vertical li.toctree-l10.current>a{padding:.4045em 15.371em}.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{padding:.4045em 1.618em .4045em 16.989em}.wy-menu-vertical li.toctree-l2.current>a,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{background:#c9c9c9}.wy-menu-vertical li.toctree-l2 button.toctree-expand{color:#a3a3a3}.wy-menu-vertical li.toctree-l3.current>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{background:#bdbdbd}.wy-menu-vertical li.toctree-l3 button.toctree-expand{color:#969696}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical li ul li a{margin-bottom:0;color:#d9d9d9;font-weight:400}.wy-menu-vertical a{line-height:18px;padding:.4045em 1.618em;display:block;position:relative;font-size:90%;color:#d9d9d9}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:hover button.toctree-expand{color:#d9d9d9}.wy-menu-vertical a:active{background-color:#2980b9;cursor:pointer;color:#fff}.wy-menu-vertical a:active button.toctree-expand{color:#fff}.wy-side-nav-search{display:block;width:300px;padding:.809em;margin-bottom:.809em;z-index:200;background-color:#2980b9;text-align:center;color:#fcfcfc}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto .809em;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-side-nav-search .wy-dropdown>a,.wy-side-nav-search>a{color:#fcfcfc;font-size:100%;font-weight:700;display:inline-block;padding:4px 6px;margin-bottom:.809em;max-width:100%}.wy-side-nav-search .wy-dropdown>a:hover,.wy-side-nav-search>a:hover{background:hsla(0,0%,100%,.1)}.wy-side-nav-search .wy-dropdown>a img.logo,.wy-side-nav-search>a img.logo{display:block;margin:0 auto;height:auto;width:auto;border-radius:0;max-width:100%;background:transparent}.wy-side-nav-search .wy-dropdown>a.icon img.logo,.wy-side-nav-search>a.icon img.logo{margin-top:.85em}.wy-side-nav-search>div.version{margin-top:-.4045em;margin-bottom:.809em;font-weight:400;color:hsla(0,0%,100%,.3)}.wy-nav .wy-menu-vertical header{color:#2980b9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980b9;color:#fff}[data-menu-wrap]{-webkit-transition:all .2s ease-in;-moz-transition:all .2s ease-in;transition:all .2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:#fcfcfc}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:fixed;top:0;bottom:0;left:0;padding-bottom:2em;width:300px;overflow-x:hidden;overflow-y:hidden;min-height:100%;color:#9b9b9b;background:#343131;z-index:200}.wy-side-scroll{width:320px;position:relative;overflow-x:hidden;overflow-y:scroll;height:100%}.wy-nav-top{display:none;background:#2980b9;color:#fff;padding:.4045em .809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:after,.wy-nav-top:before{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:700}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer;padding-top:inherit}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:grey}footer p{margin-bottom:12px}.rst-content footer span.commit tt,footer span.commit .rst-content tt,footer span.commit code{padding:0;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:1em;background:none;border:none;color:grey}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:after,.rst-footer-buttons:before{width:100%;display:table;content:""}.rst-footer-buttons:after{clear:both}.rst-breadcrumbs-buttons{margin-top:12px;*zoom:1}.rst-breadcrumbs-buttons:after,.rst-breadcrumbs-buttons:before{display:table;content:""}.rst-breadcrumbs-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:1px solid #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:1px solid #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:grey;font-size:90%}.genindextable li>ul{margin-left:24px}@media screen and (max-width:768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-menu.wy-menu-vertical,.wy-side-nav-search,.wy-side-scroll{width:auto}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width:1100px){.wy-nav-content-wrap{background:rgba(0,0,0,.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.rst-versions,.wy-nav-side,footer{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60;*zoom:1}.rst-versions .rst-current-version:after,.rst-versions .rst-current-version:before{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-content .code-block-caption .rst-versions .rst-current-version .headerlink,.rst-content .eqno .rst-versions .rst-current-version .headerlink,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-content p .rst-versions .rst-current-version .headerlink,.rst-content table>caption .rst-versions .rst-current-version .headerlink,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .icon,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-versions .rst-current-version .rst-content .code-block-caption .headerlink,.rst-versions .rst-current-version .rst-content .eqno .headerlink,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-versions .rst-current-version .rst-content p .headerlink,.rst-versions .rst-current-version .rst-content table>caption .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-versions .rst-current-version .wy-menu-vertical li button.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version button.toctree-expand{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}.rst-content .toctree-wrapper>p.caption,.rst-content h1,.rst-content h2,.rst-content h3,.rst-content h4,.rst-content h5,.rst-content h6{margin-bottom:24px}.rst-content img{max-width:100%;height:auto}.rst-content div.figure,.rst-content figure{margin-bottom:24px}.rst-content div.figure .caption-text,.rst-content figure .caption-text{font-style:italic}.rst-content div.figure p:last-child.caption,.rst-content figure p:last-child.caption{margin-bottom:0}.rst-content div.figure.align-center,.rst-content figure.align-center{text-align:center}.rst-content .section>a>img,.rst-content .section>img,.rst-content section>a>img,.rst-content section>img{margin-bottom:24px}.rst-content abbr[title]{text-decoration:none}.rst-content.style-external-links a.reference.external:after{font-family:FontAwesome;content:"\f08e";color:#b3b3b3;vertical-align:super;font-size:60%;margin:0 .2em}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content pre.literal-block{white-space:pre;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;display:block;overflow:auto}.rst-content div[class^=highlight],.rst-content pre.literal-block{border:1px solid #e1e4e5;overflow-x:auto;margin:1px 0 24px}.rst-content div[class^=highlight] div[class^=highlight],.rst-content pre.literal-block div[class^=highlight]{padding:0;border:none;margin:0}.rst-content div[class^=highlight] td.code{width:100%}.rst-content .linenodiv pre{border-right:1px solid #e6e9ea;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;user-select:none;pointer-events:none}.rst-content div[class^=highlight] pre{white-space:pre;margin:0;padding:12px;display:block;overflow:auto}.rst-content div[class^=highlight] pre .hll{display:block;margin:0 -12px;padding:0 12px}.rst-content .linenodiv pre,.rst-content div[class^=highlight] pre,.rst-content pre.literal-block{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:12px;line-height:1.4}.rst-content div.highlight .gp,.rst-content div.highlight span.linenos{user-select:none;pointer-events:none}.rst-content div.highlight span.linenos{display:inline-block;padding-left:0;padding-right:12px;margin-right:12px;border-right:1px solid #e6e9ea}.rst-content .code-block-caption{font-style:italic;font-size:85%;line-height:1;padding:1em 0;text-align:center}@media print{.rst-content .codeblock,.rst-content div[class^=highlight],.rst-content div[class^=highlight] pre{white-space:pre-wrap}}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning{clear:both}.rst-content .admonition-todo .last,.rst-content .admonition-todo>:last-child,.rst-content .admonition .last,.rst-content .admonition>:last-child,.rst-content .attention .last,.rst-content .attention>:last-child,.rst-content .caution .last,.rst-content .caution>:last-child,.rst-content .danger .last,.rst-content .danger>:last-child,.rst-content .error .last,.rst-content .error>:last-child,.rst-content .hint .last,.rst-content .hint>:last-child,.rst-content .important .last,.rst-content .important>:last-child,.rst-content .note .last,.rst-content .note>:last-child,.rst-content .seealso .last,.rst-content .seealso>:last-child,.rst-content .tip .last,.rst-content .tip>:last-child,.rst-content .warning .last,.rst-content .warning>:last-child{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent!important;border-color:rgba(0,0,0,.1)!important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha>li,.rst-content .toctree-wrapper ol.loweralpha,.rst-content .toctree-wrapper ol.loweralpha>li,.rst-content section ol.loweralpha,.rst-content section ol.loweralpha>li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha>li,.rst-content .toctree-wrapper ol.upperalpha,.rst-content .toctree-wrapper ol.upperalpha>li,.rst-content section ol.upperalpha,.rst-content section ol.upperalpha>li{list-style:upper-alpha}.rst-content .section ol li>*,.rst-content .section ul li>*,.rst-content .toctree-wrapper ol li>*,.rst-content .toctree-wrapper ul li>*,.rst-content section ol li>*,.rst-content section ul li>*{margin-top:12px;margin-bottom:12px}.rst-content .section ol li>:first-child,.rst-content .section ul li>:first-child,.rst-content .toctree-wrapper ol li>:first-child,.rst-content .toctree-wrapper ul li>:first-child,.rst-content section ol li>:first-child,.rst-content section ul li>:first-child{margin-top:0}.rst-content .section ol li>p,.rst-content .section ol li>p:last-child,.rst-content .section ul li>p,.rst-content .section ul li>p:last-child,.rst-content .toctree-wrapper ol li>p,.rst-content .toctree-wrapper ol li>p:last-child,.rst-content .toctree-wrapper ul li>p,.rst-content .toctree-wrapper ul li>p:last-child,.rst-content section ol li>p,.rst-content section ol li>p:last-child,.rst-content section ul li>p,.rst-content section ul li>p:last-child{margin-bottom:12px}.rst-content .section ol li>p:only-child,.rst-content .section ol li>p:only-child:last-child,.rst-content .section ul li>p:only-child,.rst-content .section ul li>p:only-child:last-child,.rst-content .toctree-wrapper ol li>p:only-child,.rst-content .toctree-wrapper ol li>p:only-child:last-child,.rst-content .toctree-wrapper ul li>p:only-child,.rst-content .toctree-wrapper ul li>p:only-child:last-child,.rst-content section ol li>p:only-child,.rst-content section ol li>p:only-child:last-child,.rst-content section ul li>p:only-child,.rst-content section ul li>p:only-child:last-child{margin-bottom:0}.rst-content .section ol li>ol,.rst-content .section ol li>ul,.rst-content .section ul li>ol,.rst-content .section ul li>ul,.rst-content .toctree-wrapper ol li>ol,.rst-content .toctree-wrapper ol li>ul,.rst-content .toctree-wrapper ul li>ol,.rst-content .toctree-wrapper ul li>ul,.rst-content section ol li>ol,.rst-content section ol li>ul,.rst-content section ul li>ol,.rst-content section ul li>ul{margin-bottom:12px}.rst-content .section ol.simple li>*,.rst-content .section ol.simple li ol,.rst-content .section ol.simple li ul,.rst-content .section ul.simple li>*,.rst-content .section ul.simple li ol,.rst-content .section ul.simple li ul,.rst-content .toctree-wrapper ol.simple li>*,.rst-content .toctree-wrapper ol.simple li ol,.rst-content .toctree-wrapper ol.simple li ul,.rst-content .toctree-wrapper ul.simple li>*,.rst-content .toctree-wrapper ul.simple li ol,.rst-content .toctree-wrapper ul.simple li ul,.rst-content section ol.simple li>*,.rst-content section ol.simple li ol,.rst-content section ol.simple li ul,.rst-content section ul.simple li>*,.rst-content section ul.simple li ol,.rst-content section ul.simple li ul{margin-top:0;margin-bottom:0}.rst-content .line-block{margin-left:0;margin-bottom:24px;line-height:24px}.rst-content .line-block .line-block{margin-left:24px;margin-bottom:0}.rst-content .topic-title{font-weight:700;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0 0 24px 24px}.rst-content .align-left{float:left;margin:0 24px 24px 0}.rst-content .align-center{margin:auto}.rst-content .align-center:not(table){display:block}.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink{opacity:0;font-size:14px;font-family:FontAwesome;margin-left:.5em}.rst-content .code-block-caption .headerlink:focus,.rst-content .code-block-caption:hover .headerlink,.rst-content .eqno .headerlink:focus,.rst-content .eqno:hover .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink:focus,.rst-content .toctree-wrapper>p.caption:hover .headerlink,.rst-content dl dt .headerlink:focus,.rst-content dl dt:hover .headerlink,.rst-content h1 .headerlink:focus,.rst-content h1:hover .headerlink,.rst-content h2 .headerlink:focus,.rst-content h2:hover .headerlink,.rst-content h3 .headerlink:focus,.rst-content h3:hover .headerlink,.rst-content h4 .headerlink:focus,.rst-content h4:hover .headerlink,.rst-content h5 .headerlink:focus,.rst-content h5:hover .headerlink,.rst-content h6 .headerlink:focus,.rst-content h6:hover .headerlink,.rst-content p.caption .headerlink:focus,.rst-content p.caption:hover .headerlink,.rst-content p .headerlink:focus,.rst-content p:hover .headerlink,.rst-content table>caption .headerlink:focus,.rst-content table>caption:hover .headerlink{opacity:1}.rst-content p a{overflow-wrap:anywhere}.rst-content .wy-table td p,.rst-content .wy-table td ul,.rst-content .wy-table th p,.rst-content .wy-table th ul,.rst-content table.docutils td p,.rst-content table.docutils td ul,.rst-content table.docutils th p,.rst-content table.docutils th ul,.rst-content table.field-list td p,.rst-content table.field-list td ul,.rst-content table.field-list th p,.rst-content table.field-list th ul{font-size:inherit}.rst-content .btn:focus{outline:2px solid}.rst-content table>caption .headerlink:after{font-size:12px}.rst-content .centered{text-align:center}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:1px solid #e1e4e5}.rst-content .sidebar dl,.rst-content .sidebar p,.rst-content .sidebar ul{font-size:90%}.rst-content .sidebar .last,.rst-content .sidebar>:last-child{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif;font-weight:700;background:#e1e4e5;padding:6px 12px;margin:-24px -24px 24px;font-size:100%}.rst-content .highlighted{background:#f1c40f;box-shadow:0 0 0 2px #f1c40f;display:inline;font-weight:700}.rst-content .citation-reference,.rst-content .footnote-reference{vertical-align:baseline;position:relative;top:-.4em;line-height:0;font-size:90%}.rst-content .hlist{width:100%}.rst-content dl dt span.classifier:before{content:" : "}.rst-content dl dt span.classifier-delimiter{display:none!important}html.writer-html4 .rst-content table.docutils.citation,html.writer-html4 .rst-content table.docutils.footnote{background:none;border:none}html.writer-html4 .rst-content table.docutils.citation td,html.writer-html4 .rst-content table.docutils.citation tr,html.writer-html4 .rst-content table.docutils.footnote td,html.writer-html4 .rst-content table.docutils.footnote tr{border:none;background-color:transparent!important;white-space:normal}html.writer-html4 .rst-content table.docutils.citation td.label,html.writer-html4 .rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{display:grid;grid-template-columns:max-content auto}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{padding-left:1rem}html.writer-html5 .rst-content dl.citation>dt:after,html.writer-html5 .rst-content dl.field-list>dt:after,html.writer-html5 .rst-content dl.footnote>dt:after{content:":"}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dd,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dd,html.writer-html5 .rst-content dl.footnote>dt{margin-bottom:0}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{font-size:.9rem}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.footnote>dt{margin:0 .5rem .5rem 0;line-height:1.2rem;word-break:break-all;font-weight:400}html.writer-html5 .rst-content dl.citation>dt>span.brackets,html.writer-html5 .rst-content dl.footnote>dt>span.brackets{margin-right:.5rem}html.writer-html5 .rst-content dl.citation>dt>span.brackets:before,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:before{content:"["}html.writer-html5 .rst-content dl.citation>dt>span.brackets:after,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:after{content:"]"}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref{font-style:italic}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.footnote>dd{margin:0 0 .5rem;line-height:1.2rem}html.writer-html5 .rst-content dl.citation>dd p,html.writer-html5 .rst-content dl.footnote>dd p,html.writer-html5 .rst-content dl.option-list kbd{font-size:.9rem}.rst-content dl.citation,.rst-content table.docutils.footnote,html.writer-html4 .rst-content table.docutils.citation,html.writer-html5 .rst-content dl.footnote{color:grey}.rst-content dl.citation code,.rst-content dl.citation tt,.rst-content table.docutils.footnote code,.rst-content table.docutils.footnote tt,html.writer-html4 .rst-content table.docutils.citation code,html.writer-html4 .rst-content table.docutils.citation tt,html.writer-html5 .rst-content dl.footnote code,html.writer-html5 .rst-content dl.footnote tt{color:#555}.rst-content .wy-table-responsive.citation,.rst-content .wy-table-responsive.footnote{margin-bottom:0}.rst-content .wy-table-responsive.citation+:not(.citation),.rst-content .wy-table-responsive.footnote+:not(.footnote){margin-top:24px}.rst-content .wy-table-responsive.citation:last-child,.rst-content .wy-table-responsive.footnote:last-child{margin-bottom:24px}.rst-content table.docutils th{border-color:#e1e4e5}html.writer-html5 .rst-content table.docutils th{border:1px solid #e1e4e5}html.writer-html5 .rst-content table.docutils td>p,html.writer-html5 .rst-content table.docutils th>p{line-height:1rem;margin-bottom:0;font-size:.9rem}.rst-content table.docutils td .last,.rst-content table.docutils td .last>:last-child{margin-bottom:0}.rst-content table.field-list,.rst-content table.field-list td{border:none}.rst-content table.field-list td p{line-height:inherit}.rst-content table.field-list td>strong{display:inline-block}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left}.rst-content code,.rst-content tt{color:#000;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;padding:2px 5px}.rst-content code big,.rst-content code em,.rst-content tt big,.rst-content tt em{font-size:100%!important;line-height:normal}.rst-content code.literal,.rst-content tt.literal{color:#e74c3c;white-space:normal}.rst-content code.xref,.rst-content tt.xref,a .rst-content code,a .rst-content tt{font-weight:700;color:#404040;overflow-wrap:normal}.rst-content kbd,.rst-content pre,.rst-content samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace}.rst-content a code,.rst-content a tt{color:#2980b9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:700;margin-bottom:12px}.rst-content dl ol,.rst-content dl p,.rst-content dl table,.rst-content dl ul{margin-bottom:12px}.rst-content dl dd{margin:0 0 12px 24px;line-height:24px}.rst-content dl dd>ol:last-child,.rst-content dl dd>p:last-child,.rst-content dl dd>table:last-child,.rst-content dl dd>ul:last-child{margin-bottom:0}html.writer-html4 .rst-content dl:not(.docutils),html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple){margin-bottom:24px}html.writer-html4 .rst-content dl:not(.docutils)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{display:table;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980b9;border-top:3px solid #6ab0de;padding:6px;position:relative}html.writer-html4 .rst-content dl:not(.docutils)>dt:before,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:before{color:#6ab0de}html.writer-html4 .rst-content dl:not(.docutils)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{margin-bottom:6px;border:none;border-left:3px solid #ccc;background:#f0f0f0;color:#555}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils)>dt:first-child,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:first-child{margin-top:0}html.writer-html4 .rst-content dl:not(.docutils) code.descclassname,html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descclassname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{background-color:transparent;border:none;padding:0;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .optional,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .property,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .property{display:inline-block;padding-right:8px;max-width:100%}html.writer-html4 .rst-content dl:not(.docutils) .k,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .k{font-style:italic}html.writer-html4 .rst-content dl:not(.docutils) .descclassname,html.writer-html4 .rst-content dl:not(.docutils) .descname,html.writer-html4 .rst-content dl:not(.docutils) .sig-name,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .sig-name{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#000}.rst-content .viewcode-back,.rst-content .viewcode-link{display:inline-block;color:#27ae60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}.rst-content p.rubric{margin-bottom:12px;font-weight:700}.rst-content code.download,.rst-content tt.download{background:inherit;padding:inherit;font-weight:400;font-family:inherit;font-size:inherit;color:inherit;border:inherit;white-space:inherit}.rst-content code.download span:first-child,.rst-content tt.download span:first-child{-webkit-font-smoothing:subpixel-antialiased}.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{margin-right:4px}.rst-content .guilabel{border:1px solid #7fbbe3;background:#e7f2fa;font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>.kbd,.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>kbd{color:inherit;font-size:80%;background-color:#fff;border:1px solid #a6a6a6;border-radius:4px;box-shadow:0 2px grey;padding:2.4px 6px;margin:auto 0}.rst-content .versionmodified{font-style:italic}@media screen and (max-width:480px){.rst-content .sidebar{width:100%}}span[id*=MathJax-Span]{color:#404040}.math{text-align:center}@font-face{font-family:Lato;src:url(fonts/lato-normal.woff2?bd03a2cc277bbbc338d464e679fe9942) format("woff2"),url(fonts/lato-normal.woff?27bd77b9162d388cb8d4c4217c7c5e2a) format("woff");font-weight:400;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold.woff2?cccb897485813c7c256901dbca54ecf2) format("woff2"),url(fonts/lato-bold.woff?d878b6c29b10beca227e9eef4246111b) format("woff");font-weight:700;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold-italic.woff2?0b6bb6725576b072c5d0b02ecdd1900d) format("woff2"),url(fonts/lato-bold-italic.woff?9c7e4e9eb485b4a121c760e61bc3707c) format("woff");font-weight:700;font-style:italic;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-normal-italic.woff2?4eb103b4d12be57cb1d040ed5e162e9d) format("woff2"),url(fonts/lato-normal-italic.woff?f28f2d6482446544ef1ea1ccc6dd5892) format("woff");font-weight:400;font-style:italic;font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:400;src:url(fonts/Roboto-Slab-Regular.woff2?7abf5b8d04d26a2cafea937019bca958) format("woff2"),url(fonts/Roboto-Slab-Regular.woff?c1be9284088d487c5e3ff0a10a92e58c) format("woff");font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:700;src:url(fonts/Roboto-Slab-Bold.woff2?9984f4a9bda09be08e83f2506954adbe) format("woff2"),url(fonts/Roboto-Slab-Bold.woff?bed5564a116b05148e3b3bea6fb1162a) format("woff");font-display:block} \ No newline at end of file diff --git a/docs/docs_build/_static/customStyles.css b/docs/docs_build/_static/customStyles.css deleted file mode 100644 index 45063a6..0000000 --- a/docs/docs_build/_static/customStyles.css +++ /dev/null @@ -1,5 +0,0 @@ -@import url("css/theme.css"); - -.wy-nav-content { - max-width: 90%; -} diff --git a/docs/docs_build/_static/design-style.4045f2051d55cab465a707391d5b2007.min.css b/docs/docs_build/_static/design-style.4045f2051d55cab465a707391d5b2007.min.css deleted file mode 100644 index 3225661..0000000 --- a/docs/docs_build/_static/design-style.4045f2051d55cab465a707391d5b2007.min.css +++ /dev/null @@ -1 +0,0 @@ -.sd-bg-primary{background-color:var(--sd-color-primary) !important}.sd-bg-text-primary{color:var(--sd-color-primary-text) !important}button.sd-bg-primary:focus,button.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}a.sd-bg-primary:focus,a.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}.sd-bg-secondary{background-color:var(--sd-color-secondary) !important}.sd-bg-text-secondary{color:var(--sd-color-secondary-text) !important}button.sd-bg-secondary:focus,button.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}a.sd-bg-secondary:focus,a.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}.sd-bg-success{background-color:var(--sd-color-success) !important}.sd-bg-text-success{color:var(--sd-color-success-text) !important}button.sd-bg-success:focus,button.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}a.sd-bg-success:focus,a.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}.sd-bg-info{background-color:var(--sd-color-info) !important}.sd-bg-text-info{color:var(--sd-color-info-text) !important}button.sd-bg-info:focus,button.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}a.sd-bg-info:focus,a.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}.sd-bg-warning{background-color:var(--sd-color-warning) !important}.sd-bg-text-warning{color:var(--sd-color-warning-text) !important}button.sd-bg-warning:focus,button.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}a.sd-bg-warning:focus,a.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}.sd-bg-danger{background-color:var(--sd-color-danger) !important}.sd-bg-text-danger{color:var(--sd-color-danger-text) !important}button.sd-bg-danger:focus,button.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}a.sd-bg-danger:focus,a.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}.sd-bg-light{background-color:var(--sd-color-light) !important}.sd-bg-text-light{color:var(--sd-color-light-text) !important}button.sd-bg-light:focus,button.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}a.sd-bg-light:focus,a.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}.sd-bg-muted{background-color:var(--sd-color-muted) !important}.sd-bg-text-muted{color:var(--sd-color-muted-text) !important}button.sd-bg-muted:focus,button.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}a.sd-bg-muted:focus,a.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}.sd-bg-dark{background-color:var(--sd-color-dark) !important}.sd-bg-text-dark{color:var(--sd-color-dark-text) !important}button.sd-bg-dark:focus,button.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}a.sd-bg-dark:focus,a.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}.sd-bg-black{background-color:var(--sd-color-black) !important}.sd-bg-text-black{color:var(--sd-color-black-text) !important}button.sd-bg-black:focus,button.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}a.sd-bg-black:focus,a.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}.sd-bg-white{background-color:var(--sd-color-white) !important}.sd-bg-text-white{color:var(--sd-color-white-text) !important}button.sd-bg-white:focus,button.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}a.sd-bg-white:focus,a.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}.sd-text-primary,.sd-text-primary>p{color:var(--sd-color-primary) !important}a.sd-text-primary:focus,a.sd-text-primary:hover{color:var(--sd-color-primary-highlight) !important}.sd-text-secondary,.sd-text-secondary>p{color:var(--sd-color-secondary) !important}a.sd-text-secondary:focus,a.sd-text-secondary:hover{color:var(--sd-color-secondary-highlight) !important}.sd-text-success,.sd-text-success>p{color:var(--sd-color-success) !important}a.sd-text-success:focus,a.sd-text-success:hover{color:var(--sd-color-success-highlight) !important}.sd-text-info,.sd-text-info>p{color:var(--sd-color-info) !important}a.sd-text-info:focus,a.sd-text-info:hover{color:var(--sd-color-info-highlight) !important}.sd-text-warning,.sd-text-warning>p{color:var(--sd-color-warning) !important}a.sd-text-warning:focus,a.sd-text-warning:hover{color:var(--sd-color-warning-highlight) !important}.sd-text-danger,.sd-text-danger>p{color:var(--sd-color-danger) !important}a.sd-text-danger:focus,a.sd-text-danger:hover{color:var(--sd-color-danger-highlight) !important}.sd-text-light,.sd-text-light>p{color:var(--sd-color-light) !important}a.sd-text-light:focus,a.sd-text-light:hover{color:var(--sd-color-light-highlight) !important}.sd-text-muted,.sd-text-muted>p{color:var(--sd-color-muted) !important}a.sd-text-muted:focus,a.sd-text-muted:hover{color:var(--sd-color-muted-highlight) !important}.sd-text-dark,.sd-text-dark>p{color:var(--sd-color-dark) !important}a.sd-text-dark:focus,a.sd-text-dark:hover{color:var(--sd-color-dark-highlight) !important}.sd-text-black,.sd-text-black>p{color:var(--sd-color-black) !important}a.sd-text-black:focus,a.sd-text-black:hover{color:var(--sd-color-black-highlight) !important}.sd-text-white,.sd-text-white>p{color:var(--sd-color-white) !important}a.sd-text-white:focus,a.sd-text-white:hover{color:var(--sd-color-white-highlight) !important}.sd-outline-primary{border-color:var(--sd-color-primary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-primary:focus,a.sd-outline-primary:hover{border-color:var(--sd-color-primary-highlight) !important}.sd-outline-secondary{border-color:var(--sd-color-secondary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-secondary:focus,a.sd-outline-secondary:hover{border-color:var(--sd-color-secondary-highlight) !important}.sd-outline-success{border-color:var(--sd-color-success) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-success:focus,a.sd-outline-success:hover{border-color:var(--sd-color-success-highlight) !important}.sd-outline-info{border-color:var(--sd-color-info) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-info:focus,a.sd-outline-info:hover{border-color:var(--sd-color-info-highlight) !important}.sd-outline-warning{border-color:var(--sd-color-warning) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-warning:focus,a.sd-outline-warning:hover{border-color:var(--sd-color-warning-highlight) !important}.sd-outline-danger{border-color:var(--sd-color-danger) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-danger:focus,a.sd-outline-danger:hover{border-color:var(--sd-color-danger-highlight) !important}.sd-outline-light{border-color:var(--sd-color-light) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-light:focus,a.sd-outline-light:hover{border-color:var(--sd-color-light-highlight) !important}.sd-outline-muted{border-color:var(--sd-color-muted) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-muted:focus,a.sd-outline-muted:hover{border-color:var(--sd-color-muted-highlight) !important}.sd-outline-dark{border-color:var(--sd-color-dark) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-dark:focus,a.sd-outline-dark:hover{border-color:var(--sd-color-dark-highlight) !important}.sd-outline-black{border-color:var(--sd-color-black) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-black:focus,a.sd-outline-black:hover{border-color:var(--sd-color-black-highlight) !important}.sd-outline-white{border-color:var(--sd-color-white) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-white:focus,a.sd-outline-white:hover{border-color:var(--sd-color-white-highlight) !important}.sd-bg-transparent{background-color:transparent !important}.sd-outline-transparent{border-color:transparent !important}.sd-text-transparent{color:transparent !important}.sd-p-0{padding:0 !important}.sd-pt-0,.sd-py-0{padding-top:0 !important}.sd-pr-0,.sd-px-0{padding-right:0 !important}.sd-pb-0,.sd-py-0{padding-bottom:0 !important}.sd-pl-0,.sd-px-0{padding-left:0 !important}.sd-p-1{padding:.25rem !important}.sd-pt-1,.sd-py-1{padding-top:.25rem !important}.sd-pr-1,.sd-px-1{padding-right:.25rem !important}.sd-pb-1,.sd-py-1{padding-bottom:.25rem !important}.sd-pl-1,.sd-px-1{padding-left:.25rem !important}.sd-p-2{padding:.5rem !important}.sd-pt-2,.sd-py-2{padding-top:.5rem !important}.sd-pr-2,.sd-px-2{padding-right:.5rem !important}.sd-pb-2,.sd-py-2{padding-bottom:.5rem !important}.sd-pl-2,.sd-px-2{padding-left:.5rem !important}.sd-p-3{padding:1rem !important}.sd-pt-3,.sd-py-3{padding-top:1rem !important}.sd-pr-3,.sd-px-3{padding-right:1rem !important}.sd-pb-3,.sd-py-3{padding-bottom:1rem !important}.sd-pl-3,.sd-px-3{padding-left:1rem !important}.sd-p-4{padding:1.5rem !important}.sd-pt-4,.sd-py-4{padding-top:1.5rem !important}.sd-pr-4,.sd-px-4{padding-right:1.5rem !important}.sd-pb-4,.sd-py-4{padding-bottom:1.5rem !important}.sd-pl-4,.sd-px-4{padding-left:1.5rem !important}.sd-p-5{padding:3rem !important}.sd-pt-5,.sd-py-5{padding-top:3rem !important}.sd-pr-5,.sd-px-5{padding-right:3rem !important}.sd-pb-5,.sd-py-5{padding-bottom:3rem !important}.sd-pl-5,.sd-px-5{padding-left:3rem !important}.sd-m-auto{margin:auto !important}.sd-mt-auto,.sd-my-auto{margin-top:auto !important}.sd-mr-auto,.sd-mx-auto{margin-right:auto !important}.sd-mb-auto,.sd-my-auto{margin-bottom:auto !important}.sd-ml-auto,.sd-mx-auto{margin-left:auto !important}.sd-m-0{margin:0 !important}.sd-mt-0,.sd-my-0{margin-top:0 !important}.sd-mr-0,.sd-mx-0{margin-right:0 !important}.sd-mb-0,.sd-my-0{margin-bottom:0 !important}.sd-ml-0,.sd-mx-0{margin-left:0 !important}.sd-m-1{margin:.25rem !important}.sd-mt-1,.sd-my-1{margin-top:.25rem !important}.sd-mr-1,.sd-mx-1{margin-right:.25rem !important}.sd-mb-1,.sd-my-1{margin-bottom:.25rem !important}.sd-ml-1,.sd-mx-1{margin-left:.25rem !important}.sd-m-2{margin:.5rem !important}.sd-mt-2,.sd-my-2{margin-top:.5rem !important}.sd-mr-2,.sd-mx-2{margin-right:.5rem !important}.sd-mb-2,.sd-my-2{margin-bottom:.5rem !important}.sd-ml-2,.sd-mx-2{margin-left:.5rem !important}.sd-m-3{margin:1rem !important}.sd-mt-3,.sd-my-3{margin-top:1rem !important}.sd-mr-3,.sd-mx-3{margin-right:1rem !important}.sd-mb-3,.sd-my-3{margin-bottom:1rem !important}.sd-ml-3,.sd-mx-3{margin-left:1rem !important}.sd-m-4{margin:1.5rem !important}.sd-mt-4,.sd-my-4{margin-top:1.5rem !important}.sd-mr-4,.sd-mx-4{margin-right:1.5rem !important}.sd-mb-4,.sd-my-4{margin-bottom:1.5rem !important}.sd-ml-4,.sd-mx-4{margin-left:1.5rem !important}.sd-m-5{margin:3rem !important}.sd-mt-5,.sd-my-5{margin-top:3rem !important}.sd-mr-5,.sd-mx-5{margin-right:3rem !important}.sd-mb-5,.sd-my-5{margin-bottom:3rem !important}.sd-ml-5,.sd-mx-5{margin-left:3rem !important}.sd-w-25{width:25% !important}.sd-w-50{width:50% !important}.sd-w-75{width:75% !important}.sd-w-100{width:100% !important}.sd-w-auto{width:auto !important}.sd-h-25{height:25% !important}.sd-h-50{height:50% !important}.sd-h-75{height:75% !important}.sd-h-100{height:100% !important}.sd-h-auto{height:auto !important}.sd-d-none{display:none !important}.sd-d-inline{display:inline !important}.sd-d-inline-block{display:inline-block !important}.sd-d-block{display:block !important}.sd-d-grid{display:grid !important}.sd-d-flex-row{display:-ms-flexbox !important;display:flex !important;flex-direction:row !important}.sd-d-flex-column{display:-ms-flexbox !important;display:flex !important;flex-direction:column !important}.sd-d-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}@media(min-width: 576px){.sd-d-sm-none{display:none !important}.sd-d-sm-inline{display:inline !important}.sd-d-sm-inline-block{display:inline-block !important}.sd-d-sm-block{display:block !important}.sd-d-sm-grid{display:grid !important}.sd-d-sm-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-sm-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 768px){.sd-d-md-none{display:none !important}.sd-d-md-inline{display:inline !important}.sd-d-md-inline-block{display:inline-block !important}.sd-d-md-block{display:block !important}.sd-d-md-grid{display:grid !important}.sd-d-md-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-md-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 992px){.sd-d-lg-none{display:none !important}.sd-d-lg-inline{display:inline !important}.sd-d-lg-inline-block{display:inline-block !important}.sd-d-lg-block{display:block !important}.sd-d-lg-grid{display:grid !important}.sd-d-lg-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-lg-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 1200px){.sd-d-xl-none{display:none !important}.sd-d-xl-inline{display:inline !important}.sd-d-xl-inline-block{display:inline-block !important}.sd-d-xl-block{display:block !important}.sd-d-xl-grid{display:grid !important}.sd-d-xl-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-xl-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}.sd-align-major-start{justify-content:flex-start !important}.sd-align-major-end{justify-content:flex-end !important}.sd-align-major-center{justify-content:center !important}.sd-align-major-justify{justify-content:space-between !important}.sd-align-major-spaced{justify-content:space-evenly !important}.sd-align-minor-start{align-items:flex-start !important}.sd-align-minor-end{align-items:flex-end !important}.sd-align-minor-center{align-items:center !important}.sd-align-minor-stretch{align-items:stretch !important}.sd-text-justify{text-align:justify !important}.sd-text-left{text-align:left !important}.sd-text-right{text-align:right !important}.sd-text-center{text-align:center !important}.sd-font-weight-light{font-weight:300 !important}.sd-font-weight-lighter{font-weight:lighter !important}.sd-font-weight-normal{font-weight:400 !important}.sd-font-weight-bold{font-weight:700 !important}.sd-font-weight-bolder{font-weight:bolder !important}.sd-font-italic{font-style:italic !important}.sd-text-decoration-none{text-decoration:none !important}.sd-text-lowercase{text-transform:lowercase !important}.sd-text-uppercase{text-transform:uppercase !important}.sd-text-capitalize{text-transform:capitalize !important}.sd-text-wrap{white-space:normal !important}.sd-text-nowrap{white-space:nowrap !important}.sd-text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sd-fs-1,.sd-fs-1>p{font-size:calc(1.375rem + 1.5vw) !important;line-height:unset !important}.sd-fs-2,.sd-fs-2>p{font-size:calc(1.325rem + 0.9vw) !important;line-height:unset !important}.sd-fs-3,.sd-fs-3>p{font-size:calc(1.3rem + 0.6vw) !important;line-height:unset !important}.sd-fs-4,.sd-fs-4>p{font-size:calc(1.275rem + 0.3vw) !important;line-height:unset !important}.sd-fs-5,.sd-fs-5>p{font-size:1.25rem !important;line-height:unset !important}.sd-fs-6,.sd-fs-6>p{font-size:1rem !important;line-height:unset !important}.sd-border-0{border:0 solid !important}.sd-border-top-0{border-top:0 solid !important}.sd-border-bottom-0{border-bottom:0 solid !important}.sd-border-right-0{border-right:0 solid !important}.sd-border-left-0{border-left:0 solid !important}.sd-border-1{border:1px solid !important}.sd-border-top-1{border-top:1px solid !important}.sd-border-bottom-1{border-bottom:1px solid !important}.sd-border-right-1{border-right:1px solid !important}.sd-border-left-1{border-left:1px solid !important}.sd-border-2{border:2px solid !important}.sd-border-top-2{border-top:2px solid !important}.sd-border-bottom-2{border-bottom:2px solid !important}.sd-border-right-2{border-right:2px solid !important}.sd-border-left-2{border-left:2px solid !important}.sd-border-3{border:3px solid !important}.sd-border-top-3{border-top:3px solid !important}.sd-border-bottom-3{border-bottom:3px solid !important}.sd-border-right-3{border-right:3px solid !important}.sd-border-left-3{border-left:3px solid !important}.sd-border-4{border:4px solid !important}.sd-border-top-4{border-top:4px solid !important}.sd-border-bottom-4{border-bottom:4px solid !important}.sd-border-right-4{border-right:4px solid !important}.sd-border-left-4{border-left:4px solid !important}.sd-border-5{border:5px solid !important}.sd-border-top-5{border-top:5px solid !important}.sd-border-bottom-5{border-bottom:5px solid !important}.sd-border-right-5{border-right:5px solid !important}.sd-border-left-5{border-left:5px solid !important}.sd-rounded-0{border-radius:0 !important}.sd-rounded-1{border-radius:.2rem !important}.sd-rounded-2{border-radius:.3rem !important}.sd-rounded-3{border-radius:.5rem !important}.sd-rounded-pill{border-radius:50rem !important}.sd-rounded-circle{border-radius:50% !important}.shadow-none{box-shadow:none !important}.sd-shadow-sm{box-shadow:0 .125rem .25rem var(--sd-color-shadow) !important}.sd-shadow-md{box-shadow:0 .5rem 1rem var(--sd-color-shadow) !important}.sd-shadow-lg{box-shadow:0 1rem 3rem var(--sd-color-shadow) !important}@keyframes sd-slide-from-left{0%{transform:translateX(-100%)}100%{transform:translateX(0)}}@keyframes sd-slide-from-right{0%{transform:translateX(200%)}100%{transform:translateX(0)}}@keyframes sd-grow100{0%{transform:scale(0);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50{0%{transform:scale(0.5);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50-rot20{0%{transform:scale(0.5) rotateZ(-20deg);opacity:.5}75%{transform:scale(1) rotateZ(5deg);opacity:1}95%{transform:scale(1) rotateZ(-1deg);opacity:1}100%{transform:scale(1) rotateZ(0);opacity:1}}.sd-animate-slide-from-left{animation:1s ease-out 0s 1 normal none running sd-slide-from-left}.sd-animate-slide-from-right{animation:1s ease-out 0s 1 normal none running sd-slide-from-right}.sd-animate-grow100{animation:1s ease-out 0s 1 normal none running sd-grow100}.sd-animate-grow50{animation:1s ease-out 0s 1 normal none running sd-grow50}.sd-animate-grow50-rot20{animation:1s ease-out 0s 1 normal none running sd-grow50-rot20}.sd-badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.sd-badge:empty{display:none}a.sd-badge{text-decoration:none}.sd-btn .sd-badge{position:relative;top:-1px}.sd-btn{background-color:transparent;border:1px solid transparent;border-radius:.25rem;cursor:pointer;display:inline-block;font-weight:400;font-size:1rem;line-height:1.5;padding:.375rem .75rem;text-align:center;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;vertical-align:middle;user-select:none;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none}.sd-btn:hover{text-decoration:none}@media(prefers-reduced-motion: reduce){.sd-btn{transition:none}}.sd-btn-primary,.sd-btn-outline-primary:hover,.sd-btn-outline-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-primary:hover,.sd-btn-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary-highlight) !important;border-color:var(--sd-color-primary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-primary{color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary,.sd-btn-outline-secondary:hover,.sd-btn-outline-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary:hover,.sd-btn-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary-highlight) !important;border-color:var(--sd-color-secondary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-secondary{color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success,.sd-btn-outline-success:hover,.sd-btn-outline-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success:hover,.sd-btn-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success-highlight) !important;border-color:var(--sd-color-success-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-success{color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info,.sd-btn-outline-info:hover,.sd-btn-outline-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info:hover,.sd-btn-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info-highlight) !important;border-color:var(--sd-color-info-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-info{color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning,.sd-btn-outline-warning:hover,.sd-btn-outline-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning:hover,.sd-btn-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning-highlight) !important;border-color:var(--sd-color-warning-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-warning{color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger,.sd-btn-outline-danger:hover,.sd-btn-outline-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger:hover,.sd-btn-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger-highlight) !important;border-color:var(--sd-color-danger-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-danger{color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light,.sd-btn-outline-light:hover,.sd-btn-outline-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light:hover,.sd-btn-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light-highlight) !important;border-color:var(--sd-color-light-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-light{color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted,.sd-btn-outline-muted:hover,.sd-btn-outline-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted:hover,.sd-btn-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted-highlight) !important;border-color:var(--sd-color-muted-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-muted{color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark,.sd-btn-outline-dark:hover,.sd-btn-outline-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark:hover,.sd-btn-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark-highlight) !important;border-color:var(--sd-color-dark-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-dark{color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black,.sd-btn-outline-black:hover,.sd-btn-outline-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black:hover,.sd-btn-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black-highlight) !important;border-color:var(--sd-color-black-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-black{color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white,.sd-btn-outline-white:hover,.sd-btn-outline-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white:hover,.sd-btn-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white-highlight) !important;border-color:var(--sd-color-white-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-white{color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.sd-hide-link-text{font-size:0}.sd-octicon,.sd-material-icon{display:inline-block;fill:currentColor;vertical-align:middle}.sd-avatar-xs{border-radius:50%;object-fit:cover;object-position:center;width:1rem;height:1rem}.sd-avatar-sm{border-radius:50%;object-fit:cover;object-position:center;width:3rem;height:3rem}.sd-avatar-md{border-radius:50%;object-fit:cover;object-position:center;width:5rem;height:5rem}.sd-avatar-lg{border-radius:50%;object-fit:cover;object-position:center;width:7rem;height:7rem}.sd-avatar-xl{border-radius:50%;object-fit:cover;object-position:center;width:10rem;height:10rem}.sd-avatar-inherit{border-radius:50%;object-fit:cover;object-position:center;width:inherit;height:inherit}.sd-avatar-initial{border-radius:50%;object-fit:cover;object-position:center;width:initial;height:initial}.sd-card{background-clip:border-box;background-color:var(--sd-color-card-background);border:1px solid var(--sd-color-card-border);border-radius:.25rem;color:var(--sd-color-card-text);display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;position:relative;word-wrap:break-word}.sd-card>hr{margin-left:0;margin-right:0}.sd-card-hover:hover{border-color:var(--sd-color-card-border-hover);transform:scale(1.01)}.sd-card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem 1rem}.sd-card-title{margin-bottom:.5rem}.sd-card-subtitle{margin-top:-0.25rem;margin-bottom:0}.sd-card-text:last-child{margin-bottom:0}.sd-card-link:hover{text-decoration:none}.sd-card-link+.card-link{margin-left:1rem}.sd-card-header{padding:.5rem 1rem;margin-bottom:0;background-color:var(--sd-color-card-header);border-bottom:1px solid var(--sd-color-card-border)}.sd-card-header:first-child{border-radius:calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0}.sd-card-footer{padding:.5rem 1rem;background-color:var(--sd-color-card-footer);border-top:1px solid var(--sd-color-card-border)}.sd-card-footer:last-child{border-radius:0 0 calc(0.25rem - 1px) calc(0.25rem - 1px)}.sd-card-header-tabs{margin-right:-0.5rem;margin-bottom:-0.5rem;margin-left:-0.5rem;border-bottom:0}.sd-card-header-pills{margin-right:-0.5rem;margin-left:-0.5rem}.sd-card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom,.sd-card-img-top{width:100%}.sd-card-img,.sd-card-img-top{border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom{border-bottom-left-radius:calc(0.25rem - 1px);border-bottom-right-radius:calc(0.25rem - 1px)}.sd-cards-carousel{width:100%;display:flex;flex-wrap:nowrap;-ms-flex-direction:row;flex-direction:row;overflow-x:hidden;scroll-snap-type:x mandatory}.sd-cards-carousel.sd-show-scrollbar{overflow-x:auto}.sd-cards-carousel:hover,.sd-cards-carousel:focus{overflow-x:auto}.sd-cards-carousel>.sd-card{flex-shrink:0;scroll-snap-align:start}.sd-cards-carousel>.sd-card:not(:last-child){margin-right:3px}.sd-card-cols-1>.sd-card{width:90%}.sd-card-cols-2>.sd-card{width:45%}.sd-card-cols-3>.sd-card{width:30%}.sd-card-cols-4>.sd-card{width:22.5%}.sd-card-cols-5>.sd-card{width:18%}.sd-card-cols-6>.sd-card{width:15%}.sd-card-cols-7>.sd-card{width:12.8571428571%}.sd-card-cols-8>.sd-card{width:11.25%}.sd-card-cols-9>.sd-card{width:10%}.sd-card-cols-10>.sd-card{width:9%}.sd-card-cols-11>.sd-card{width:8.1818181818%}.sd-card-cols-12>.sd-card{width:7.5%}.sd-container,.sd-container-fluid,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container-xl{margin-left:auto;margin-right:auto;padding-left:var(--sd-gutter-x, 0.75rem);padding-right:var(--sd-gutter-x, 0.75rem);width:100%}@media(min-width: 576px){.sd-container-sm,.sd-container{max-width:540px}}@media(min-width: 768px){.sd-container-md,.sd-container-sm,.sd-container{max-width:720px}}@media(min-width: 992px){.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:960px}}@media(min-width: 1200px){.sd-container-xl,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:1140px}}.sd-row{--sd-gutter-x: 1.5rem;--sd-gutter-y: 0;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-top:calc(var(--sd-gutter-y) * -1);margin-right:calc(var(--sd-gutter-x) * -0.5);margin-left:calc(var(--sd-gutter-x) * -0.5)}.sd-row>*{box-sizing:border-box;flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--sd-gutter-x) * 0.5);padding-left:calc(var(--sd-gutter-x) * 0.5);margin-top:var(--sd-gutter-y)}.sd-col{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-auto>*{flex:0 0 auto;width:auto}.sd-row-cols-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}@media(min-width: 576px){.sd-col-sm{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-sm-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-sm-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-sm-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-sm-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-sm-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-sm-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-sm-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-sm-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-sm-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-sm-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-sm-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-sm-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-sm-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 768px){.sd-col-md{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-md-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-md-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-md-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-md-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-md-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-md-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-md-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-md-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-md-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-md-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-md-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-md-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-md-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 992px){.sd-col-lg{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-lg-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-lg-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-lg-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-lg-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-lg-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-lg-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-lg-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-lg-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-lg-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-lg-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-lg-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-lg-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-lg-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 1200px){.sd-col-xl{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-xl-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-xl-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-xl-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-xl-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-xl-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-xl-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-xl-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-xl-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-xl-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-xl-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-xl-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-xl-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-xl-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}.sd-col-auto{flex:0 0 auto;-ms-flex:0 0 auto;width:auto}.sd-col-1{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}.sd-col-2{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-col-3{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-col-4{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-col-5{flex:0 0 auto;-ms-flex:0 0 auto;width:41.6666666667%}.sd-col-6{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-col-7{flex:0 0 auto;-ms-flex:0 0 auto;width:58.3333333333%}.sd-col-8{flex:0 0 auto;-ms-flex:0 0 auto;width:66.6666666667%}.sd-col-9{flex:0 0 auto;-ms-flex:0 0 auto;width:75%}.sd-col-10{flex:0 0 auto;-ms-flex:0 0 auto;width:83.3333333333%}.sd-col-11{flex:0 0 auto;-ms-flex:0 0 auto;width:91.6666666667%}.sd-col-12{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-g-0,.sd-gy-0{--sd-gutter-y: 0}.sd-g-0,.sd-gx-0{--sd-gutter-x: 0}.sd-g-1,.sd-gy-1{--sd-gutter-y: 0.25rem}.sd-g-1,.sd-gx-1{--sd-gutter-x: 0.25rem}.sd-g-2,.sd-gy-2{--sd-gutter-y: 0.5rem}.sd-g-2,.sd-gx-2{--sd-gutter-x: 0.5rem}.sd-g-3,.sd-gy-3{--sd-gutter-y: 1rem}.sd-g-3,.sd-gx-3{--sd-gutter-x: 1rem}.sd-g-4,.sd-gy-4{--sd-gutter-y: 1.5rem}.sd-g-4,.sd-gx-4{--sd-gutter-x: 1.5rem}.sd-g-5,.sd-gy-5{--sd-gutter-y: 3rem}.sd-g-5,.sd-gx-5{--sd-gutter-x: 3rem}@media(min-width: 576px){.sd-col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-sm-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-sm-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-sm-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-sm-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-sm-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-sm-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-sm-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-sm-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-sm-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-sm-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-sm-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-sm-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-sm-0,.sd-gy-sm-0{--sd-gutter-y: 0}.sd-g-sm-0,.sd-gx-sm-0{--sd-gutter-x: 0}.sd-g-sm-1,.sd-gy-sm-1{--sd-gutter-y: 0.25rem}.sd-g-sm-1,.sd-gx-sm-1{--sd-gutter-x: 0.25rem}.sd-g-sm-2,.sd-gy-sm-2{--sd-gutter-y: 0.5rem}.sd-g-sm-2,.sd-gx-sm-2{--sd-gutter-x: 0.5rem}.sd-g-sm-3,.sd-gy-sm-3{--sd-gutter-y: 1rem}.sd-g-sm-3,.sd-gx-sm-3{--sd-gutter-x: 1rem}.sd-g-sm-4,.sd-gy-sm-4{--sd-gutter-y: 1.5rem}.sd-g-sm-4,.sd-gx-sm-4{--sd-gutter-x: 1.5rem}.sd-g-sm-5,.sd-gy-sm-5{--sd-gutter-y: 3rem}.sd-g-sm-5,.sd-gx-sm-5{--sd-gutter-x: 3rem}}@media(min-width: 768px){.sd-col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-md-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-md-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-md-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-md-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-md-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-md-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-md-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-md-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-md-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-md-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-md-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-md-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-md-0,.sd-gy-md-0{--sd-gutter-y: 0}.sd-g-md-0,.sd-gx-md-0{--sd-gutter-x: 0}.sd-g-md-1,.sd-gy-md-1{--sd-gutter-y: 0.25rem}.sd-g-md-1,.sd-gx-md-1{--sd-gutter-x: 0.25rem}.sd-g-md-2,.sd-gy-md-2{--sd-gutter-y: 0.5rem}.sd-g-md-2,.sd-gx-md-2{--sd-gutter-x: 0.5rem}.sd-g-md-3,.sd-gy-md-3{--sd-gutter-y: 1rem}.sd-g-md-3,.sd-gx-md-3{--sd-gutter-x: 1rem}.sd-g-md-4,.sd-gy-md-4{--sd-gutter-y: 1.5rem}.sd-g-md-4,.sd-gx-md-4{--sd-gutter-x: 1.5rem}.sd-g-md-5,.sd-gy-md-5{--sd-gutter-y: 3rem}.sd-g-md-5,.sd-gx-md-5{--sd-gutter-x: 3rem}}@media(min-width: 992px){.sd-col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-lg-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-lg-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-lg-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-lg-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-lg-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-lg-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-lg-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-lg-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-lg-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-lg-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-lg-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-lg-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-lg-0,.sd-gy-lg-0{--sd-gutter-y: 0}.sd-g-lg-0,.sd-gx-lg-0{--sd-gutter-x: 0}.sd-g-lg-1,.sd-gy-lg-1{--sd-gutter-y: 0.25rem}.sd-g-lg-1,.sd-gx-lg-1{--sd-gutter-x: 0.25rem}.sd-g-lg-2,.sd-gy-lg-2{--sd-gutter-y: 0.5rem}.sd-g-lg-2,.sd-gx-lg-2{--sd-gutter-x: 0.5rem}.sd-g-lg-3,.sd-gy-lg-3{--sd-gutter-y: 1rem}.sd-g-lg-3,.sd-gx-lg-3{--sd-gutter-x: 1rem}.sd-g-lg-4,.sd-gy-lg-4{--sd-gutter-y: 1.5rem}.sd-g-lg-4,.sd-gx-lg-4{--sd-gutter-x: 1.5rem}.sd-g-lg-5,.sd-gy-lg-5{--sd-gutter-y: 3rem}.sd-g-lg-5,.sd-gx-lg-5{--sd-gutter-x: 3rem}}@media(min-width: 1200px){.sd-col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-xl-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-xl-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-xl-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-xl-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-xl-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-xl-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-xl-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-xl-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-xl-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-xl-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-xl-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-xl-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-xl-0,.sd-gy-xl-0{--sd-gutter-y: 0}.sd-g-xl-0,.sd-gx-xl-0{--sd-gutter-x: 0}.sd-g-xl-1,.sd-gy-xl-1{--sd-gutter-y: 0.25rem}.sd-g-xl-1,.sd-gx-xl-1{--sd-gutter-x: 0.25rem}.sd-g-xl-2,.sd-gy-xl-2{--sd-gutter-y: 0.5rem}.sd-g-xl-2,.sd-gx-xl-2{--sd-gutter-x: 0.5rem}.sd-g-xl-3,.sd-gy-xl-3{--sd-gutter-y: 1rem}.sd-g-xl-3,.sd-gx-xl-3{--sd-gutter-x: 1rem}.sd-g-xl-4,.sd-gy-xl-4{--sd-gutter-y: 1.5rem}.sd-g-xl-4,.sd-gx-xl-4{--sd-gutter-x: 1.5rem}.sd-g-xl-5,.sd-gy-xl-5{--sd-gutter-y: 3rem}.sd-g-xl-5,.sd-gx-xl-5{--sd-gutter-x: 3rem}}.sd-flex-row-reverse{flex-direction:row-reverse !important}details.sd-dropdown{position:relative}details.sd-dropdown .sd-summary-title{font-weight:700;padding-right:3em !important;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;user-select:none}details.sd-dropdown:hover{cursor:pointer}details.sd-dropdown .sd-summary-content{cursor:default}details.sd-dropdown summary{list-style:none;padding:1em}details.sd-dropdown summary .sd-octicon.no-title{vertical-align:middle}details.sd-dropdown[open] summary .sd-octicon.no-title{visibility:hidden}details.sd-dropdown summary::-webkit-details-marker{display:none}details.sd-dropdown summary:focus{outline:none}details.sd-dropdown .sd-summary-icon{margin-right:.5em}details.sd-dropdown .sd-summary-icon svg{opacity:.8}details.sd-dropdown summary:hover .sd-summary-up svg,details.sd-dropdown summary:hover .sd-summary-down svg{opacity:1;transform:scale(1.1)}details.sd-dropdown .sd-summary-up svg,details.sd-dropdown .sd-summary-down svg{display:block;opacity:.6}details.sd-dropdown .sd-summary-up,details.sd-dropdown .sd-summary-down{pointer-events:none;position:absolute;right:1em;top:1em}details.sd-dropdown[open]>.sd-summary-title .sd-summary-down{visibility:hidden}details.sd-dropdown:not([open])>.sd-summary-title .sd-summary-up{visibility:hidden}details.sd-dropdown:not([open]).sd-card{border:none}details.sd-dropdown:not([open])>.sd-card-header{border:1px solid var(--sd-color-card-border);border-radius:.25rem}details.sd-dropdown.sd-fade-in[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out;animation:sd-fade-in .5s ease-in-out}details.sd-dropdown.sd-fade-in-slide-down[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out}.sd-col>.sd-dropdown{width:100%}.sd-summary-content>.sd-tab-set:first-child{margin-top:0}@keyframes sd-fade-in{0%{opacity:0}100%{opacity:1}}@keyframes sd-slide-down{0%{transform:translate(0, -10px)}100%{transform:translate(0, 0)}}.sd-tab-set{border-radius:.125rem;display:flex;flex-wrap:wrap;margin:1em 0;position:relative}.sd-tab-set>input{opacity:0;position:absolute}.sd-tab-set>input:checked+label{border-color:var(--sd-color-tabs-underline-active);color:var(--sd-color-tabs-label-active)}.sd-tab-set>input:checked+label+.sd-tab-content{display:block}.sd-tab-set>input:not(:checked)+label:hover{color:var(--sd-color-tabs-label-hover);border-color:var(--sd-color-tabs-underline-hover)}.sd-tab-set>input:focus+label{outline-style:auto}.sd-tab-set>input:not(.focus-visible)+label{outline:none;-webkit-tap-highlight-color:transparent}.sd-tab-set>label{border-bottom:.125rem solid transparent;margin-bottom:0;color:var(--sd-color-tabs-label-inactive);border-color:var(--sd-color-tabs-underline-inactive);cursor:pointer;font-size:var(--sd-fontsize-tabs-label);font-weight:700;padding:1em 1.25em .5em;transition:color 250ms;width:auto;z-index:1}html .sd-tab-set>label:hover{color:var(--sd-color-tabs-label-active)}.sd-col>.sd-tab-set{width:100%}.sd-tab-content{box-shadow:0 -0.0625rem var(--sd-color-tabs-overline),0 .0625rem var(--sd-color-tabs-underline);display:none;order:99;padding-bottom:.75rem;padding-top:.75rem;width:100%}.sd-tab-content>:first-child{margin-top:0 !important}.sd-tab-content>:last-child{margin-bottom:0 !important}.sd-tab-content>.sd-tab-set{margin:0}.sd-sphinx-override,.sd-sphinx-override *{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.sd-sphinx-override p{margin-top:0}:root{--sd-color-primary: #007bff;--sd-color-secondary: #6c757d;--sd-color-success: #28a745;--sd-color-info: #17a2b8;--sd-color-warning: #f0b37e;--sd-color-danger: #dc3545;--sd-color-light: #f8f9fa;--sd-color-muted: #6c757d;--sd-color-dark: #212529;--sd-color-black: black;--sd-color-white: white;--sd-color-primary-highlight: #0069d9;--sd-color-secondary-highlight: #5c636a;--sd-color-success-highlight: #228e3b;--sd-color-info-highlight: #148a9c;--sd-color-warning-highlight: #cc986b;--sd-color-danger-highlight: #bb2d3b;--sd-color-light-highlight: #d3d4d5;--sd-color-muted-highlight: #5c636a;--sd-color-dark-highlight: #1c1f23;--sd-color-black-highlight: black;--sd-color-white-highlight: #d9d9d9;--sd-color-primary-text: #fff;--sd-color-secondary-text: #fff;--sd-color-success-text: #fff;--sd-color-info-text: #fff;--sd-color-warning-text: #212529;--sd-color-danger-text: #fff;--sd-color-light-text: #212529;--sd-color-muted-text: #fff;--sd-color-dark-text: #fff;--sd-color-black-text: #fff;--sd-color-white-text: #212529;--sd-color-shadow: rgba(0, 0, 0, 0.15);--sd-color-card-border: rgba(0, 0, 0, 0.125);--sd-color-card-border-hover: hsla(231, 99%, 66%, 1);--sd-color-card-background: transparent;--sd-color-card-text: inherit;--sd-color-card-header: transparent;--sd-color-card-footer: transparent;--sd-color-tabs-label-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-hover: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-inactive: hsl(0, 0%, 66%);--sd-color-tabs-underline-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-underline-hover: rgba(178, 206, 245, 0.62);--sd-color-tabs-underline-inactive: transparent;--sd-color-tabs-overline: rgb(222, 222, 222);--sd-color-tabs-underline: rgb(222, 222, 222);--sd-fontsize-tabs-label: 1rem} diff --git a/docs/docs_build/_static/design-tabs.js b/docs/docs_build/_static/design-tabs.js deleted file mode 100644 index 36b38cf..0000000 --- a/docs/docs_build/_static/design-tabs.js +++ /dev/null @@ -1,27 +0,0 @@ -var sd_labels_by_text = {}; - -function ready() { - const li = document.getElementsByClassName("sd-tab-label"); - for (const label of li) { - syncId = label.getAttribute("data-sync-id"); - if (syncId) { - label.onclick = onLabelClick; - if (!sd_labels_by_text[syncId]) { - sd_labels_by_text[syncId] = []; - } - sd_labels_by_text[syncId].push(label); - } - } -} - -function onLabelClick() { - // Activate other inputs with the same sync id. - syncId = this.getAttribute("data-sync-id"); - for (label of sd_labels_by_text[syncId]) { - if (label === this) continue; - label.previousElementSibling.checked = true; - } - window.localStorage.setItem("sphinx-design-last-tab", syncId); -} - -document.addEventListener("DOMContentLoaded", ready, false); diff --git a/docs/docs_build/_static/doctools.js b/docs/docs_build/_static/doctools.js deleted file mode 100644 index 527b876..0000000 --- a/docs/docs_build/_static/doctools.js +++ /dev/null @@ -1,156 +0,0 @@ -/* - * doctools.js - * ~~~~~~~~~~~ - * - * Base JavaScript utilities for all Sphinx HTML documentation. - * - * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * - */ -"use strict"; - -const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ - "TEXTAREA", - "INPUT", - "SELECT", - "BUTTON", -]); - -const _ready = (callback) => { - if (document.readyState !== "loading") { - callback(); - } else { - document.addEventListener("DOMContentLoaded", callback); - } -}; - -/** - * Small JavaScript module for the documentation. - */ -const Documentation = { - init: () => { - Documentation.initDomainIndexTable(); - Documentation.initOnKeyListeners(); - }, - - /** - * i18n support - */ - TRANSLATIONS: {}, - PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), - LOCALE: "unknown", - - // gettext and ngettext don't access this so that the functions - // can safely bound to a different name (_ = Documentation.gettext) - gettext: (string) => { - const translated = Documentation.TRANSLATIONS[string]; - switch (typeof translated) { - case "undefined": - return string; // no translation - case "string": - return translated; // translation exists - default: - return translated[0]; // (singular, plural) translation tuple exists - } - }, - - ngettext: (singular, plural, n) => { - const translated = Documentation.TRANSLATIONS[singular]; - if (typeof translated !== "undefined") - return translated[Documentation.PLURAL_EXPR(n)]; - return n === 1 ? singular : plural; - }, - - addTranslations: (catalog) => { - Object.assign(Documentation.TRANSLATIONS, catalog.messages); - Documentation.PLURAL_EXPR = new Function( - "n", - `return (${catalog.plural_expr})` - ); - Documentation.LOCALE = catalog.locale; - }, - - /** - * helper function to focus on search bar - */ - focusSearchBar: () => { - document.querySelectorAll("input[name=q]")[0]?.focus(); - }, - - /** - * Initialise the domain index toggle buttons - */ - initDomainIndexTable: () => { - const toggler = (el) => { - const idNumber = el.id.substr(7); - const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); - if (el.src.substr(-9) === "minus.png") { - el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; - toggledRows.forEach((el) => (el.style.display = "none")); - } else { - el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; - toggledRows.forEach((el) => (el.style.display = "")); - } - }; - - const togglerElements = document.querySelectorAll("img.toggler"); - togglerElements.forEach((el) => - el.addEventListener("click", (event) => toggler(event.currentTarget)) - ); - togglerElements.forEach((el) => (el.style.display = "")); - if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); - }, - - initOnKeyListeners: () => { - // only install a listener if it is really needed - if ( - !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && - !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS - ) - return; - - document.addEventListener("keydown", (event) => { - // bail for input elements - if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; - // bail with special keys - if (event.altKey || event.ctrlKey || event.metaKey) return; - - if (!event.shiftKey) { - switch (event.key) { - case "ArrowLeft": - if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; - - const prevLink = document.querySelector('link[rel="prev"]'); - if (prevLink && prevLink.href) { - window.location.href = prevLink.href; - event.preventDefault(); - } - break; - case "ArrowRight": - if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; - - const nextLink = document.querySelector('link[rel="next"]'); - if (nextLink && nextLink.href) { - window.location.href = nextLink.href; - event.preventDefault(); - } - break; - } - } - - // some keyboard layouts may need Shift to get / - switch (event.key) { - case "/": - if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; - Documentation.focusSearchBar(); - event.preventDefault(); - } - }); - }, -}; - -// quick alias for translations -const _ = Documentation.gettext; - -_ready(Documentation.init); diff --git a/docs/docs_build/_static/documentation_options.js b/docs/docs_build/_static/documentation_options.js deleted file mode 100644 index b57ae3b..0000000 --- a/docs/docs_build/_static/documentation_options.js +++ /dev/null @@ -1,14 +0,0 @@ -var DOCUMENTATION_OPTIONS = { - URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), - VERSION: '', - LANGUAGE: 'en', - COLLAPSE_INDEX: false, - BUILDER: 'html', - FILE_SUFFIX: '.html', - LINK_SUFFIX: '.html', - HAS_SOURCE: true, - SOURCELINK_SUFFIX: '.txt', - NAVIGATION_WITH_KEYS: false, - SHOW_SEARCH_SUMMARY: true, - ENABLE_SEARCH_SHORTCUTS: true, -}; \ No newline at end of file diff --git a/docs/docs_build/_static/file.png b/docs/docs_build/_static/file.png deleted file mode 100644 index a858a41..0000000 Binary files a/docs/docs_build/_static/file.png and /dev/null differ diff --git a/docs/docs_build/_static/jquery-3.6.0.js b/docs/docs_build/_static/jquery-3.6.0.js deleted file mode 100644 index fc6c299..0000000 --- a/docs/docs_build/_static/jquery-3.6.0.js +++ /dev/null @@ -1,10881 +0,0 @@ -/*! - * jQuery JavaScript Library v3.6.0 - * https://jquery.com/ - * - * Includes Sizzle.js - * https://sizzlejs.com/ - * - * Copyright OpenJS Foundation and other contributors - * Released under the MIT license - * https://jquery.org/license - * - * Date: 2021-03-02T17:08Z - */ -( function( global, factory ) { - - "use strict"; - - if ( typeof module === "object" && typeof module.exports === "object" ) { - - // For CommonJS and CommonJS-like environments where a proper `window` - // is present, execute the factory and get jQuery. - // For environments that do not have a `window` with a `document` - // (such as Node.js), expose a factory as module.exports. - // This accentuates the need for the creation of a real `window`. - // e.g. var jQuery = require("jquery")(window); - // See ticket #14549 for more info. - module.exports = global.document ? - factory( global, true ) : - function( w ) { - if ( !w.document ) { - throw new Error( "jQuery requires a window with a document" ); - } - return factory( w ); - }; - } else { - factory( global ); - } - -// Pass this if window is not defined yet -} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) { - -// Edge <= 12 - 13+, Firefox <=18 - 45+, IE 10 - 11, Safari 5.1 - 9+, iOS 6 - 9.1 -// throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode -// arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common -// enough that all such attempts are guarded in a try block. -"use strict"; - -var arr = []; - -var getProto = Object.getPrototypeOf; - -var slice = arr.slice; - -var flat = arr.flat ? function( array ) { - return arr.flat.call( array ); -} : function( array ) { - return arr.concat.apply( [], array ); -}; - - -var push = arr.push; - -var indexOf = arr.indexOf; - -var class2type = {}; - -var toString = class2type.toString; - -var hasOwn = class2type.hasOwnProperty; - -var fnToString = hasOwn.toString; - -var ObjectFunctionString = fnToString.call( Object ); - -var support = {}; - -var isFunction = function isFunction( obj ) { - - // Support: Chrome <=57, Firefox <=52 - // In some browsers, typeof returns "function" for HTML elements - // (i.e., `typeof document.createElement( "object" ) === "function"`). - // We don't want to classify *any* DOM node as a function. - // Support: QtWeb <=3.8.5, WebKit <=534.34, wkhtmltopdf tool <=0.12.5 - // Plus for old WebKit, typeof returns "function" for HTML collections - // (e.g., `typeof document.getElementsByTagName("div") === "function"`). (gh-4756) - return typeof obj === "function" && typeof obj.nodeType !== "number" && - typeof obj.item !== "function"; - }; - - -var isWindow = function isWindow( obj ) { - return obj != null && obj === obj.window; - }; - - -var document = window.document; - - - - var preservedScriptAttributes = { - type: true, - src: true, - nonce: true, - noModule: true - }; - - function DOMEval( code, node, doc ) { - doc = doc || document; - - var i, val, - script = doc.createElement( "script" ); - - script.text = code; - if ( node ) { - for ( i in preservedScriptAttributes ) { - - // Support: Firefox 64+, Edge 18+ - // Some browsers don't support the "nonce" property on scripts. - // On the other hand, just using `getAttribute` is not enough as - // the `nonce` attribute is reset to an empty string whenever it - // becomes browsing-context connected. - // See https://github.com/whatwg/html/issues/2369 - // See https://html.spec.whatwg.org/#nonce-attributes - // The `node.getAttribute` check was added for the sake of - // `jQuery.globalEval` so that it can fake a nonce-containing node - // via an object. - val = node[ i ] || node.getAttribute && node.getAttribute( i ); - if ( val ) { - script.setAttribute( i, val ); - } - } - } - doc.head.appendChild( script ).parentNode.removeChild( script ); - } - - -function toType( obj ) { - if ( obj == null ) { - return obj + ""; - } - - // Support: Android <=2.3 only (functionish RegExp) - return typeof obj === "object" || typeof obj === "function" ? - class2type[ toString.call( obj ) ] || "object" : - typeof obj; -} -/* global Symbol */ -// Defining this global in .eslintrc.json would create a danger of using the global -// unguarded in another place, it seems safer to define global only for this module - - - -var - version = "3.6.0", - - // Define a local copy of jQuery - jQuery = function( selector, context ) { - - // The jQuery object is actually just the init constructor 'enhanced' - // Need init if jQuery is called (just allow error to be thrown if not included) - return new jQuery.fn.init( selector, context ); - }; - -jQuery.fn = jQuery.prototype = { - - // The current version of jQuery being used - jquery: version, - - constructor: jQuery, - - // The default length of a jQuery object is 0 - length: 0, - - toArray: function() { - return slice.call( this ); - }, - - // Get the Nth element in the matched element set OR - // Get the whole matched element set as a clean array - get: function( num ) { - - // Return all the elements in a clean array - if ( num == null ) { - return slice.call( this ); - } - - // Return just the one element from the set - return num < 0 ? this[ num + this.length ] : this[ num ]; - }, - - // Take an array of elements and push it onto the stack - // (returning the new matched element set) - pushStack: function( elems ) { - - // Build a new jQuery matched element set - var ret = jQuery.merge( this.constructor(), elems ); - - // Add the old object onto the stack (as a reference) - ret.prevObject = this; - - // Return the newly-formed element set - return ret; - }, - - // Execute a callback for every element in the matched set. - each: function( callback ) { - return jQuery.each( this, callback ); - }, - - map: function( callback ) { - return this.pushStack( jQuery.map( this, function( elem, i ) { - return callback.call( elem, i, elem ); - } ) ); - }, - - slice: function() { - return this.pushStack( slice.apply( this, arguments ) ); - }, - - first: function() { - return this.eq( 0 ); - }, - - last: function() { - return this.eq( -1 ); - }, - - even: function() { - return this.pushStack( jQuery.grep( this, function( _elem, i ) { - return ( i + 1 ) % 2; - } ) ); - }, - - odd: function() { - return this.pushStack( jQuery.grep( this, function( _elem, i ) { - return i % 2; - } ) ); - }, - - eq: function( i ) { - var len = this.length, - j = +i + ( i < 0 ? len : 0 ); - return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] ); - }, - - end: function() { - return this.prevObject || this.constructor(); - }, - - // For internal use only. - // Behaves like an Array's method, not like a jQuery method. - push: push, - sort: arr.sort, - splice: arr.splice -}; - -jQuery.extend = jQuery.fn.extend = function() { - var options, name, src, copy, copyIsArray, clone, - target = arguments[ 0 ] || {}, - i = 1, - length = arguments.length, - deep = false; - - // Handle a deep copy situation - if ( typeof target === "boolean" ) { - deep = target; - - // Skip the boolean and the target - target = arguments[ i ] || {}; - i++; - } - - // Handle case when target is a string or something (possible in deep copy) - if ( typeof target !== "object" && !isFunction( target ) ) { - target = {}; - } - - // Extend jQuery itself if only one argument is passed - if ( i === length ) { - target = this; - i--; - } - - for ( ; i < length; i++ ) { - - // Only deal with non-null/undefined values - if ( ( options = arguments[ i ] ) != null ) { - - // Extend the base object - for ( name in options ) { - copy = options[ name ]; - - // Prevent Object.prototype pollution - // Prevent never-ending loop - if ( name === "__proto__" || target === copy ) { - continue; - } - - // Recurse if we're merging plain objects or arrays - if ( deep && copy && ( jQuery.isPlainObject( copy ) || - ( copyIsArray = Array.isArray( copy ) ) ) ) { - src = target[ name ]; - - // Ensure proper type for the source value - if ( copyIsArray && !Array.isArray( src ) ) { - clone = []; - } else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) { - clone = {}; - } else { - clone = src; - } - copyIsArray = false; - - // Never move original objects, clone them - target[ name ] = jQuery.extend( deep, clone, copy ); - - // Don't bring in undefined values - } else if ( copy !== undefined ) { - target[ name ] = copy; - } - } - } - } - - // Return the modified object - return target; -}; - -jQuery.extend( { - - // Unique for each copy of jQuery on the page - expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), - - // Assume jQuery is ready without the ready module - isReady: true, - - error: function( msg ) { - throw new Error( msg ); - }, - - noop: function() {}, - - isPlainObject: function( obj ) { - var proto, Ctor; - - // Detect obvious negatives - // Use toString instead of jQuery.type to catch host objects - if ( !obj || toString.call( obj ) !== "[object Object]" ) { - return false; - } - - proto = getProto( obj ); - - // Objects with no prototype (e.g., `Object.create( null )`) are plain - if ( !proto ) { - return true; - } - - // Objects with prototype are plain iff they were constructed by a global Object function - Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor; - return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString; - }, - - isEmptyObject: function( obj ) { - var name; - - for ( name in obj ) { - return false; - } - return true; - }, - - // Evaluates a script in a provided context; falls back to the global one - // if not specified. - globalEval: function( code, options, doc ) { - DOMEval( code, { nonce: options && options.nonce }, doc ); - }, - - each: function( obj, callback ) { - var length, i = 0; - - if ( isArrayLike( obj ) ) { - length = obj.length; - for ( ; i < length; i++ ) { - if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { - break; - } - } - } else { - for ( i in obj ) { - if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { - break; - } - } - } - - return obj; - }, - - // results is for internal usage only - makeArray: function( arr, results ) { - var ret = results || []; - - if ( arr != null ) { - if ( isArrayLike( Object( arr ) ) ) { - jQuery.merge( ret, - typeof arr === "string" ? - [ arr ] : arr - ); - } else { - push.call( ret, arr ); - } - } - - return ret; - }, - - inArray: function( elem, arr, i ) { - return arr == null ? -1 : indexOf.call( arr, elem, i ); - }, - - // Support: Android <=4.0 only, PhantomJS 1 only - // push.apply(_, arraylike) throws on ancient WebKit - merge: function( first, second ) { - var len = +second.length, - j = 0, - i = first.length; - - for ( ; j < len; j++ ) { - first[ i++ ] = second[ j ]; - } - - first.length = i; - - return first; - }, - - grep: function( elems, callback, invert ) { - var callbackInverse, - matches = [], - i = 0, - length = elems.length, - callbackExpect = !invert; - - // Go through the array, only saving the items - // that pass the validator function - for ( ; i < length; i++ ) { - callbackInverse = !callback( elems[ i ], i ); - if ( callbackInverse !== callbackExpect ) { - matches.push( elems[ i ] ); - } - } - - return matches; - }, - - // arg is for internal usage only - map: function( elems, callback, arg ) { - var length, value, - i = 0, - ret = []; - - // Go through the array, translating each of the items to their new values - if ( isArrayLike( elems ) ) { - length = elems.length; - for ( ; i < length; i++ ) { - value = callback( elems[ i ], i, arg ); - - if ( value != null ) { - ret.push( value ); - } - } - - // Go through every key on the object, - } else { - for ( i in elems ) { - value = callback( elems[ i ], i, arg ); - - if ( value != null ) { - ret.push( value ); - } - } - } - - // Flatten any nested arrays - return flat( ret ); - }, - - // A global GUID counter for objects - guid: 1, - - // jQuery.support is not used in Core but other projects attach their - // properties to it so it needs to exist. - support: support -} ); - -if ( typeof Symbol === "function" ) { - jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ]; -} - -// Populate the class2type map -jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), - function( _i, name ) { - class2type[ "[object " + name + "]" ] = name.toLowerCase(); - } ); - -function isArrayLike( obj ) { - - // Support: real iOS 8.2 only (not reproducible in simulator) - // `in` check used to prevent JIT error (gh-2145) - // hasOwn isn't used here due to false negatives - // regarding Nodelist length in IE - var length = !!obj && "length" in obj && obj.length, - type = toType( obj ); - - if ( isFunction( obj ) || isWindow( obj ) ) { - return false; - } - - return type === "array" || length === 0 || - typeof length === "number" && length > 0 && ( length - 1 ) in obj; -} -var Sizzle = -/*! - * Sizzle CSS Selector Engine v2.3.6 - * https://sizzlejs.com/ - * - * Copyright JS Foundation and other contributors - * Released under the MIT license - * https://js.foundation/ - * - * Date: 2021-02-16 - */ -( function( window ) { -var i, - support, - Expr, - getText, - isXML, - tokenize, - compile, - select, - outermostContext, - sortInput, - hasDuplicate, - - // Local document vars - setDocument, - document, - docElem, - documentIsHTML, - rbuggyQSA, - rbuggyMatches, - matches, - contains, - - // Instance-specific data - expando = "sizzle" + 1 * new Date(), - preferredDoc = window.document, - dirruns = 0, - done = 0, - classCache = createCache(), - tokenCache = createCache(), - compilerCache = createCache(), - nonnativeSelectorCache = createCache(), - sortOrder = function( a, b ) { - if ( a === b ) { - hasDuplicate = true; - } - return 0; - }, - - // Instance methods - hasOwn = ( {} ).hasOwnProperty, - arr = [], - pop = arr.pop, - pushNative = arr.push, - push = arr.push, - slice = arr.slice, - - // Use a stripped-down indexOf as it's faster than native - // https://jsperf.com/thor-indexof-vs-for/5 - indexOf = function( list, elem ) { - var i = 0, - len = list.length; - for ( ; i < len; i++ ) { - if ( list[ i ] === elem ) { - return i; - } - } - return -1; - }, - - booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|" + - "ismap|loop|multiple|open|readonly|required|scoped", - - // Regular expressions - - // http://www.w3.org/TR/css3-selectors/#whitespace - whitespace = "[\\x20\\t\\r\\n\\f]", - - // https://www.w3.org/TR/css-syntax-3/#ident-token-diagram - identifier = "(?:\\\\[\\da-fA-F]{1,6}" + whitespace + - "?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+", - - // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors - attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace + - - // Operator (capture 2) - "*([*^$|!~]?=)" + whitespace + - - // "Attribute values must be CSS identifiers [capture 5] - // or strings [capture 3 or capture 4]" - "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + - whitespace + "*\\]", - - pseudos = ":(" + identifier + ")(?:\\((" + - - // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: - // 1. quoted (capture 3; capture 4 or capture 5) - "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + - - // 2. simple (capture 6) - "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + - - // 3. anything else (capture 2) - ".*" + - ")\\)|)", - - // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter - rwhitespace = new RegExp( whitespace + "+", "g" ), - rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + - whitespace + "+$", "g" ), - - rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), - rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + - "*" ), - rdescend = new RegExp( whitespace + "|>" ), - - rpseudo = new RegExp( pseudos ), - ridentifier = new RegExp( "^" + identifier + "$" ), - - matchExpr = { - "ID": new RegExp( "^#(" + identifier + ")" ), - "CLASS": new RegExp( "^\\.(" + identifier + ")" ), - "TAG": new RegExp( "^(" + identifier + "|[*])" ), - "ATTR": new RegExp( "^" + attributes ), - "PSEUDO": new RegExp( "^" + pseudos ), - "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + - whitespace + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + - whitespace + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), - "bool": new RegExp( "^(?:" + booleans + ")$", "i" ), - - // For use in libraries implementing .is() - // We use this for POS matching in `select` - "needsContext": new RegExp( "^" + whitespace + - "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace + - "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) - }, - - rhtml = /HTML$/i, - rinputs = /^(?:input|select|textarea|button)$/i, - rheader = /^h\d$/i, - - rnative = /^[^{]+\{\s*\[native \w/, - - // Easily-parseable/retrievable ID or TAG or CLASS selectors - rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, - - rsibling = /[+~]/, - - // CSS escapes - // http://www.w3.org/TR/CSS21/syndata.html#escaped-characters - runescape = new RegExp( "\\\\[\\da-fA-F]{1,6}" + whitespace + "?|\\\\([^\\r\\n\\f])", "g" ), - funescape = function( escape, nonHex ) { - var high = "0x" + escape.slice( 1 ) - 0x10000; - - return nonHex ? - - // Strip the backslash prefix from a non-hex escape sequence - nonHex : - - // Replace a hexadecimal escape sequence with the encoded Unicode code point - // Support: IE <=11+ - // For values outside the Basic Multilingual Plane (BMP), manually construct a - // surrogate pair - high < 0 ? - String.fromCharCode( high + 0x10000 ) : - String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); - }, - - // CSS string/identifier serialization - // https://drafts.csswg.org/cssom/#common-serializing-idioms - rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g, - fcssescape = function( ch, asCodePoint ) { - if ( asCodePoint ) { - - // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER - if ( ch === "\0" ) { - return "\uFFFD"; - } - - // Control characters and (dependent upon position) numbers get escaped as code points - return ch.slice( 0, -1 ) + "\\" + - ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " "; - } - - // Other potentially-special ASCII characters get backslash-escaped - return "\\" + ch; - }, - - // Used for iframes - // See setDocument() - // Removing the function wrapper causes a "Permission Denied" - // error in IE - unloadHandler = function() { - setDocument(); - }, - - inDisabledFieldset = addCombinator( - function( elem ) { - return elem.disabled === true && elem.nodeName.toLowerCase() === "fieldset"; - }, - { dir: "parentNode", next: "legend" } - ); - -// Optimize for push.apply( _, NodeList ) -try { - push.apply( - ( arr = slice.call( preferredDoc.childNodes ) ), - preferredDoc.childNodes - ); - - // Support: Android<4.0 - // Detect silently failing push.apply - // eslint-disable-next-line no-unused-expressions - arr[ preferredDoc.childNodes.length ].nodeType; -} catch ( e ) { - push = { apply: arr.length ? - - // Leverage slice if possible - function( target, els ) { - pushNative.apply( target, slice.call( els ) ); - } : - - // Support: IE<9 - // Otherwise append directly - function( target, els ) { - var j = target.length, - i = 0; - - // Can't trust NodeList.length - while ( ( target[ j++ ] = els[ i++ ] ) ) {} - target.length = j - 1; - } - }; -} - -function Sizzle( selector, context, results, seed ) { - var m, i, elem, nid, match, groups, newSelector, - newContext = context && context.ownerDocument, - - // nodeType defaults to 9, since context defaults to document - nodeType = context ? context.nodeType : 9; - - results = results || []; - - // Return early from calls with invalid selector or context - if ( typeof selector !== "string" || !selector || - nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) { - - return results; - } - - // Try to shortcut find operations (as opposed to filters) in HTML documents - if ( !seed ) { - setDocument( context ); - context = context || document; - - if ( documentIsHTML ) { - - // If the selector is sufficiently simple, try using a "get*By*" DOM method - // (excepting DocumentFragment context, where the methods don't exist) - if ( nodeType !== 11 && ( match = rquickExpr.exec( selector ) ) ) { - - // ID selector - if ( ( m = match[ 1 ] ) ) { - - // Document context - if ( nodeType === 9 ) { - if ( ( elem = context.getElementById( m ) ) ) { - - // Support: IE, Opera, Webkit - // TODO: identify versions - // getElementById can match elements by name instead of ID - if ( elem.id === m ) { - results.push( elem ); - return results; - } - } else { - return results; - } - - // Element context - } else { - - // Support: IE, Opera, Webkit - // TODO: identify versions - // getElementById can match elements by name instead of ID - if ( newContext && ( elem = newContext.getElementById( m ) ) && - contains( context, elem ) && - elem.id === m ) { - - results.push( elem ); - return results; - } - } - - // Type selector - } else if ( match[ 2 ] ) { - push.apply( results, context.getElementsByTagName( selector ) ); - return results; - - // Class selector - } else if ( ( m = match[ 3 ] ) && support.getElementsByClassName && - context.getElementsByClassName ) { - - push.apply( results, context.getElementsByClassName( m ) ); - return results; - } - } - - // Take advantage of querySelectorAll - if ( support.qsa && - !nonnativeSelectorCache[ selector + " " ] && - ( !rbuggyQSA || !rbuggyQSA.test( selector ) ) && - - // Support: IE 8 only - // Exclude object elements - ( nodeType !== 1 || context.nodeName.toLowerCase() !== "object" ) ) { - - newSelector = selector; - newContext = context; - - // qSA considers elements outside a scoping root when evaluating child or - // descendant combinators, which is not what we want. - // In such cases, we work around the behavior by prefixing every selector in the - // list with an ID selector referencing the scope context. - // The technique has to be used as well when a leading combinator is used - // as such selectors are not recognized by querySelectorAll. - // Thanks to Andrew Dupont for this technique. - if ( nodeType === 1 && - ( rdescend.test( selector ) || rcombinators.test( selector ) ) ) { - - // Expand context for sibling selectors - newContext = rsibling.test( selector ) && testContext( context.parentNode ) || - context; - - // We can use :scope instead of the ID hack if the browser - // supports it & if we're not changing the context. - if ( newContext !== context || !support.scope ) { - - // Capture the context ID, setting it first if necessary - if ( ( nid = context.getAttribute( "id" ) ) ) { - nid = nid.replace( rcssescape, fcssescape ); - } else { - context.setAttribute( "id", ( nid = expando ) ); - } - } - - // Prefix every selector in the list - groups = tokenize( selector ); - i = groups.length; - while ( i-- ) { - groups[ i ] = ( nid ? "#" + nid : ":scope" ) + " " + - toSelector( groups[ i ] ); - } - newSelector = groups.join( "," ); - } - - try { - push.apply( results, - newContext.querySelectorAll( newSelector ) - ); - return results; - } catch ( qsaError ) { - nonnativeSelectorCache( selector, true ); - } finally { - if ( nid === expando ) { - context.removeAttribute( "id" ); - } - } - } - } - } - - // All others - return select( selector.replace( rtrim, "$1" ), context, results, seed ); -} - -/** - * Create key-value caches of limited size - * @returns {function(string, object)} Returns the Object data after storing it on itself with - * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) - * deleting the oldest entry - */ -function createCache() { - var keys = []; - - function cache( key, value ) { - - // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) - if ( keys.push( key + " " ) > Expr.cacheLength ) { - - // Only keep the most recent entries - delete cache[ keys.shift() ]; - } - return ( cache[ key + " " ] = value ); - } - return cache; -} - -/** - * Mark a function for special use by Sizzle - * @param {Function} fn The function to mark - */ -function markFunction( fn ) { - fn[ expando ] = true; - return fn; -} - -/** - * Support testing using an element - * @param {Function} fn Passed the created element and returns a boolean result - */ -function assert( fn ) { - var el = document.createElement( "fieldset" ); - - try { - return !!fn( el ); - } catch ( e ) { - return false; - } finally { - - // Remove from its parent by default - if ( el.parentNode ) { - el.parentNode.removeChild( el ); - } - - // release memory in IE - el = null; - } -} - -/** - * Adds the same handler for all of the specified attrs - * @param {String} attrs Pipe-separated list of attributes - * @param {Function} handler The method that will be applied - */ -function addHandle( attrs, handler ) { - var arr = attrs.split( "|" ), - i = arr.length; - - while ( i-- ) { - Expr.attrHandle[ arr[ i ] ] = handler; - } -} - -/** - * Checks document order of two siblings - * @param {Element} a - * @param {Element} b - * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b - */ -function siblingCheck( a, b ) { - var cur = b && a, - diff = cur && a.nodeType === 1 && b.nodeType === 1 && - a.sourceIndex - b.sourceIndex; - - // Use IE sourceIndex if available on both nodes - if ( diff ) { - return diff; - } - - // Check if b follows a - if ( cur ) { - while ( ( cur = cur.nextSibling ) ) { - if ( cur === b ) { - return -1; - } - } - } - - return a ? 1 : -1; -} - -/** - * Returns a function to use in pseudos for input types - * @param {String} type - */ -function createInputPseudo( type ) { - return function( elem ) { - var name = elem.nodeName.toLowerCase(); - return name === "input" && elem.type === type; - }; -} - -/** - * Returns a function to use in pseudos for buttons - * @param {String} type - */ -function createButtonPseudo( type ) { - return function( elem ) { - var name = elem.nodeName.toLowerCase(); - return ( name === "input" || name === "button" ) && elem.type === type; - }; -} - -/** - * Returns a function to use in pseudos for :enabled/:disabled - * @param {Boolean} disabled true for :disabled; false for :enabled - */ -function createDisabledPseudo( disabled ) { - - // Known :disabled false positives: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable - return function( elem ) { - - // Only certain elements can match :enabled or :disabled - // https://html.spec.whatwg.org/multipage/scripting.html#selector-enabled - // https://html.spec.whatwg.org/multipage/scripting.html#selector-disabled - if ( "form" in elem ) { - - // Check for inherited disabledness on relevant non-disabled elements: - // * listed form-associated elements in a disabled fieldset - // https://html.spec.whatwg.org/multipage/forms.html#category-listed - // https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled - // * option elements in a disabled optgroup - // https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled - // All such elements have a "form" property. - if ( elem.parentNode && elem.disabled === false ) { - - // Option elements defer to a parent optgroup if present - if ( "label" in elem ) { - if ( "label" in elem.parentNode ) { - return elem.parentNode.disabled === disabled; - } else { - return elem.disabled === disabled; - } - } - - // Support: IE 6 - 11 - // Use the isDisabled shortcut property to check for disabled fieldset ancestors - return elem.isDisabled === disabled || - - // Where there is no isDisabled, check manually - /* jshint -W018 */ - elem.isDisabled !== !disabled && - inDisabledFieldset( elem ) === disabled; - } - - return elem.disabled === disabled; - - // Try to winnow out elements that can't be disabled before trusting the disabled property. - // Some victims get caught in our net (label, legend, menu, track), but it shouldn't - // even exist on them, let alone have a boolean value. - } else if ( "label" in elem ) { - return elem.disabled === disabled; - } - - // Remaining elements are neither :enabled nor :disabled - return false; - }; -} - -/** - * Returns a function to use in pseudos for positionals - * @param {Function} fn - */ -function createPositionalPseudo( fn ) { - return markFunction( function( argument ) { - argument = +argument; - return markFunction( function( seed, matches ) { - var j, - matchIndexes = fn( [], seed.length, argument ), - i = matchIndexes.length; - - // Match elements found at the specified indexes - while ( i-- ) { - if ( seed[ ( j = matchIndexes[ i ] ) ] ) { - seed[ j ] = !( matches[ j ] = seed[ j ] ); - } - } - } ); - } ); -} - -/** - * Checks a node for validity as a Sizzle context - * @param {Element|Object=} context - * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value - */ -function testContext( context ) { - return context && typeof context.getElementsByTagName !== "undefined" && context; -} - -// Expose support vars for convenience -support = Sizzle.support = {}; - -/** - * Detects XML nodes - * @param {Element|Object} elem An element or a document - * @returns {Boolean} True iff elem is a non-HTML XML node - */ -isXML = Sizzle.isXML = function( elem ) { - var namespace = elem && elem.namespaceURI, - docElem = elem && ( elem.ownerDocument || elem ).documentElement; - - // Support: IE <=8 - // Assume HTML when documentElement doesn't yet exist, such as inside loading iframes - // https://bugs.jquery.com/ticket/4833 - return !rhtml.test( namespace || docElem && docElem.nodeName || "HTML" ); -}; - -/** - * Sets document-related variables once based on the current document - * @param {Element|Object} [doc] An element or document object to use to set the document - * @returns {Object} Returns the current document - */ -setDocument = Sizzle.setDocument = function( node ) { - var hasCompare, subWindow, - doc = node ? node.ownerDocument || node : preferredDoc; - - // Return early if doc is invalid or already selected - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( doc == document || doc.nodeType !== 9 || !doc.documentElement ) { - return document; - } - - // Update global variables - document = doc; - docElem = document.documentElement; - documentIsHTML = !isXML( document ); - - // Support: IE 9 - 11+, Edge 12 - 18+ - // Accessing iframe documents after unload throws "permission denied" errors (jQuery #13936) - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( preferredDoc != document && - ( subWindow = document.defaultView ) && subWindow.top !== subWindow ) { - - // Support: IE 11, Edge - if ( subWindow.addEventListener ) { - subWindow.addEventListener( "unload", unloadHandler, false ); - - // Support: IE 9 - 10 only - } else if ( subWindow.attachEvent ) { - subWindow.attachEvent( "onunload", unloadHandler ); - } - } - - // Support: IE 8 - 11+, Edge 12 - 18+, Chrome <=16 - 25 only, Firefox <=3.6 - 31 only, - // Safari 4 - 5 only, Opera <=11.6 - 12.x only - // IE/Edge & older browsers don't support the :scope pseudo-class. - // Support: Safari 6.0 only - // Safari 6.0 supports :scope but it's an alias of :root there. - support.scope = assert( function( el ) { - docElem.appendChild( el ).appendChild( document.createElement( "div" ) ); - return typeof el.querySelectorAll !== "undefined" && - !el.querySelectorAll( ":scope fieldset div" ).length; - } ); - - /* Attributes - ---------------------------------------------------------------------- */ - - // Support: IE<8 - // Verify that getAttribute really returns attributes and not properties - // (excepting IE8 booleans) - support.attributes = assert( function( el ) { - el.className = "i"; - return !el.getAttribute( "className" ); - } ); - - /* getElement(s)By* - ---------------------------------------------------------------------- */ - - // Check if getElementsByTagName("*") returns only elements - support.getElementsByTagName = assert( function( el ) { - el.appendChild( document.createComment( "" ) ); - return !el.getElementsByTagName( "*" ).length; - } ); - - // Support: IE<9 - support.getElementsByClassName = rnative.test( document.getElementsByClassName ); - - // Support: IE<10 - // Check if getElementById returns elements by name - // The broken getElementById methods don't pick up programmatically-set names, - // so use a roundabout getElementsByName test - support.getById = assert( function( el ) { - docElem.appendChild( el ).id = expando; - return !document.getElementsByName || !document.getElementsByName( expando ).length; - } ); - - // ID filter and find - if ( support.getById ) { - Expr.filter[ "ID" ] = function( id ) { - var attrId = id.replace( runescape, funescape ); - return function( elem ) { - return elem.getAttribute( "id" ) === attrId; - }; - }; - Expr.find[ "ID" ] = function( id, context ) { - if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { - var elem = context.getElementById( id ); - return elem ? [ elem ] : []; - } - }; - } else { - Expr.filter[ "ID" ] = function( id ) { - var attrId = id.replace( runescape, funescape ); - return function( elem ) { - var node = typeof elem.getAttributeNode !== "undefined" && - elem.getAttributeNode( "id" ); - return node && node.value === attrId; - }; - }; - - // Support: IE 6 - 7 only - // getElementById is not reliable as a find shortcut - Expr.find[ "ID" ] = function( id, context ) { - if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { - var node, i, elems, - elem = context.getElementById( id ); - - if ( elem ) { - - // Verify the id attribute - node = elem.getAttributeNode( "id" ); - if ( node && node.value === id ) { - return [ elem ]; - } - - // Fall back on getElementsByName - elems = context.getElementsByName( id ); - i = 0; - while ( ( elem = elems[ i++ ] ) ) { - node = elem.getAttributeNode( "id" ); - if ( node && node.value === id ) { - return [ elem ]; - } - } - } - - return []; - } - }; - } - - // Tag - Expr.find[ "TAG" ] = support.getElementsByTagName ? - function( tag, context ) { - if ( typeof context.getElementsByTagName !== "undefined" ) { - return context.getElementsByTagName( tag ); - - // DocumentFragment nodes don't have gEBTN - } else if ( support.qsa ) { - return context.querySelectorAll( tag ); - } - } : - - function( tag, context ) { - var elem, - tmp = [], - i = 0, - - // By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too - results = context.getElementsByTagName( tag ); - - // Filter out possible comments - if ( tag === "*" ) { - while ( ( elem = results[ i++ ] ) ) { - if ( elem.nodeType === 1 ) { - tmp.push( elem ); - } - } - - return tmp; - } - return results; - }; - - // Class - Expr.find[ "CLASS" ] = support.getElementsByClassName && function( className, context ) { - if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) { - return context.getElementsByClassName( className ); - } - }; - - /* QSA/matchesSelector - ---------------------------------------------------------------------- */ - - // QSA and matchesSelector support - - // matchesSelector(:active) reports false when true (IE9/Opera 11.5) - rbuggyMatches = []; - - // qSa(:focus) reports false when true (Chrome 21) - // We allow this because of a bug in IE8/9 that throws an error - // whenever `document.activeElement` is accessed on an iframe - // So, we allow :focus to pass through QSA all the time to avoid the IE error - // See https://bugs.jquery.com/ticket/13378 - rbuggyQSA = []; - - if ( ( support.qsa = rnative.test( document.querySelectorAll ) ) ) { - - // Build QSA regex - // Regex strategy adopted from Diego Perini - assert( function( el ) { - - var input; - - // Select is set to empty string on purpose - // This is to test IE's treatment of not explicitly - // setting a boolean content attribute, - // since its presence should be enough - // https://bugs.jquery.com/ticket/12359 - docElem.appendChild( el ).innerHTML = "" + - ""; - - // Support: IE8, Opera 11-12.16 - // Nothing should be selected when empty strings follow ^= or $= or *= - // The test attribute must be unknown in Opera but "safe" for WinRT - // https://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section - if ( el.querySelectorAll( "[msallowcapture^='']" ).length ) { - rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); - } - - // Support: IE8 - // Boolean attributes and "value" are not treated correctly - if ( !el.querySelectorAll( "[selected]" ).length ) { - rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); - } - - // Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+ - if ( !el.querySelectorAll( "[id~=" + expando + "-]" ).length ) { - rbuggyQSA.push( "~=" ); - } - - // Support: IE 11+, Edge 15 - 18+ - // IE 11/Edge don't find elements on a `[name='']` query in some cases. - // Adding a temporary attribute to the document before the selection works - // around the issue. - // Interestingly, IE 10 & older don't seem to have the issue. - input = document.createElement( "input" ); - input.setAttribute( "name", "" ); - el.appendChild( input ); - if ( !el.querySelectorAll( "[name='']" ).length ) { - rbuggyQSA.push( "\\[" + whitespace + "*name" + whitespace + "*=" + - whitespace + "*(?:''|\"\")" ); - } - - // Webkit/Opera - :checked should return selected option elements - // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked - // IE8 throws error here and will not see later tests - if ( !el.querySelectorAll( ":checked" ).length ) { - rbuggyQSA.push( ":checked" ); - } - - // Support: Safari 8+, iOS 8+ - // https://bugs.webkit.org/show_bug.cgi?id=136851 - // In-page `selector#id sibling-combinator selector` fails - if ( !el.querySelectorAll( "a#" + expando + "+*" ).length ) { - rbuggyQSA.push( ".#.+[+~]" ); - } - - // Support: Firefox <=3.6 - 5 only - // Old Firefox doesn't throw on a badly-escaped identifier. - el.querySelectorAll( "\\\f" ); - rbuggyQSA.push( "[\\r\\n\\f]" ); - } ); - - assert( function( el ) { - el.innerHTML = "" + - ""; - - // Support: Windows 8 Native Apps - // The type and name attributes are restricted during .innerHTML assignment - var input = document.createElement( "input" ); - input.setAttribute( "type", "hidden" ); - el.appendChild( input ).setAttribute( "name", "D" ); - - // Support: IE8 - // Enforce case-sensitivity of name attribute - if ( el.querySelectorAll( "[name=d]" ).length ) { - rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" ); - } - - // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) - // IE8 throws error here and will not see later tests - if ( el.querySelectorAll( ":enabled" ).length !== 2 ) { - rbuggyQSA.push( ":enabled", ":disabled" ); - } - - // Support: IE9-11+ - // IE's :disabled selector does not pick up the children of disabled fieldsets - docElem.appendChild( el ).disabled = true; - if ( el.querySelectorAll( ":disabled" ).length !== 2 ) { - rbuggyQSA.push( ":enabled", ":disabled" ); - } - - // Support: Opera 10 - 11 only - // Opera 10-11 does not throw on post-comma invalid pseudos - el.querySelectorAll( "*,:x" ); - rbuggyQSA.push( ",.*:" ); - } ); - } - - if ( ( support.matchesSelector = rnative.test( ( matches = docElem.matches || - docElem.webkitMatchesSelector || - docElem.mozMatchesSelector || - docElem.oMatchesSelector || - docElem.msMatchesSelector ) ) ) ) { - - assert( function( el ) { - - // Check to see if it's possible to do matchesSelector - // on a disconnected node (IE 9) - support.disconnectedMatch = matches.call( el, "*" ); - - // This should fail with an exception - // Gecko does not error, returns false instead - matches.call( el, "[s!='']:x" ); - rbuggyMatches.push( "!=", pseudos ); - } ); - } - - rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join( "|" ) ); - rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join( "|" ) ); - - /* Contains - ---------------------------------------------------------------------- */ - hasCompare = rnative.test( docElem.compareDocumentPosition ); - - // Element contains another - // Purposefully self-exclusive - // As in, an element does not contain itself - contains = hasCompare || rnative.test( docElem.contains ) ? - function( a, b ) { - var adown = a.nodeType === 9 ? a.documentElement : a, - bup = b && b.parentNode; - return a === bup || !!( bup && bup.nodeType === 1 && ( - adown.contains ? - adown.contains( bup ) : - a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 - ) ); - } : - function( a, b ) { - if ( b ) { - while ( ( b = b.parentNode ) ) { - if ( b === a ) { - return true; - } - } - } - return false; - }; - - /* Sorting - ---------------------------------------------------------------------- */ - - // Document order sorting - sortOrder = hasCompare ? - function( a, b ) { - - // Flag for duplicate removal - if ( a === b ) { - hasDuplicate = true; - return 0; - } - - // Sort on method existence if only one input has compareDocumentPosition - var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; - if ( compare ) { - return compare; - } - - // Calculate position if both inputs belong to the same document - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - compare = ( a.ownerDocument || a ) == ( b.ownerDocument || b ) ? - a.compareDocumentPosition( b ) : - - // Otherwise we know they are disconnected - 1; - - // Disconnected nodes - if ( compare & 1 || - ( !support.sortDetached && b.compareDocumentPosition( a ) === compare ) ) { - - // Choose the first element that is related to our preferred document - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( a == document || a.ownerDocument == preferredDoc && - contains( preferredDoc, a ) ) { - return -1; - } - - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( b == document || b.ownerDocument == preferredDoc && - contains( preferredDoc, b ) ) { - return 1; - } - - // Maintain original order - return sortInput ? - ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : - 0; - } - - return compare & 4 ? -1 : 1; - } : - function( a, b ) { - - // Exit early if the nodes are identical - if ( a === b ) { - hasDuplicate = true; - return 0; - } - - var cur, - i = 0, - aup = a.parentNode, - bup = b.parentNode, - ap = [ a ], - bp = [ b ]; - - // Parentless nodes are either documents or disconnected - if ( !aup || !bup ) { - - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - /* eslint-disable eqeqeq */ - return a == document ? -1 : - b == document ? 1 : - /* eslint-enable eqeqeq */ - aup ? -1 : - bup ? 1 : - sortInput ? - ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : - 0; - - // If the nodes are siblings, we can do a quick check - } else if ( aup === bup ) { - return siblingCheck( a, b ); - } - - // Otherwise we need full lists of their ancestors for comparison - cur = a; - while ( ( cur = cur.parentNode ) ) { - ap.unshift( cur ); - } - cur = b; - while ( ( cur = cur.parentNode ) ) { - bp.unshift( cur ); - } - - // Walk down the tree looking for a discrepancy - while ( ap[ i ] === bp[ i ] ) { - i++; - } - - return i ? - - // Do a sibling check if the nodes have a common ancestor - siblingCheck( ap[ i ], bp[ i ] ) : - - // Otherwise nodes in our document sort first - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - /* eslint-disable eqeqeq */ - ap[ i ] == preferredDoc ? -1 : - bp[ i ] == preferredDoc ? 1 : - /* eslint-enable eqeqeq */ - 0; - }; - - return document; -}; - -Sizzle.matches = function( expr, elements ) { - return Sizzle( expr, null, null, elements ); -}; - -Sizzle.matchesSelector = function( elem, expr ) { - setDocument( elem ); - - if ( support.matchesSelector && documentIsHTML && - !nonnativeSelectorCache[ expr + " " ] && - ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) && - ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { - - try { - var ret = matches.call( elem, expr ); - - // IE 9's matchesSelector returns false on disconnected nodes - if ( ret || support.disconnectedMatch || - - // As well, disconnected nodes are said to be in a document - // fragment in IE 9 - elem.document && elem.document.nodeType !== 11 ) { - return ret; - } - } catch ( e ) { - nonnativeSelectorCache( expr, true ); - } - } - - return Sizzle( expr, document, null, [ elem ] ).length > 0; -}; - -Sizzle.contains = function( context, elem ) { - - // Set document vars if needed - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( ( context.ownerDocument || context ) != document ) { - setDocument( context ); - } - return contains( context, elem ); -}; - -Sizzle.attr = function( elem, name ) { - - // Set document vars if needed - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( ( elem.ownerDocument || elem ) != document ) { - setDocument( elem ); - } - - var fn = Expr.attrHandle[ name.toLowerCase() ], - - // Don't get fooled by Object.prototype properties (jQuery #13807) - val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? - fn( elem, name, !documentIsHTML ) : - undefined; - - return val !== undefined ? - val : - support.attributes || !documentIsHTML ? - elem.getAttribute( name ) : - ( val = elem.getAttributeNode( name ) ) && val.specified ? - val.value : - null; -}; - -Sizzle.escape = function( sel ) { - return ( sel + "" ).replace( rcssescape, fcssescape ); -}; - -Sizzle.error = function( msg ) { - throw new Error( "Syntax error, unrecognized expression: " + msg ); -}; - -/** - * Document sorting and removing duplicates - * @param {ArrayLike} results - */ -Sizzle.uniqueSort = function( results ) { - var elem, - duplicates = [], - j = 0, - i = 0; - - // Unless we *know* we can detect duplicates, assume their presence - hasDuplicate = !support.detectDuplicates; - sortInput = !support.sortStable && results.slice( 0 ); - results.sort( sortOrder ); - - if ( hasDuplicate ) { - while ( ( elem = results[ i++ ] ) ) { - if ( elem === results[ i ] ) { - j = duplicates.push( i ); - } - } - while ( j-- ) { - results.splice( duplicates[ j ], 1 ); - } - } - - // Clear input after sorting to release objects - // See https://github.com/jquery/sizzle/pull/225 - sortInput = null; - - return results; -}; - -/** - * Utility function for retrieving the text value of an array of DOM nodes - * @param {Array|Element} elem - */ -getText = Sizzle.getText = function( elem ) { - var node, - ret = "", - i = 0, - nodeType = elem.nodeType; - - if ( !nodeType ) { - - // If no nodeType, this is expected to be an array - while ( ( node = elem[ i++ ] ) ) { - - // Do not traverse comment nodes - ret += getText( node ); - } - } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { - - // Use textContent for elements - // innerText usage removed for consistency of new lines (jQuery #11153) - if ( typeof elem.textContent === "string" ) { - return elem.textContent; - } else { - - // Traverse its children - for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { - ret += getText( elem ); - } - } - } else if ( nodeType === 3 || nodeType === 4 ) { - return elem.nodeValue; - } - - // Do not include comment or processing instruction nodes - - return ret; -}; - -Expr = Sizzle.selectors = { - - // Can be adjusted by the user - cacheLength: 50, - - createPseudo: markFunction, - - match: matchExpr, - - attrHandle: {}, - - find: {}, - - relative: { - ">": { dir: "parentNode", first: true }, - " ": { dir: "parentNode" }, - "+": { dir: "previousSibling", first: true }, - "~": { dir: "previousSibling" } - }, - - preFilter: { - "ATTR": function( match ) { - match[ 1 ] = match[ 1 ].replace( runescape, funescape ); - - // Move the given value to match[3] whether quoted or unquoted - match[ 3 ] = ( match[ 3 ] || match[ 4 ] || - match[ 5 ] || "" ).replace( runescape, funescape ); - - if ( match[ 2 ] === "~=" ) { - match[ 3 ] = " " + match[ 3 ] + " "; - } - - return match.slice( 0, 4 ); - }, - - "CHILD": function( match ) { - - /* matches from matchExpr["CHILD"] - 1 type (only|nth|...) - 2 what (child|of-type) - 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) - 4 xn-component of xn+y argument ([+-]?\d*n|) - 5 sign of xn-component - 6 x of xn-component - 7 sign of y-component - 8 y of y-component - */ - match[ 1 ] = match[ 1 ].toLowerCase(); - - if ( match[ 1 ].slice( 0, 3 ) === "nth" ) { - - // nth-* requires argument - if ( !match[ 3 ] ) { - Sizzle.error( match[ 0 ] ); - } - - // numeric x and y parameters for Expr.filter.CHILD - // remember that false/true cast respectively to 0/1 - match[ 4 ] = +( match[ 4 ] ? - match[ 5 ] + ( match[ 6 ] || 1 ) : - 2 * ( match[ 3 ] === "even" || match[ 3 ] === "odd" ) ); - match[ 5 ] = +( ( match[ 7 ] + match[ 8 ] ) || match[ 3 ] === "odd" ); - - // other types prohibit arguments - } else if ( match[ 3 ] ) { - Sizzle.error( match[ 0 ] ); - } - - return match; - }, - - "PSEUDO": function( match ) { - var excess, - unquoted = !match[ 6 ] && match[ 2 ]; - - if ( matchExpr[ "CHILD" ].test( match[ 0 ] ) ) { - return null; - } - - // Accept quoted arguments as-is - if ( match[ 3 ] ) { - match[ 2 ] = match[ 4 ] || match[ 5 ] || ""; - - // Strip excess characters from unquoted arguments - } else if ( unquoted && rpseudo.test( unquoted ) && - - // Get excess from tokenize (recursively) - ( excess = tokenize( unquoted, true ) ) && - - // advance to the next closing parenthesis - ( excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length ) ) { - - // excess is a negative index - match[ 0 ] = match[ 0 ].slice( 0, excess ); - match[ 2 ] = unquoted.slice( 0, excess ); - } - - // Return only captures needed by the pseudo filter method (type and argument) - return match.slice( 0, 3 ); - } - }, - - filter: { - - "TAG": function( nodeNameSelector ) { - var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); - return nodeNameSelector === "*" ? - function() { - return true; - } : - function( elem ) { - return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; - }; - }, - - "CLASS": function( className ) { - var pattern = classCache[ className + " " ]; - - return pattern || - ( pattern = new RegExp( "(^|" + whitespace + - ")" + className + "(" + whitespace + "|$)" ) ) && classCache( - className, function( elem ) { - return pattern.test( - typeof elem.className === "string" && elem.className || - typeof elem.getAttribute !== "undefined" && - elem.getAttribute( "class" ) || - "" - ); - } ); - }, - - "ATTR": function( name, operator, check ) { - return function( elem ) { - var result = Sizzle.attr( elem, name ); - - if ( result == null ) { - return operator === "!="; - } - if ( !operator ) { - return true; - } - - result += ""; - - /* eslint-disable max-len */ - - return operator === "=" ? result === check : - operator === "!=" ? result !== check : - operator === "^=" ? check && result.indexOf( check ) === 0 : - operator === "*=" ? check && result.indexOf( check ) > -1 : - operator === "$=" ? check && result.slice( -check.length ) === check : - operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 : - operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : - false; - /* eslint-enable max-len */ - - }; - }, - - "CHILD": function( type, what, _argument, first, last ) { - var simple = type.slice( 0, 3 ) !== "nth", - forward = type.slice( -4 ) !== "last", - ofType = what === "of-type"; - - return first === 1 && last === 0 ? - - // Shortcut for :nth-*(n) - function( elem ) { - return !!elem.parentNode; - } : - - function( elem, _context, xml ) { - var cache, uniqueCache, outerCache, node, nodeIndex, start, - dir = simple !== forward ? "nextSibling" : "previousSibling", - parent = elem.parentNode, - name = ofType && elem.nodeName.toLowerCase(), - useCache = !xml && !ofType, - diff = false; - - if ( parent ) { - - // :(first|last|only)-(child|of-type) - if ( simple ) { - while ( dir ) { - node = elem; - while ( ( node = node[ dir ] ) ) { - if ( ofType ? - node.nodeName.toLowerCase() === name : - node.nodeType === 1 ) { - - return false; - } - } - - // Reverse direction for :only-* (if we haven't yet done so) - start = dir = type === "only" && !start && "nextSibling"; - } - return true; - } - - start = [ forward ? parent.firstChild : parent.lastChild ]; - - // non-xml :nth-child(...) stores cache data on `parent` - if ( forward && useCache ) { - - // Seek `elem` from a previously-cached index - - // ...in a gzip-friendly way - node = parent; - outerCache = node[ expando ] || ( node[ expando ] = {} ); - - // Support: IE <9 only - // Defend against cloned attroperties (jQuery gh-1709) - uniqueCache = outerCache[ node.uniqueID ] || - ( outerCache[ node.uniqueID ] = {} ); - - cache = uniqueCache[ type ] || []; - nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; - diff = nodeIndex && cache[ 2 ]; - node = nodeIndex && parent.childNodes[ nodeIndex ]; - - while ( ( node = ++nodeIndex && node && node[ dir ] || - - // Fallback to seeking `elem` from the start - ( diff = nodeIndex = 0 ) || start.pop() ) ) { - - // When found, cache indexes on `parent` and break - if ( node.nodeType === 1 && ++diff && node === elem ) { - uniqueCache[ type ] = [ dirruns, nodeIndex, diff ]; - break; - } - } - - } else { - - // Use previously-cached element index if available - if ( useCache ) { - - // ...in a gzip-friendly way - node = elem; - outerCache = node[ expando ] || ( node[ expando ] = {} ); - - // Support: IE <9 only - // Defend against cloned attroperties (jQuery gh-1709) - uniqueCache = outerCache[ node.uniqueID ] || - ( outerCache[ node.uniqueID ] = {} ); - - cache = uniqueCache[ type ] || []; - nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; - diff = nodeIndex; - } - - // xml :nth-child(...) - // or :nth-last-child(...) or :nth(-last)?-of-type(...) - if ( diff === false ) { - - // Use the same loop as above to seek `elem` from the start - while ( ( node = ++nodeIndex && node && node[ dir ] || - ( diff = nodeIndex = 0 ) || start.pop() ) ) { - - if ( ( ofType ? - node.nodeName.toLowerCase() === name : - node.nodeType === 1 ) && - ++diff ) { - - // Cache the index of each encountered element - if ( useCache ) { - outerCache = node[ expando ] || - ( node[ expando ] = {} ); - - // Support: IE <9 only - // Defend against cloned attroperties (jQuery gh-1709) - uniqueCache = outerCache[ node.uniqueID ] || - ( outerCache[ node.uniqueID ] = {} ); - - uniqueCache[ type ] = [ dirruns, diff ]; - } - - if ( node === elem ) { - break; - } - } - } - } - } - - // Incorporate the offset, then check against cycle size - diff -= last; - return diff === first || ( diff % first === 0 && diff / first >= 0 ); - } - }; - }, - - "PSEUDO": function( pseudo, argument ) { - - // pseudo-class names are case-insensitive - // http://www.w3.org/TR/selectors/#pseudo-classes - // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters - // Remember that setFilters inherits from pseudos - var args, - fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || - Sizzle.error( "unsupported pseudo: " + pseudo ); - - // The user may use createPseudo to indicate that - // arguments are needed to create the filter function - // just as Sizzle does - if ( fn[ expando ] ) { - return fn( argument ); - } - - // But maintain support for old signatures - if ( fn.length > 1 ) { - args = [ pseudo, pseudo, "", argument ]; - return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? - markFunction( function( seed, matches ) { - var idx, - matched = fn( seed, argument ), - i = matched.length; - while ( i-- ) { - idx = indexOf( seed, matched[ i ] ); - seed[ idx ] = !( matches[ idx ] = matched[ i ] ); - } - } ) : - function( elem ) { - return fn( elem, 0, args ); - }; - } - - return fn; - } - }, - - pseudos: { - - // Potentially complex pseudos - "not": markFunction( function( selector ) { - - // Trim the selector passed to compile - // to avoid treating leading and trailing - // spaces as combinators - var input = [], - results = [], - matcher = compile( selector.replace( rtrim, "$1" ) ); - - return matcher[ expando ] ? - markFunction( function( seed, matches, _context, xml ) { - var elem, - unmatched = matcher( seed, null, xml, [] ), - i = seed.length; - - // Match elements unmatched by `matcher` - while ( i-- ) { - if ( ( elem = unmatched[ i ] ) ) { - seed[ i ] = !( matches[ i ] = elem ); - } - } - } ) : - function( elem, _context, xml ) { - input[ 0 ] = elem; - matcher( input, null, xml, results ); - - // Don't keep the element (issue #299) - input[ 0 ] = null; - return !results.pop(); - }; - } ), - - "has": markFunction( function( selector ) { - return function( elem ) { - return Sizzle( selector, elem ).length > 0; - }; - } ), - - "contains": markFunction( function( text ) { - text = text.replace( runescape, funescape ); - return function( elem ) { - return ( elem.textContent || getText( elem ) ).indexOf( text ) > -1; - }; - } ), - - // "Whether an element is represented by a :lang() selector - // is based solely on the element's language value - // being equal to the identifier C, - // or beginning with the identifier C immediately followed by "-". - // The matching of C against the element's language value is performed case-insensitively. - // The identifier C does not have to be a valid language name." - // http://www.w3.org/TR/selectors/#lang-pseudo - "lang": markFunction( function( lang ) { - - // lang value must be a valid identifier - if ( !ridentifier.test( lang || "" ) ) { - Sizzle.error( "unsupported lang: " + lang ); - } - lang = lang.replace( runescape, funescape ).toLowerCase(); - return function( elem ) { - var elemLang; - do { - if ( ( elemLang = documentIsHTML ? - elem.lang : - elem.getAttribute( "xml:lang" ) || elem.getAttribute( "lang" ) ) ) { - - elemLang = elemLang.toLowerCase(); - return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; - } - } while ( ( elem = elem.parentNode ) && elem.nodeType === 1 ); - return false; - }; - } ), - - // Miscellaneous - "target": function( elem ) { - var hash = window.location && window.location.hash; - return hash && hash.slice( 1 ) === elem.id; - }, - - "root": function( elem ) { - return elem === docElem; - }, - - "focus": function( elem ) { - return elem === document.activeElement && - ( !document.hasFocus || document.hasFocus() ) && - !!( elem.type || elem.href || ~elem.tabIndex ); - }, - - // Boolean properties - "enabled": createDisabledPseudo( false ), - "disabled": createDisabledPseudo( true ), - - "checked": function( elem ) { - - // In CSS3, :checked should return both checked and selected elements - // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked - var nodeName = elem.nodeName.toLowerCase(); - return ( nodeName === "input" && !!elem.checked ) || - ( nodeName === "option" && !!elem.selected ); - }, - - "selected": function( elem ) { - - // Accessing this property makes selected-by-default - // options in Safari work properly - if ( elem.parentNode ) { - // eslint-disable-next-line no-unused-expressions - elem.parentNode.selectedIndex; - } - - return elem.selected === true; - }, - - // Contents - "empty": function( elem ) { - - // http://www.w3.org/TR/selectors/#empty-pseudo - // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), - // but not by others (comment: 8; processing instruction: 7; etc.) - // nodeType < 6 works because attributes (2) do not appear as children - for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { - if ( elem.nodeType < 6 ) { - return false; - } - } - return true; - }, - - "parent": function( elem ) { - return !Expr.pseudos[ "empty" ]( elem ); - }, - - // Element/input types - "header": function( elem ) { - return rheader.test( elem.nodeName ); - }, - - "input": function( elem ) { - return rinputs.test( elem.nodeName ); - }, - - "button": function( elem ) { - var name = elem.nodeName.toLowerCase(); - return name === "input" && elem.type === "button" || name === "button"; - }, - - "text": function( elem ) { - var attr; - return elem.nodeName.toLowerCase() === "input" && - elem.type === "text" && - - // Support: IE<8 - // New HTML5 attribute values (e.g., "search") appear with elem.type === "text" - ( ( attr = elem.getAttribute( "type" ) ) == null || - attr.toLowerCase() === "text" ); - }, - - // Position-in-collection - "first": createPositionalPseudo( function() { - return [ 0 ]; - } ), - - "last": createPositionalPseudo( function( _matchIndexes, length ) { - return [ length - 1 ]; - } ), - - "eq": createPositionalPseudo( function( _matchIndexes, length, argument ) { - return [ argument < 0 ? argument + length : argument ]; - } ), - - "even": createPositionalPseudo( function( matchIndexes, length ) { - var i = 0; - for ( ; i < length; i += 2 ) { - matchIndexes.push( i ); - } - return matchIndexes; - } ), - - "odd": createPositionalPseudo( function( matchIndexes, length ) { - var i = 1; - for ( ; i < length; i += 2 ) { - matchIndexes.push( i ); - } - return matchIndexes; - } ), - - "lt": createPositionalPseudo( function( matchIndexes, length, argument ) { - var i = argument < 0 ? - argument + length : - argument > length ? - length : - argument; - for ( ; --i >= 0; ) { - matchIndexes.push( i ); - } - return matchIndexes; - } ), - - "gt": createPositionalPseudo( function( matchIndexes, length, argument ) { - var i = argument < 0 ? argument + length : argument; - for ( ; ++i < length; ) { - matchIndexes.push( i ); - } - return matchIndexes; - } ) - } -}; - -Expr.pseudos[ "nth" ] = Expr.pseudos[ "eq" ]; - -// Add button/input type pseudos -for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { - Expr.pseudos[ i ] = createInputPseudo( i ); -} -for ( i in { submit: true, reset: true } ) { - Expr.pseudos[ i ] = createButtonPseudo( i ); -} - -// Easy API for creating new setFilters -function setFilters() {} -setFilters.prototype = Expr.filters = Expr.pseudos; -Expr.setFilters = new setFilters(); - -tokenize = Sizzle.tokenize = function( selector, parseOnly ) { - var matched, match, tokens, type, - soFar, groups, preFilters, - cached = tokenCache[ selector + " " ]; - - if ( cached ) { - return parseOnly ? 0 : cached.slice( 0 ); - } - - soFar = selector; - groups = []; - preFilters = Expr.preFilter; - - while ( soFar ) { - - // Comma and first run - if ( !matched || ( match = rcomma.exec( soFar ) ) ) { - if ( match ) { - - // Don't consume trailing commas as valid - soFar = soFar.slice( match[ 0 ].length ) || soFar; - } - groups.push( ( tokens = [] ) ); - } - - matched = false; - - // Combinators - if ( ( match = rcombinators.exec( soFar ) ) ) { - matched = match.shift(); - tokens.push( { - value: matched, - - // Cast descendant combinators to space - type: match[ 0 ].replace( rtrim, " " ) - } ); - soFar = soFar.slice( matched.length ); - } - - // Filters - for ( type in Expr.filter ) { - if ( ( match = matchExpr[ type ].exec( soFar ) ) && ( !preFilters[ type ] || - ( match = preFilters[ type ]( match ) ) ) ) { - matched = match.shift(); - tokens.push( { - value: matched, - type: type, - matches: match - } ); - soFar = soFar.slice( matched.length ); - } - } - - if ( !matched ) { - break; - } - } - - // Return the length of the invalid excess - // if we're just parsing - // Otherwise, throw an error or return tokens - return parseOnly ? - soFar.length : - soFar ? - Sizzle.error( selector ) : - - // Cache the tokens - tokenCache( selector, groups ).slice( 0 ); -}; - -function toSelector( tokens ) { - var i = 0, - len = tokens.length, - selector = ""; - for ( ; i < len; i++ ) { - selector += tokens[ i ].value; - } - return selector; -} - -function addCombinator( matcher, combinator, base ) { - var dir = combinator.dir, - skip = combinator.next, - key = skip || dir, - checkNonElements = base && key === "parentNode", - doneName = done++; - - return combinator.first ? - - // Check against closest ancestor/preceding element - function( elem, context, xml ) { - while ( ( elem = elem[ dir ] ) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - return matcher( elem, context, xml ); - } - } - return false; - } : - - // Check against all ancestor/preceding elements - function( elem, context, xml ) { - var oldCache, uniqueCache, outerCache, - newCache = [ dirruns, doneName ]; - - // We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching - if ( xml ) { - while ( ( elem = elem[ dir ] ) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - if ( matcher( elem, context, xml ) ) { - return true; - } - } - } - } else { - while ( ( elem = elem[ dir ] ) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - outerCache = elem[ expando ] || ( elem[ expando ] = {} ); - - // Support: IE <9 only - // Defend against cloned attroperties (jQuery gh-1709) - uniqueCache = outerCache[ elem.uniqueID ] || - ( outerCache[ elem.uniqueID ] = {} ); - - if ( skip && skip === elem.nodeName.toLowerCase() ) { - elem = elem[ dir ] || elem; - } else if ( ( oldCache = uniqueCache[ key ] ) && - oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { - - // Assign to newCache so results back-propagate to previous elements - return ( newCache[ 2 ] = oldCache[ 2 ] ); - } else { - - // Reuse newcache so results back-propagate to previous elements - uniqueCache[ key ] = newCache; - - // A match means we're done; a fail means we have to keep checking - if ( ( newCache[ 2 ] = matcher( elem, context, xml ) ) ) { - return true; - } - } - } - } - } - return false; - }; -} - -function elementMatcher( matchers ) { - return matchers.length > 1 ? - function( elem, context, xml ) { - var i = matchers.length; - while ( i-- ) { - if ( !matchers[ i ]( elem, context, xml ) ) { - return false; - } - } - return true; - } : - matchers[ 0 ]; -} - -function multipleContexts( selector, contexts, results ) { - var i = 0, - len = contexts.length; - for ( ; i < len; i++ ) { - Sizzle( selector, contexts[ i ], results ); - } - return results; -} - -function condense( unmatched, map, filter, context, xml ) { - var elem, - newUnmatched = [], - i = 0, - len = unmatched.length, - mapped = map != null; - - for ( ; i < len; i++ ) { - if ( ( elem = unmatched[ i ] ) ) { - if ( !filter || filter( elem, context, xml ) ) { - newUnmatched.push( elem ); - if ( mapped ) { - map.push( i ); - } - } - } - } - - return newUnmatched; -} - -function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { - if ( postFilter && !postFilter[ expando ] ) { - postFilter = setMatcher( postFilter ); - } - if ( postFinder && !postFinder[ expando ] ) { - postFinder = setMatcher( postFinder, postSelector ); - } - return markFunction( function( seed, results, context, xml ) { - var temp, i, elem, - preMap = [], - postMap = [], - preexisting = results.length, - - // Get initial elements from seed or context - elems = seed || multipleContexts( - selector || "*", - context.nodeType ? [ context ] : context, - [] - ), - - // Prefilter to get matcher input, preserving a map for seed-results synchronization - matcherIn = preFilter && ( seed || !selector ) ? - condense( elems, preMap, preFilter, context, xml ) : - elems, - - matcherOut = matcher ? - - // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, - postFinder || ( seed ? preFilter : preexisting || postFilter ) ? - - // ...intermediate processing is necessary - [] : - - // ...otherwise use results directly - results : - matcherIn; - - // Find primary matches - if ( matcher ) { - matcher( matcherIn, matcherOut, context, xml ); - } - - // Apply postFilter - if ( postFilter ) { - temp = condense( matcherOut, postMap ); - postFilter( temp, [], context, xml ); - - // Un-match failing elements by moving them back to matcherIn - i = temp.length; - while ( i-- ) { - if ( ( elem = temp[ i ] ) ) { - matcherOut[ postMap[ i ] ] = !( matcherIn[ postMap[ i ] ] = elem ); - } - } - } - - if ( seed ) { - if ( postFinder || preFilter ) { - if ( postFinder ) { - - // Get the final matcherOut by condensing this intermediate into postFinder contexts - temp = []; - i = matcherOut.length; - while ( i-- ) { - if ( ( elem = matcherOut[ i ] ) ) { - - // Restore matcherIn since elem is not yet a final match - temp.push( ( matcherIn[ i ] = elem ) ); - } - } - postFinder( null, ( matcherOut = [] ), temp, xml ); - } - - // Move matched elements from seed to results to keep them synchronized - i = matcherOut.length; - while ( i-- ) { - if ( ( elem = matcherOut[ i ] ) && - ( temp = postFinder ? indexOf( seed, elem ) : preMap[ i ] ) > -1 ) { - - seed[ temp ] = !( results[ temp ] = elem ); - } - } - } - - // Add elements to results, through postFinder if defined - } else { - matcherOut = condense( - matcherOut === results ? - matcherOut.splice( preexisting, matcherOut.length ) : - matcherOut - ); - if ( postFinder ) { - postFinder( null, results, matcherOut, xml ); - } else { - push.apply( results, matcherOut ); - } - } - } ); -} - -function matcherFromTokens( tokens ) { - var checkContext, matcher, j, - len = tokens.length, - leadingRelative = Expr.relative[ tokens[ 0 ].type ], - implicitRelative = leadingRelative || Expr.relative[ " " ], - i = leadingRelative ? 1 : 0, - - // The foundational matcher ensures that elements are reachable from top-level context(s) - matchContext = addCombinator( function( elem ) { - return elem === checkContext; - }, implicitRelative, true ), - matchAnyContext = addCombinator( function( elem ) { - return indexOf( checkContext, elem ) > -1; - }, implicitRelative, true ), - matchers = [ function( elem, context, xml ) { - var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( - ( checkContext = context ).nodeType ? - matchContext( elem, context, xml ) : - matchAnyContext( elem, context, xml ) ); - - // Avoid hanging onto element (issue #299) - checkContext = null; - return ret; - } ]; - - for ( ; i < len; i++ ) { - if ( ( matcher = Expr.relative[ tokens[ i ].type ] ) ) { - matchers = [ addCombinator( elementMatcher( matchers ), matcher ) ]; - } else { - matcher = Expr.filter[ tokens[ i ].type ].apply( null, tokens[ i ].matches ); - - // Return special upon seeing a positional matcher - if ( matcher[ expando ] ) { - - // Find the next relative operator (if any) for proper handling - j = ++i; - for ( ; j < len; j++ ) { - if ( Expr.relative[ tokens[ j ].type ] ) { - break; - } - } - return setMatcher( - i > 1 && elementMatcher( matchers ), - i > 1 && toSelector( - - // If the preceding token was a descendant combinator, insert an implicit any-element `*` - tokens - .slice( 0, i - 1 ) - .concat( { value: tokens[ i - 2 ].type === " " ? "*" : "" } ) - ).replace( rtrim, "$1" ), - matcher, - i < j && matcherFromTokens( tokens.slice( i, j ) ), - j < len && matcherFromTokens( ( tokens = tokens.slice( j ) ) ), - j < len && toSelector( tokens ) - ); - } - matchers.push( matcher ); - } - } - - return elementMatcher( matchers ); -} - -function matcherFromGroupMatchers( elementMatchers, setMatchers ) { - var bySet = setMatchers.length > 0, - byElement = elementMatchers.length > 0, - superMatcher = function( seed, context, xml, results, outermost ) { - var elem, j, matcher, - matchedCount = 0, - i = "0", - unmatched = seed && [], - setMatched = [], - contextBackup = outermostContext, - - // We must always have either seed elements or outermost context - elems = seed || byElement && Expr.find[ "TAG" ]( "*", outermost ), - - // Use integer dirruns iff this is the outermost matcher - dirrunsUnique = ( dirruns += contextBackup == null ? 1 : Math.random() || 0.1 ), - len = elems.length; - - if ( outermost ) { - - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - outermostContext = context == document || context || outermost; - } - - // Add elements passing elementMatchers directly to results - // Support: IE<9, Safari - // Tolerate NodeList properties (IE: "length"; Safari: ) matching elements by id - for ( ; i !== len && ( elem = elems[ i ] ) != null; i++ ) { - if ( byElement && elem ) { - j = 0; - - // Support: IE 11+, Edge 17 - 18+ - // IE/Edge sometimes throw a "Permission denied" error when strict-comparing - // two documents; shallow comparisons work. - // eslint-disable-next-line eqeqeq - if ( !context && elem.ownerDocument != document ) { - setDocument( elem ); - xml = !documentIsHTML; - } - while ( ( matcher = elementMatchers[ j++ ] ) ) { - if ( matcher( elem, context || document, xml ) ) { - results.push( elem ); - break; - } - } - if ( outermost ) { - dirruns = dirrunsUnique; - } - } - - // Track unmatched elements for set filters - if ( bySet ) { - - // They will have gone through all possible matchers - if ( ( elem = !matcher && elem ) ) { - matchedCount--; - } - - // Lengthen the array for every element, matched or not - if ( seed ) { - unmatched.push( elem ); - } - } - } - - // `i` is now the count of elements visited above, and adding it to `matchedCount` - // makes the latter nonnegative. - matchedCount += i; - - // Apply set filters to unmatched elements - // NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount` - // equals `i`), unless we didn't visit _any_ elements in the above loop because we have - // no element matchers and no seed. - // Incrementing an initially-string "0" `i` allows `i` to remain a string only in that - // case, which will result in a "00" `matchedCount` that differs from `i` but is also - // numerically zero. - if ( bySet && i !== matchedCount ) { - j = 0; - while ( ( matcher = setMatchers[ j++ ] ) ) { - matcher( unmatched, setMatched, context, xml ); - } - - if ( seed ) { - - // Reintegrate element matches to eliminate the need for sorting - if ( matchedCount > 0 ) { - while ( i-- ) { - if ( !( unmatched[ i ] || setMatched[ i ] ) ) { - setMatched[ i ] = pop.call( results ); - } - } - } - - // Discard index placeholder values to get only actual matches - setMatched = condense( setMatched ); - } - - // Add matches to results - push.apply( results, setMatched ); - - // Seedless set matches succeeding multiple successful matchers stipulate sorting - if ( outermost && !seed && setMatched.length > 0 && - ( matchedCount + setMatchers.length ) > 1 ) { - - Sizzle.uniqueSort( results ); - } - } - - // Override manipulation of globals by nested matchers - if ( outermost ) { - dirruns = dirrunsUnique; - outermostContext = contextBackup; - } - - return unmatched; - }; - - return bySet ? - markFunction( superMatcher ) : - superMatcher; -} - -compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) { - var i, - setMatchers = [], - elementMatchers = [], - cached = compilerCache[ selector + " " ]; - - if ( !cached ) { - - // Generate a function of recursive functions that can be used to check each element - if ( !match ) { - match = tokenize( selector ); - } - i = match.length; - while ( i-- ) { - cached = matcherFromTokens( match[ i ] ); - if ( cached[ expando ] ) { - setMatchers.push( cached ); - } else { - elementMatchers.push( cached ); - } - } - - // Cache the compiled function - cached = compilerCache( - selector, - matcherFromGroupMatchers( elementMatchers, setMatchers ) - ); - - // Save selector and tokenization - cached.selector = selector; - } - return cached; -}; - -/** - * A low-level selection function that works with Sizzle's compiled - * selector functions - * @param {String|Function} selector A selector or a pre-compiled - * selector function built with Sizzle.compile - * @param {Element} context - * @param {Array} [results] - * @param {Array} [seed] A set of elements to match against - */ -select = Sizzle.select = function( selector, context, results, seed ) { - var i, tokens, token, type, find, - compiled = typeof selector === "function" && selector, - match = !seed && tokenize( ( selector = compiled.selector || selector ) ); - - results = results || []; - - // Try to minimize operations if there is only one selector in the list and no seed - // (the latter of which guarantees us context) - if ( match.length === 1 ) { - - // Reduce context if the leading compound selector is an ID - tokens = match[ 0 ] = match[ 0 ].slice( 0 ); - if ( tokens.length > 2 && ( token = tokens[ 0 ] ).type === "ID" && - context.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[ 1 ].type ] ) { - - context = ( Expr.find[ "ID" ]( token.matches[ 0 ] - .replace( runescape, funescape ), context ) || [] )[ 0 ]; - if ( !context ) { - return results; - - // Precompiled matchers will still verify ancestry, so step up a level - } else if ( compiled ) { - context = context.parentNode; - } - - selector = selector.slice( tokens.shift().value.length ); - } - - // Fetch a seed set for right-to-left matching - i = matchExpr[ "needsContext" ].test( selector ) ? 0 : tokens.length; - while ( i-- ) { - token = tokens[ i ]; - - // Abort if we hit a combinator - if ( Expr.relative[ ( type = token.type ) ] ) { - break; - } - if ( ( find = Expr.find[ type ] ) ) { - - // Search, expanding context for leading sibling combinators - if ( ( seed = find( - token.matches[ 0 ].replace( runescape, funescape ), - rsibling.test( tokens[ 0 ].type ) && testContext( context.parentNode ) || - context - ) ) ) { - - // If seed is empty or no tokens remain, we can return early - tokens.splice( i, 1 ); - selector = seed.length && toSelector( tokens ); - if ( !selector ) { - push.apply( results, seed ); - return results; - } - - break; - } - } - } - } - - // Compile and execute a filtering function if one is not provided - // Provide `match` to avoid retokenization if we modified the selector above - ( compiled || compile( selector, match ) )( - seed, - context, - !documentIsHTML, - results, - !context || rsibling.test( selector ) && testContext( context.parentNode ) || context - ); - return results; -}; - -// One-time assignments - -// Sort stability -support.sortStable = expando.split( "" ).sort( sortOrder ).join( "" ) === expando; - -// Support: Chrome 14-35+ -// Always assume duplicates if they aren't passed to the comparison function -support.detectDuplicates = !!hasDuplicate; - -// Initialize against the default document -setDocument(); - -// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27) -// Detached nodes confoundingly follow *each other* -support.sortDetached = assert( function( el ) { - - // Should return 1, but returns 4 (following) - return el.compareDocumentPosition( document.createElement( "fieldset" ) ) & 1; -} ); - -// Support: IE<8 -// Prevent attribute/property "interpolation" -// https://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx -if ( !assert( function( el ) { - el.innerHTML = ""; - return el.firstChild.getAttribute( "href" ) === "#"; -} ) ) { - addHandle( "type|href|height|width", function( elem, name, isXML ) { - if ( !isXML ) { - return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 ); - } - } ); -} - -// Support: IE<9 -// Use defaultValue in place of getAttribute("value") -if ( !support.attributes || !assert( function( el ) { - el.innerHTML = ""; - el.firstChild.setAttribute( "value", "" ); - return el.firstChild.getAttribute( "value" ) === ""; -} ) ) { - addHandle( "value", function( elem, _name, isXML ) { - if ( !isXML && elem.nodeName.toLowerCase() === "input" ) { - return elem.defaultValue; - } - } ); -} - -// Support: IE<9 -// Use getAttributeNode to fetch booleans when getAttribute lies -if ( !assert( function( el ) { - return el.getAttribute( "disabled" ) == null; -} ) ) { - addHandle( booleans, function( elem, name, isXML ) { - var val; - if ( !isXML ) { - return elem[ name ] === true ? name.toLowerCase() : - ( val = elem.getAttributeNode( name ) ) && val.specified ? - val.value : - null; - } - } ); -} - -return Sizzle; - -} )( window ); - - - -jQuery.find = Sizzle; -jQuery.expr = Sizzle.selectors; - -// Deprecated -jQuery.expr[ ":" ] = jQuery.expr.pseudos; -jQuery.uniqueSort = jQuery.unique = Sizzle.uniqueSort; -jQuery.text = Sizzle.getText; -jQuery.isXMLDoc = Sizzle.isXML; -jQuery.contains = Sizzle.contains; -jQuery.escapeSelector = Sizzle.escape; - - - - -var dir = function( elem, dir, until ) { - var matched = [], - truncate = until !== undefined; - - while ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) { - if ( elem.nodeType === 1 ) { - if ( truncate && jQuery( elem ).is( until ) ) { - break; - } - matched.push( elem ); - } - } - return matched; -}; - - -var siblings = function( n, elem ) { - var matched = []; - - for ( ; n; n = n.nextSibling ) { - if ( n.nodeType === 1 && n !== elem ) { - matched.push( n ); - } - } - - return matched; -}; - - -var rneedsContext = jQuery.expr.match.needsContext; - - - -function nodeName( elem, name ) { - - return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); - -} -var rsingleTag = ( /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i ); - - - -// Implement the identical functionality for filter and not -function winnow( elements, qualifier, not ) { - if ( isFunction( qualifier ) ) { - return jQuery.grep( elements, function( elem, i ) { - return !!qualifier.call( elem, i, elem ) !== not; - } ); - } - - // Single element - if ( qualifier.nodeType ) { - return jQuery.grep( elements, function( elem ) { - return ( elem === qualifier ) !== not; - } ); - } - - // Arraylike of elements (jQuery, arguments, Array) - if ( typeof qualifier !== "string" ) { - return jQuery.grep( elements, function( elem ) { - return ( indexOf.call( qualifier, elem ) > -1 ) !== not; - } ); - } - - // Filtered directly for both simple and complex selectors - return jQuery.filter( qualifier, elements, not ); -} - -jQuery.filter = function( expr, elems, not ) { - var elem = elems[ 0 ]; - - if ( not ) { - expr = ":not(" + expr + ")"; - } - - if ( elems.length === 1 && elem.nodeType === 1 ) { - return jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : []; - } - - return jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { - return elem.nodeType === 1; - } ) ); -}; - -jQuery.fn.extend( { - find: function( selector ) { - var i, ret, - len = this.length, - self = this; - - if ( typeof selector !== "string" ) { - return this.pushStack( jQuery( selector ).filter( function() { - for ( i = 0; i < len; i++ ) { - if ( jQuery.contains( self[ i ], this ) ) { - return true; - } - } - } ) ); - } - - ret = this.pushStack( [] ); - - for ( i = 0; i < len; i++ ) { - jQuery.find( selector, self[ i ], ret ); - } - - return len > 1 ? jQuery.uniqueSort( ret ) : ret; - }, - filter: function( selector ) { - return this.pushStack( winnow( this, selector || [], false ) ); - }, - not: function( selector ) { - return this.pushStack( winnow( this, selector || [], true ) ); - }, - is: function( selector ) { - return !!winnow( - this, - - // If this is a positional/relative selector, check membership in the returned set - // so $("p:first").is("p:last") won't return true for a doc with two "p". - typeof selector === "string" && rneedsContext.test( selector ) ? - jQuery( selector ) : - selector || [], - false - ).length; - } -} ); - - -// Initialize a jQuery object - - -// A central reference to the root jQuery(document) -var rootjQuery, - - // A simple way to check for HTML strings - // Prioritize #id over to avoid XSS via location.hash (#9521) - // Strict HTML recognition (#11290: must start with <) - // Shortcut simple #id case for speed - rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/, - - init = jQuery.fn.init = function( selector, context, root ) { - var match, elem; - - // HANDLE: $(""), $(null), $(undefined), $(false) - if ( !selector ) { - return this; - } - - // Method init() accepts an alternate rootjQuery - // so migrate can support jQuery.sub (gh-2101) - root = root || rootjQuery; - - // Handle HTML strings - if ( typeof selector === "string" ) { - if ( selector[ 0 ] === "<" && - selector[ selector.length - 1 ] === ">" && - selector.length >= 3 ) { - - // Assume that strings that start and end with <> are HTML and skip the regex check - match = [ null, selector, null ]; - - } else { - match = rquickExpr.exec( selector ); - } - - // Match html or make sure no context is specified for #id - if ( match && ( match[ 1 ] || !context ) ) { - - // HANDLE: $(html) -> $(array) - if ( match[ 1 ] ) { - context = context instanceof jQuery ? context[ 0 ] : context; - - // Option to run scripts is true for back-compat - // Intentionally let the error be thrown if parseHTML is not present - jQuery.merge( this, jQuery.parseHTML( - match[ 1 ], - context && context.nodeType ? context.ownerDocument || context : document, - true - ) ); - - // HANDLE: $(html, props) - if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) { - for ( match in context ) { - - // Properties of context are called as methods if possible - if ( isFunction( this[ match ] ) ) { - this[ match ]( context[ match ] ); - - // ...and otherwise set as attributes - } else { - this.attr( match, context[ match ] ); - } - } - } - - return this; - - // HANDLE: $(#id) - } else { - elem = document.getElementById( match[ 2 ] ); - - if ( elem ) { - - // Inject the element directly into the jQuery object - this[ 0 ] = elem; - this.length = 1; - } - return this; - } - - // HANDLE: $(expr, $(...)) - } else if ( !context || context.jquery ) { - return ( context || root ).find( selector ); - - // HANDLE: $(expr, context) - // (which is just equivalent to: $(context).find(expr) - } else { - return this.constructor( context ).find( selector ); - } - - // HANDLE: $(DOMElement) - } else if ( selector.nodeType ) { - this[ 0 ] = selector; - this.length = 1; - return this; - - // HANDLE: $(function) - // Shortcut for document ready - } else if ( isFunction( selector ) ) { - return root.ready !== undefined ? - root.ready( selector ) : - - // Execute immediately if ready is not present - selector( jQuery ); - } - - return jQuery.makeArray( selector, this ); - }; - -// Give the init function the jQuery prototype for later instantiation -init.prototype = jQuery.fn; - -// Initialize central reference -rootjQuery = jQuery( document ); - - -var rparentsprev = /^(?:parents|prev(?:Until|All))/, - - // Methods guaranteed to produce a unique set when starting from a unique set - guaranteedUnique = { - children: true, - contents: true, - next: true, - prev: true - }; - -jQuery.fn.extend( { - has: function( target ) { - var targets = jQuery( target, this ), - l = targets.length; - - return this.filter( function() { - var i = 0; - for ( ; i < l; i++ ) { - if ( jQuery.contains( this, targets[ i ] ) ) { - return true; - } - } - } ); - }, - - closest: function( selectors, context ) { - var cur, - i = 0, - l = this.length, - matched = [], - targets = typeof selectors !== "string" && jQuery( selectors ); - - // Positional selectors never match, since there's no _selection_ context - if ( !rneedsContext.test( selectors ) ) { - for ( ; i < l; i++ ) { - for ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) { - - // Always skip document fragments - if ( cur.nodeType < 11 && ( targets ? - targets.index( cur ) > -1 : - - // Don't pass non-elements to Sizzle - cur.nodeType === 1 && - jQuery.find.matchesSelector( cur, selectors ) ) ) { - - matched.push( cur ); - break; - } - } - } - } - - return this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched ); - }, - - // Determine the position of an element within the set - index: function( elem ) { - - // No argument, return index in parent - if ( !elem ) { - return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1; - } - - // Index in selector - if ( typeof elem === "string" ) { - return indexOf.call( jQuery( elem ), this[ 0 ] ); - } - - // Locate the position of the desired element - return indexOf.call( this, - - // If it receives a jQuery object, the first element is used - elem.jquery ? elem[ 0 ] : elem - ); - }, - - add: function( selector, context ) { - return this.pushStack( - jQuery.uniqueSort( - jQuery.merge( this.get(), jQuery( selector, context ) ) - ) - ); - }, - - addBack: function( selector ) { - return this.add( selector == null ? - this.prevObject : this.prevObject.filter( selector ) - ); - } -} ); - -function sibling( cur, dir ) { - while ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {} - return cur; -} - -jQuery.each( { - parent: function( elem ) { - var parent = elem.parentNode; - return parent && parent.nodeType !== 11 ? parent : null; - }, - parents: function( elem ) { - return dir( elem, "parentNode" ); - }, - parentsUntil: function( elem, _i, until ) { - return dir( elem, "parentNode", until ); - }, - next: function( elem ) { - return sibling( elem, "nextSibling" ); - }, - prev: function( elem ) { - return sibling( elem, "previousSibling" ); - }, - nextAll: function( elem ) { - return dir( elem, "nextSibling" ); - }, - prevAll: function( elem ) { - return dir( elem, "previousSibling" ); - }, - nextUntil: function( elem, _i, until ) { - return dir( elem, "nextSibling", until ); - }, - prevUntil: function( elem, _i, until ) { - return dir( elem, "previousSibling", until ); - }, - siblings: function( elem ) { - return siblings( ( elem.parentNode || {} ).firstChild, elem ); - }, - children: function( elem ) { - return siblings( elem.firstChild ); - }, - contents: function( elem ) { - if ( elem.contentDocument != null && - - // Support: IE 11+ - // elements with no `data` attribute has an object - // `contentDocument` with a `null` prototype. - getProto( elem.contentDocument ) ) { - - return elem.contentDocument; - } - - // Support: IE 9 - 11 only, iOS 7 only, Android Browser <=4.3 only - // Treat the template element as a regular one in browsers that - // don't support it. - if ( nodeName( elem, "template" ) ) { - elem = elem.content || elem; - } - - return jQuery.merge( [], elem.childNodes ); - } -}, function( name, fn ) { - jQuery.fn[ name ] = function( until, selector ) { - var matched = jQuery.map( this, fn, until ); - - if ( name.slice( -5 ) !== "Until" ) { - selector = until; - } - - if ( selector && typeof selector === "string" ) { - matched = jQuery.filter( selector, matched ); - } - - if ( this.length > 1 ) { - - // Remove duplicates - if ( !guaranteedUnique[ name ] ) { - jQuery.uniqueSort( matched ); - } - - // Reverse order for parents* and prev-derivatives - if ( rparentsprev.test( name ) ) { - matched.reverse(); - } - } - - return this.pushStack( matched ); - }; -} ); -var rnothtmlwhite = ( /[^\x20\t\r\n\f]+/g ); - - - -// Convert String-formatted options into Object-formatted ones -function createOptions( options ) { - var object = {}; - jQuery.each( options.match( rnothtmlwhite ) || [], function( _, flag ) { - object[ flag ] = true; - } ); - return object; -} - -/* - * Create a callback list using the following parameters: - * - * options: an optional list of space-separated options that will change how - * the callback list behaves or a more traditional option object - * - * By default a callback list will act like an event callback list and can be - * "fired" multiple times. - * - * Possible options: - * - * once: will ensure the callback list can only be fired once (like a Deferred) - * - * memory: will keep track of previous values and will call any callback added - * after the list has been fired right away with the latest "memorized" - * values (like a Deferred) - * - * unique: will ensure a callback can only be added once (no duplicate in the list) - * - * stopOnFalse: interrupt callings when a callback returns false - * - */ -jQuery.Callbacks = function( options ) { - - // Convert options from String-formatted to Object-formatted if needed - // (we check in cache first) - options = typeof options === "string" ? - createOptions( options ) : - jQuery.extend( {}, options ); - - var // Flag to know if list is currently firing - firing, - - // Last fire value for non-forgettable lists - memory, - - // Flag to know if list was already fired - fired, - - // Flag to prevent firing - locked, - - // Actual callback list - list = [], - - // Queue of execution data for repeatable lists - queue = [], - - // Index of currently firing callback (modified by add/remove as needed) - firingIndex = -1, - - // Fire callbacks - fire = function() { - - // Enforce single-firing - locked = locked || options.once; - - // Execute callbacks for all pending executions, - // respecting firingIndex overrides and runtime changes - fired = firing = true; - for ( ; queue.length; firingIndex = -1 ) { - memory = queue.shift(); - while ( ++firingIndex < list.length ) { - - // Run callback and check for early termination - if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false && - options.stopOnFalse ) { - - // Jump to end and forget the data so .add doesn't re-fire - firingIndex = list.length; - memory = false; - } - } - } - - // Forget the data if we're done with it - if ( !options.memory ) { - memory = false; - } - - firing = false; - - // Clean up if we're done firing for good - if ( locked ) { - - // Keep an empty list if we have data for future add calls - if ( memory ) { - list = []; - - // Otherwise, this object is spent - } else { - list = ""; - } - } - }, - - // Actual Callbacks object - self = { - - // Add a callback or a collection of callbacks to the list - add: function() { - if ( list ) { - - // If we have memory from a past run, we should fire after adding - if ( memory && !firing ) { - firingIndex = list.length - 1; - queue.push( memory ); - } - - ( function add( args ) { - jQuery.each( args, function( _, arg ) { - if ( isFunction( arg ) ) { - if ( !options.unique || !self.has( arg ) ) { - list.push( arg ); - } - } else if ( arg && arg.length && toType( arg ) !== "string" ) { - - // Inspect recursively - add( arg ); - } - } ); - } )( arguments ); - - if ( memory && !firing ) { - fire(); - } - } - return this; - }, - - // Remove a callback from the list - remove: function() { - jQuery.each( arguments, function( _, arg ) { - var index; - while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { - list.splice( index, 1 ); - - // Handle firing indexes - if ( index <= firingIndex ) { - firingIndex--; - } - } - } ); - return this; - }, - - // Check if a given callback is in the list. - // If no argument is given, return whether or not list has callbacks attached. - has: function( fn ) { - return fn ? - jQuery.inArray( fn, list ) > -1 : - list.length > 0; - }, - - // Remove all callbacks from the list - empty: function() { - if ( list ) { - list = []; - } - return this; - }, - - // Disable .fire and .add - // Abort any current/pending executions - // Clear all callbacks and values - disable: function() { - locked = queue = []; - list = memory = ""; - return this; - }, - disabled: function() { - return !list; - }, - - // Disable .fire - // Also disable .add unless we have memory (since it would have no effect) - // Abort any pending executions - lock: function() { - locked = queue = []; - if ( !memory && !firing ) { - list = memory = ""; - } - return this; - }, - locked: function() { - return !!locked; - }, - - // Call all callbacks with the given context and arguments - fireWith: function( context, args ) { - if ( !locked ) { - args = args || []; - args = [ context, args.slice ? args.slice() : args ]; - queue.push( args ); - if ( !firing ) { - fire(); - } - } - return this; - }, - - // Call all the callbacks with the given arguments - fire: function() { - self.fireWith( this, arguments ); - return this; - }, - - // To know if the callbacks have already been called at least once - fired: function() { - return !!fired; - } - }; - - return self; -}; - - -function Identity( v ) { - return v; -} -function Thrower( ex ) { - throw ex; -} - -function adoptValue( value, resolve, reject, noValue ) { - var method; - - try { - - // Check for promise aspect first to privilege synchronous behavior - if ( value && isFunction( ( method = value.promise ) ) ) { - method.call( value ).done( resolve ).fail( reject ); - - // Other thenables - } else if ( value && isFunction( ( method = value.then ) ) ) { - method.call( value, resolve, reject ); - - // Other non-thenables - } else { - - // Control `resolve` arguments by letting Array#slice cast boolean `noValue` to integer: - // * false: [ value ].slice( 0 ) => resolve( value ) - // * true: [ value ].slice( 1 ) => resolve() - resolve.apply( undefined, [ value ].slice( noValue ) ); - } - - // For Promises/A+, convert exceptions into rejections - // Since jQuery.when doesn't unwrap thenables, we can skip the extra checks appearing in - // Deferred#then to conditionally suppress rejection. - } catch ( value ) { - - // Support: Android 4.0 only - // Strict mode functions invoked without .call/.apply get global-object context - reject.apply( undefined, [ value ] ); - } -} - -jQuery.extend( { - - Deferred: function( func ) { - var tuples = [ - - // action, add listener, callbacks, - // ... .then handlers, argument index, [final state] - [ "notify", "progress", jQuery.Callbacks( "memory" ), - jQuery.Callbacks( "memory" ), 2 ], - [ "resolve", "done", jQuery.Callbacks( "once memory" ), - jQuery.Callbacks( "once memory" ), 0, "resolved" ], - [ "reject", "fail", jQuery.Callbacks( "once memory" ), - jQuery.Callbacks( "once memory" ), 1, "rejected" ] - ], - state = "pending", - promise = { - state: function() { - return state; - }, - always: function() { - deferred.done( arguments ).fail( arguments ); - return this; - }, - "catch": function( fn ) { - return promise.then( null, fn ); - }, - - // Keep pipe for back-compat - pipe: function( /* fnDone, fnFail, fnProgress */ ) { - var fns = arguments; - - return jQuery.Deferred( function( newDefer ) { - jQuery.each( tuples, function( _i, tuple ) { - - // Map tuples (progress, done, fail) to arguments (done, fail, progress) - var fn = isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ]; - - // deferred.progress(function() { bind to newDefer or newDefer.notify }) - // deferred.done(function() { bind to newDefer or newDefer.resolve }) - // deferred.fail(function() { bind to newDefer or newDefer.reject }) - deferred[ tuple[ 1 ] ]( function() { - var returned = fn && fn.apply( this, arguments ); - if ( returned && isFunction( returned.promise ) ) { - returned.promise() - .progress( newDefer.notify ) - .done( newDefer.resolve ) - .fail( newDefer.reject ); - } else { - newDefer[ tuple[ 0 ] + "With" ]( - this, - fn ? [ returned ] : arguments - ); - } - } ); - } ); - fns = null; - } ).promise(); - }, - then: function( onFulfilled, onRejected, onProgress ) { - var maxDepth = 0; - function resolve( depth, deferred, handler, special ) { - return function() { - var that = this, - args = arguments, - mightThrow = function() { - var returned, then; - - // Support: Promises/A+ section 2.3.3.3.3 - // https://promisesaplus.com/#point-59 - // Ignore double-resolution attempts - if ( depth < maxDepth ) { - return; - } - - returned = handler.apply( that, args ); - - // Support: Promises/A+ section 2.3.1 - // https://promisesaplus.com/#point-48 - if ( returned === deferred.promise() ) { - throw new TypeError( "Thenable self-resolution" ); - } - - // Support: Promises/A+ sections 2.3.3.1, 3.5 - // https://promisesaplus.com/#point-54 - // https://promisesaplus.com/#point-75 - // Retrieve `then` only once - then = returned && - - // Support: Promises/A+ section 2.3.4 - // https://promisesaplus.com/#point-64 - // Only check objects and functions for thenability - ( typeof returned === "object" || - typeof returned === "function" ) && - returned.then; - - // Handle a returned thenable - if ( isFunction( then ) ) { - - // Special processors (notify) just wait for resolution - if ( special ) { - then.call( - returned, - resolve( maxDepth, deferred, Identity, special ), - resolve( maxDepth, deferred, Thrower, special ) - ); - - // Normal processors (resolve) also hook into progress - } else { - - // ...and disregard older resolution values - maxDepth++; - - then.call( - returned, - resolve( maxDepth, deferred, Identity, special ), - resolve( maxDepth, deferred, Thrower, special ), - resolve( maxDepth, deferred, Identity, - deferred.notifyWith ) - ); - } - - // Handle all other returned values - } else { - - // Only substitute handlers pass on context - // and multiple values (non-spec behavior) - if ( handler !== Identity ) { - that = undefined; - args = [ returned ]; - } - - // Process the value(s) - // Default process is resolve - ( special || deferred.resolveWith )( that, args ); - } - }, - - // Only normal processors (resolve) catch and reject exceptions - process = special ? - mightThrow : - function() { - try { - mightThrow(); - } catch ( e ) { - - if ( jQuery.Deferred.exceptionHook ) { - jQuery.Deferred.exceptionHook( e, - process.stackTrace ); - } - - // Support: Promises/A+ section 2.3.3.3.4.1 - // https://promisesaplus.com/#point-61 - // Ignore post-resolution exceptions - if ( depth + 1 >= maxDepth ) { - - // Only substitute handlers pass on context - // and multiple values (non-spec behavior) - if ( handler !== Thrower ) { - that = undefined; - args = [ e ]; - } - - deferred.rejectWith( that, args ); - } - } - }; - - // Support: Promises/A+ section 2.3.3.3.1 - // https://promisesaplus.com/#point-57 - // Re-resolve promises immediately to dodge false rejection from - // subsequent errors - if ( depth ) { - process(); - } else { - - // Call an optional hook to record the stack, in case of exception - // since it's otherwise lost when execution goes async - if ( jQuery.Deferred.getStackHook ) { - process.stackTrace = jQuery.Deferred.getStackHook(); - } - window.setTimeout( process ); - } - }; - } - - return jQuery.Deferred( function( newDefer ) { - - // progress_handlers.add( ... ) - tuples[ 0 ][ 3 ].add( - resolve( - 0, - newDefer, - isFunction( onProgress ) ? - onProgress : - Identity, - newDefer.notifyWith - ) - ); - - // fulfilled_handlers.add( ... ) - tuples[ 1 ][ 3 ].add( - resolve( - 0, - newDefer, - isFunction( onFulfilled ) ? - onFulfilled : - Identity - ) - ); - - // rejected_handlers.add( ... ) - tuples[ 2 ][ 3 ].add( - resolve( - 0, - newDefer, - isFunction( onRejected ) ? - onRejected : - Thrower - ) - ); - } ).promise(); - }, - - // Get a promise for this deferred - // If obj is provided, the promise aspect is added to the object - promise: function( obj ) { - return obj != null ? jQuery.extend( obj, promise ) : promise; - } - }, - deferred = {}; - - // Add list-specific methods - jQuery.each( tuples, function( i, tuple ) { - var list = tuple[ 2 ], - stateString = tuple[ 5 ]; - - // promise.progress = list.add - // promise.done = list.add - // promise.fail = list.add - promise[ tuple[ 1 ] ] = list.add; - - // Handle state - if ( stateString ) { - list.add( - function() { - - // state = "resolved" (i.e., fulfilled) - // state = "rejected" - state = stateString; - }, - - // rejected_callbacks.disable - // fulfilled_callbacks.disable - tuples[ 3 - i ][ 2 ].disable, - - // rejected_handlers.disable - // fulfilled_handlers.disable - tuples[ 3 - i ][ 3 ].disable, - - // progress_callbacks.lock - tuples[ 0 ][ 2 ].lock, - - // progress_handlers.lock - tuples[ 0 ][ 3 ].lock - ); - } - - // progress_handlers.fire - // fulfilled_handlers.fire - // rejected_handlers.fire - list.add( tuple[ 3 ].fire ); - - // deferred.notify = function() { deferred.notifyWith(...) } - // deferred.resolve = function() { deferred.resolveWith(...) } - // deferred.reject = function() { deferred.rejectWith(...) } - deferred[ tuple[ 0 ] ] = function() { - deferred[ tuple[ 0 ] + "With" ]( this === deferred ? undefined : this, arguments ); - return this; - }; - - // deferred.notifyWith = list.fireWith - // deferred.resolveWith = list.fireWith - // deferred.rejectWith = list.fireWith - deferred[ tuple[ 0 ] + "With" ] = list.fireWith; - } ); - - // Make the deferred a promise - promise.promise( deferred ); - - // Call given func if any - if ( func ) { - func.call( deferred, deferred ); - } - - // All done! - return deferred; - }, - - // Deferred helper - when: function( singleValue ) { - var - - // count of uncompleted subordinates - remaining = arguments.length, - - // count of unprocessed arguments - i = remaining, - - // subordinate fulfillment data - resolveContexts = Array( i ), - resolveValues = slice.call( arguments ), - - // the primary Deferred - primary = jQuery.Deferred(), - - // subordinate callback factory - updateFunc = function( i ) { - return function( value ) { - resolveContexts[ i ] = this; - resolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; - if ( !( --remaining ) ) { - primary.resolveWith( resolveContexts, resolveValues ); - } - }; - }; - - // Single- and empty arguments are adopted like Promise.resolve - if ( remaining <= 1 ) { - adoptValue( singleValue, primary.done( updateFunc( i ) ).resolve, primary.reject, - !remaining ); - - // Use .then() to unwrap secondary thenables (cf. gh-3000) - if ( primary.state() === "pending" || - isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) { - - return primary.then(); - } - } - - // Multiple arguments are aggregated like Promise.all array elements - while ( i-- ) { - adoptValue( resolveValues[ i ], updateFunc( i ), primary.reject ); - } - - return primary.promise(); - } -} ); - - -// These usually indicate a programmer mistake during development, -// warn about them ASAP rather than swallowing them by default. -var rerrorNames = /^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/; - -jQuery.Deferred.exceptionHook = function( error, stack ) { - - // Support: IE 8 - 9 only - // Console exists when dev tools are open, which can happen at any time - if ( window.console && window.console.warn && error && rerrorNames.test( error.name ) ) { - window.console.warn( "jQuery.Deferred exception: " + error.message, error.stack, stack ); - } -}; - - - - -jQuery.readyException = function( error ) { - window.setTimeout( function() { - throw error; - } ); -}; - - - - -// The deferred used on DOM ready -var readyList = jQuery.Deferred(); - -jQuery.fn.ready = function( fn ) { - - readyList - .then( fn ) - - // Wrap jQuery.readyException in a function so that the lookup - // happens at the time of error handling instead of callback - // registration. - .catch( function( error ) { - jQuery.readyException( error ); - } ); - - return this; -}; - -jQuery.extend( { - - // Is the DOM ready to be used? Set to true once it occurs. - isReady: false, - - // A counter to track how many items to wait for before - // the ready event fires. See #6781 - readyWait: 1, - - // Handle when the DOM is ready - ready: function( wait ) { - - // Abort if there are pending holds or we're already ready - if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { - return; - } - - // Remember that the DOM is ready - jQuery.isReady = true; - - // If a normal DOM Ready event fired, decrement, and wait if need be - if ( wait !== true && --jQuery.readyWait > 0 ) { - return; - } - - // If there are functions bound, to execute - readyList.resolveWith( document, [ jQuery ] ); - } -} ); - -jQuery.ready.then = readyList.then; - -// The ready event handler and self cleanup method -function completed() { - document.removeEventListener( "DOMContentLoaded", completed ); - window.removeEventListener( "load", completed ); - jQuery.ready(); -} - -// Catch cases where $(document).ready() is called -// after the browser event has already occurred. -// Support: IE <=9 - 10 only -// Older IE sometimes signals "interactive" too soon -if ( document.readyState === "complete" || - ( document.readyState !== "loading" && !document.documentElement.doScroll ) ) { - - // Handle it asynchronously to allow scripts the opportunity to delay ready - window.setTimeout( jQuery.ready ); - -} else { - - // Use the handy event callback - document.addEventListener( "DOMContentLoaded", completed ); - - // A fallback to window.onload, that will always work - window.addEventListener( "load", completed ); -} - - - - -// Multifunctional method to get and set values of a collection -// The value/s can optionally be executed if it's a function -var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { - var i = 0, - len = elems.length, - bulk = key == null; - - // Sets many values - if ( toType( key ) === "object" ) { - chainable = true; - for ( i in key ) { - access( elems, fn, i, key[ i ], true, emptyGet, raw ); - } - - // Sets one value - } else if ( value !== undefined ) { - chainable = true; - - if ( !isFunction( value ) ) { - raw = true; - } - - if ( bulk ) { - - // Bulk operations run against the entire set - if ( raw ) { - fn.call( elems, value ); - fn = null; - - // ...except when executing function values - } else { - bulk = fn; - fn = function( elem, _key, value ) { - return bulk.call( jQuery( elem ), value ); - }; - } - } - - if ( fn ) { - for ( ; i < len; i++ ) { - fn( - elems[ i ], key, raw ? - value : - value.call( elems[ i ], i, fn( elems[ i ], key ) ) - ); - } - } - } - - if ( chainable ) { - return elems; - } - - // Gets - if ( bulk ) { - return fn.call( elems ); - } - - return len ? fn( elems[ 0 ], key ) : emptyGet; -}; - - -// Matches dashed string for camelizing -var rmsPrefix = /^-ms-/, - rdashAlpha = /-([a-z])/g; - -// Used by camelCase as callback to replace() -function fcamelCase( _all, letter ) { - return letter.toUpperCase(); -} - -// Convert dashed to camelCase; used by the css and data modules -// Support: IE <=9 - 11, Edge 12 - 15 -// Microsoft forgot to hump their vendor prefix (#9572) -function camelCase( string ) { - return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); -} -var acceptData = function( owner ) { - - // Accepts only: - // - Node - // - Node.ELEMENT_NODE - // - Node.DOCUMENT_NODE - // - Object - // - Any - return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType ); -}; - - - - -function Data() { - this.expando = jQuery.expando + Data.uid++; -} - -Data.uid = 1; - -Data.prototype = { - - cache: function( owner ) { - - // Check if the owner object already has a cache - var value = owner[ this.expando ]; - - // If not, create one - if ( !value ) { - value = {}; - - // We can accept data for non-element nodes in modern browsers, - // but we should not, see #8335. - // Always return an empty object. - if ( acceptData( owner ) ) { - - // If it is a node unlikely to be stringify-ed or looped over - // use plain assignment - if ( owner.nodeType ) { - owner[ this.expando ] = value; - - // Otherwise secure it in a non-enumerable property - // configurable must be true to allow the property to be - // deleted when data is removed - } else { - Object.defineProperty( owner, this.expando, { - value: value, - configurable: true - } ); - } - } - } - - return value; - }, - set: function( owner, data, value ) { - var prop, - cache = this.cache( owner ); - - // Handle: [ owner, key, value ] args - // Always use camelCase key (gh-2257) - if ( typeof data === "string" ) { - cache[ camelCase( data ) ] = value; - - // Handle: [ owner, { properties } ] args - } else { - - // Copy the properties one-by-one to the cache object - for ( prop in data ) { - cache[ camelCase( prop ) ] = data[ prop ]; - } - } - return cache; - }, - get: function( owner, key ) { - return key === undefined ? - this.cache( owner ) : - - // Always use camelCase key (gh-2257) - owner[ this.expando ] && owner[ this.expando ][ camelCase( key ) ]; - }, - access: function( owner, key, value ) { - - // In cases where either: - // - // 1. No key was specified - // 2. A string key was specified, but no value provided - // - // Take the "read" path and allow the get method to determine - // which value to return, respectively either: - // - // 1. The entire cache object - // 2. The data stored at the key - // - if ( key === undefined || - ( ( key && typeof key === "string" ) && value === undefined ) ) { - - return this.get( owner, key ); - } - - // When the key is not a string, or both a key and value - // are specified, set or extend (existing objects) with either: - // - // 1. An object of properties - // 2. A key and value - // - this.set( owner, key, value ); - - // Since the "set" path can have two possible entry points - // return the expected data based on which path was taken[*] - return value !== undefined ? value : key; - }, - remove: function( owner, key ) { - var i, - cache = owner[ this.expando ]; - - if ( cache === undefined ) { - return; - } - - if ( key !== undefined ) { - - // Support array or space separated string of keys - if ( Array.isArray( key ) ) { - - // If key is an array of keys... - // We always set camelCase keys, so remove that. - key = key.map( camelCase ); - } else { - key = camelCase( key ); - - // If a key with the spaces exists, use it. - // Otherwise, create an array by matching non-whitespace - key = key in cache ? - [ key ] : - ( key.match( rnothtmlwhite ) || [] ); - } - - i = key.length; - - while ( i-- ) { - delete cache[ key[ i ] ]; - } - } - - // Remove the expando if there's no more data - if ( key === undefined || jQuery.isEmptyObject( cache ) ) { - - // Support: Chrome <=35 - 45 - // Webkit & Blink performance suffers when deleting properties - // from DOM nodes, so set to undefined instead - // https://bugs.chromium.org/p/chromium/issues/detail?id=378607 (bug restricted) - if ( owner.nodeType ) { - owner[ this.expando ] = undefined; - } else { - delete owner[ this.expando ]; - } - } - }, - hasData: function( owner ) { - var cache = owner[ this.expando ]; - return cache !== undefined && !jQuery.isEmptyObject( cache ); - } -}; -var dataPriv = new Data(); - -var dataUser = new Data(); - - - -// Implementation Summary -// -// 1. Enforce API surface and semantic compatibility with 1.9.x branch -// 2. Improve the module's maintainability by reducing the storage -// paths to a single mechanism. -// 3. Use the same single mechanism to support "private" and "user" data. -// 4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData) -// 5. Avoid exposing implementation details on user objects (eg. expando properties) -// 6. Provide a clear path for implementation upgrade to WeakMap in 2014 - -var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, - rmultiDash = /[A-Z]/g; - -function getData( data ) { - if ( data === "true" ) { - return true; - } - - if ( data === "false" ) { - return false; - } - - if ( data === "null" ) { - return null; - } - - // Only convert to a number if it doesn't change the string - if ( data === +data + "" ) { - return +data; - } - - if ( rbrace.test( data ) ) { - return JSON.parse( data ); - } - - return data; -} - -function dataAttr( elem, key, data ) { - var name; - - // If nothing was found internally, try to fetch any - // data from the HTML5 data-* attribute - if ( data === undefined && elem.nodeType === 1 ) { - name = "data-" + key.replace( rmultiDash, "-$&" ).toLowerCase(); - data = elem.getAttribute( name ); - - if ( typeof data === "string" ) { - try { - data = getData( data ); - } catch ( e ) {} - - // Make sure we set the data so it isn't changed later - dataUser.set( elem, key, data ); - } else { - data = undefined; - } - } - return data; -} - -jQuery.extend( { - hasData: function( elem ) { - return dataUser.hasData( elem ) || dataPriv.hasData( elem ); - }, - - data: function( elem, name, data ) { - return dataUser.access( elem, name, data ); - }, - - removeData: function( elem, name ) { - dataUser.remove( elem, name ); - }, - - // TODO: Now that all calls to _data and _removeData have been replaced - // with direct calls to dataPriv methods, these can be deprecated. - _data: function( elem, name, data ) { - return dataPriv.access( elem, name, data ); - }, - - _removeData: function( elem, name ) { - dataPriv.remove( elem, name ); - } -} ); - -jQuery.fn.extend( { - data: function( key, value ) { - var i, name, data, - elem = this[ 0 ], - attrs = elem && elem.attributes; - - // Gets all values - if ( key === undefined ) { - if ( this.length ) { - data = dataUser.get( elem ); - - if ( elem.nodeType === 1 && !dataPriv.get( elem, "hasDataAttrs" ) ) { - i = attrs.length; - while ( i-- ) { - - // Support: IE 11 only - // The attrs elements can be null (#14894) - if ( attrs[ i ] ) { - name = attrs[ i ].name; - if ( name.indexOf( "data-" ) === 0 ) { - name = camelCase( name.slice( 5 ) ); - dataAttr( elem, name, data[ name ] ); - } - } - } - dataPriv.set( elem, "hasDataAttrs", true ); - } - } - - return data; - } - - // Sets multiple values - if ( typeof key === "object" ) { - return this.each( function() { - dataUser.set( this, key ); - } ); - } - - return access( this, function( value ) { - var data; - - // The calling jQuery object (element matches) is not empty - // (and therefore has an element appears at this[ 0 ]) and the - // `value` parameter was not undefined. An empty jQuery object - // will result in `undefined` for elem = this[ 0 ] which will - // throw an exception if an attempt to read a data cache is made. - if ( elem && value === undefined ) { - - // Attempt to get data from the cache - // The key will always be camelCased in Data - data = dataUser.get( elem, key ); - if ( data !== undefined ) { - return data; - } - - // Attempt to "discover" the data in - // HTML5 custom data-* attrs - data = dataAttr( elem, key ); - if ( data !== undefined ) { - return data; - } - - // We tried really hard, but the data doesn't exist. - return; - } - - // Set the data... - this.each( function() { - - // We always store the camelCased key - dataUser.set( this, key, value ); - } ); - }, null, value, arguments.length > 1, null, true ); - }, - - removeData: function( key ) { - return this.each( function() { - dataUser.remove( this, key ); - } ); - } -} ); - - -jQuery.extend( { - queue: function( elem, type, data ) { - var queue; - - if ( elem ) { - type = ( type || "fx" ) + "queue"; - queue = dataPriv.get( elem, type ); - - // Speed up dequeue by getting out quickly if this is just a lookup - if ( data ) { - if ( !queue || Array.isArray( data ) ) { - queue = dataPriv.access( elem, type, jQuery.makeArray( data ) ); - } else { - queue.push( data ); - } - } - return queue || []; - } - }, - - dequeue: function( elem, type ) { - type = type || "fx"; - - var queue = jQuery.queue( elem, type ), - startLength = queue.length, - fn = queue.shift(), - hooks = jQuery._queueHooks( elem, type ), - next = function() { - jQuery.dequeue( elem, type ); - }; - - // If the fx queue is dequeued, always remove the progress sentinel - if ( fn === "inprogress" ) { - fn = queue.shift(); - startLength--; - } - - if ( fn ) { - - // Add a progress sentinel to prevent the fx queue from being - // automatically dequeued - if ( type === "fx" ) { - queue.unshift( "inprogress" ); - } - - // Clear up the last queue stop function - delete hooks.stop; - fn.call( elem, next, hooks ); - } - - if ( !startLength && hooks ) { - hooks.empty.fire(); - } - }, - - // Not public - generate a queueHooks object, or return the current one - _queueHooks: function( elem, type ) { - var key = type + "queueHooks"; - return dataPriv.get( elem, key ) || dataPriv.access( elem, key, { - empty: jQuery.Callbacks( "once memory" ).add( function() { - dataPriv.remove( elem, [ type + "queue", key ] ); - } ) - } ); - } -} ); - -jQuery.fn.extend( { - queue: function( type, data ) { - var setter = 2; - - if ( typeof type !== "string" ) { - data = type; - type = "fx"; - setter--; - } - - if ( arguments.length < setter ) { - return jQuery.queue( this[ 0 ], type ); - } - - return data === undefined ? - this : - this.each( function() { - var queue = jQuery.queue( this, type, data ); - - // Ensure a hooks for this queue - jQuery._queueHooks( this, type ); - - if ( type === "fx" && queue[ 0 ] !== "inprogress" ) { - jQuery.dequeue( this, type ); - } - } ); - }, - dequeue: function( type ) { - return this.each( function() { - jQuery.dequeue( this, type ); - } ); - }, - clearQueue: function( type ) { - return this.queue( type || "fx", [] ); - }, - - // Get a promise resolved when queues of a certain type - // are emptied (fx is the type by default) - promise: function( type, obj ) { - var tmp, - count = 1, - defer = jQuery.Deferred(), - elements = this, - i = this.length, - resolve = function() { - if ( !( --count ) ) { - defer.resolveWith( elements, [ elements ] ); - } - }; - - if ( typeof type !== "string" ) { - obj = type; - type = undefined; - } - type = type || "fx"; - - while ( i-- ) { - tmp = dataPriv.get( elements[ i ], type + "queueHooks" ); - if ( tmp && tmp.empty ) { - count++; - tmp.empty.add( resolve ); - } - } - resolve(); - return defer.promise( obj ); - } -} ); -var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source; - -var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" ); - - -var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; - -var documentElement = document.documentElement; - - - - var isAttached = function( elem ) { - return jQuery.contains( elem.ownerDocument, elem ); - }, - composed = { composed: true }; - - // Support: IE 9 - 11+, Edge 12 - 18+, iOS 10.0 - 10.2 only - // Check attachment across shadow DOM boundaries when possible (gh-3504) - // Support: iOS 10.0-10.2 only - // Early iOS 10 versions support `attachShadow` but not `getRootNode`, - // leading to errors. We need to check for `getRootNode`. - if ( documentElement.getRootNode ) { - isAttached = function( elem ) { - return jQuery.contains( elem.ownerDocument, elem ) || - elem.getRootNode( composed ) === elem.ownerDocument; - }; - } -var isHiddenWithinTree = function( elem, el ) { - - // isHiddenWithinTree might be called from jQuery#filter function; - // in that case, element will be second argument - elem = el || elem; - - // Inline style trumps all - return elem.style.display === "none" || - elem.style.display === "" && - - // Otherwise, check computed style - // Support: Firefox <=43 - 45 - // Disconnected elements can have computed display: none, so first confirm that elem is - // in the document. - isAttached( elem ) && - - jQuery.css( elem, "display" ) === "none"; - }; - - - -function adjustCSS( elem, prop, valueParts, tween ) { - var adjusted, scale, - maxIterations = 20, - currentValue = tween ? - function() { - return tween.cur(); - } : - function() { - return jQuery.css( elem, prop, "" ); - }, - initial = currentValue(), - unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), - - // Starting value computation is required for potential unit mismatches - initialInUnit = elem.nodeType && - ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) && - rcssNum.exec( jQuery.css( elem, prop ) ); - - if ( initialInUnit && initialInUnit[ 3 ] !== unit ) { - - // Support: Firefox <=54 - // Halve the iteration target value to prevent interference from CSS upper bounds (gh-2144) - initial = initial / 2; - - // Trust units reported by jQuery.css - unit = unit || initialInUnit[ 3 ]; - - // Iteratively approximate from a nonzero starting point - initialInUnit = +initial || 1; - - while ( maxIterations-- ) { - - // Evaluate and update our best guess (doubling guesses that zero out). - // Finish if the scale equals or crosses 1 (making the old*new product non-positive). - jQuery.style( elem, prop, initialInUnit + unit ); - if ( ( 1 - scale ) * ( 1 - ( scale = currentValue() / initial || 0.5 ) ) <= 0 ) { - maxIterations = 0; - } - initialInUnit = initialInUnit / scale; - - } - - initialInUnit = initialInUnit * 2; - jQuery.style( elem, prop, initialInUnit + unit ); - - // Make sure we update the tween properties later on - valueParts = valueParts || []; - } - - if ( valueParts ) { - initialInUnit = +initialInUnit || +initial || 0; - - // Apply relative offset (+=/-=) if specified - adjusted = valueParts[ 1 ] ? - initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] : - +valueParts[ 2 ]; - if ( tween ) { - tween.unit = unit; - tween.start = initialInUnit; - tween.end = adjusted; - } - } - return adjusted; -} - - -var defaultDisplayMap = {}; - -function getDefaultDisplay( elem ) { - var temp, - doc = elem.ownerDocument, - nodeName = elem.nodeName, - display = defaultDisplayMap[ nodeName ]; - - if ( display ) { - return display; - } - - temp = doc.body.appendChild( doc.createElement( nodeName ) ); - display = jQuery.css( temp, "display" ); - - temp.parentNode.removeChild( temp ); - - if ( display === "none" ) { - display = "block"; - } - defaultDisplayMap[ nodeName ] = display; - - return display; -} - -function showHide( elements, show ) { - var display, elem, - values = [], - index = 0, - length = elements.length; - - // Determine new display value for elements that need to change - for ( ; index < length; index++ ) { - elem = elements[ index ]; - if ( !elem.style ) { - continue; - } - - display = elem.style.display; - if ( show ) { - - // Since we force visibility upon cascade-hidden elements, an immediate (and slow) - // check is required in this first loop unless we have a nonempty display value (either - // inline or about-to-be-restored) - if ( display === "none" ) { - values[ index ] = dataPriv.get( elem, "display" ) || null; - if ( !values[ index ] ) { - elem.style.display = ""; - } - } - if ( elem.style.display === "" && isHiddenWithinTree( elem ) ) { - values[ index ] = getDefaultDisplay( elem ); - } - } else { - if ( display !== "none" ) { - values[ index ] = "none"; - - // Remember what we're overwriting - dataPriv.set( elem, "display", display ); - } - } - } - - // Set the display of the elements in a second loop to avoid constant reflow - for ( index = 0; index < length; index++ ) { - if ( values[ index ] != null ) { - elements[ index ].style.display = values[ index ]; - } - } - - return elements; -} - -jQuery.fn.extend( { - show: function() { - return showHide( this, true ); - }, - hide: function() { - return showHide( this ); - }, - toggle: function( state ) { - if ( typeof state === "boolean" ) { - return state ? this.show() : this.hide(); - } - - return this.each( function() { - if ( isHiddenWithinTree( this ) ) { - jQuery( this ).show(); - } else { - jQuery( this ).hide(); - } - } ); - } -} ); -var rcheckableType = ( /^(?:checkbox|radio)$/i ); - -var rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]*)/i ); - -var rscriptType = ( /^$|^module$|\/(?:java|ecma)script/i ); - - - -( function() { - var fragment = document.createDocumentFragment(), - div = fragment.appendChild( document.createElement( "div" ) ), - input = document.createElement( "input" ); - - // Support: Android 4.0 - 4.3 only - // Check state lost if the name is set (#11217) - // Support: Windows Web Apps (WWA) - // `name` and `type` must use .setAttribute for WWA (#14901) - input.setAttribute( "type", "radio" ); - input.setAttribute( "checked", "checked" ); - input.setAttribute( "name", "t" ); - - div.appendChild( input ); - - // Support: Android <=4.1 only - // Older WebKit doesn't clone checked state correctly in fragments - support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; - - // Support: IE <=11 only - // Make sure textarea (and checkbox) defaultValue is properly cloned - div.innerHTML = ""; - support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; - - // Support: IE <=9 only - // IE <=9 replaces "; - support.option = !!div.lastChild; -} )(); - - -// We have to close these tags to support XHTML (#13200) -var wrapMap = { - - // XHTML parsers do not magically insert elements in the - // same way that tag soup parsers do. So we cannot shorten - // this by omitting or other required elements. - thead: [ 1, "", "
" ], - col: [ 2, "", "
" ], - tr: [ 2, "", "
" ], - td: [ 3, "", "
" ], - - _default: [ 0, "", "" ] -}; - -wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; -wrapMap.th = wrapMap.td; - -// Support: IE <=9 only -if ( !support.option ) { - wrapMap.optgroup = wrapMap.option = [ 1, "" ]; -} - - -function getAll( context, tag ) { - - // Support: IE <=9 - 11 only - // Use typeof to avoid zero-argument method invocation on host objects (#15151) - var ret; - - if ( typeof context.getElementsByTagName !== "undefined" ) { - ret = context.getElementsByTagName( tag || "*" ); - - } else if ( typeof context.querySelectorAll !== "undefined" ) { - ret = context.querySelectorAll( tag || "*" ); - - } else { - ret = []; - } - - if ( tag === undefined || tag && nodeName( context, tag ) ) { - return jQuery.merge( [ context ], ret ); - } - - return ret; -} - - -// Mark scripts as having already been evaluated -function setGlobalEval( elems, refElements ) { - var i = 0, - l = elems.length; - - for ( ; i < l; i++ ) { - dataPriv.set( - elems[ i ], - "globalEval", - !refElements || dataPriv.get( refElements[ i ], "globalEval" ) - ); - } -} - - -var rhtml = /<|&#?\w+;/; - -function buildFragment( elems, context, scripts, selection, ignored ) { - var elem, tmp, tag, wrap, attached, j, - fragment = context.createDocumentFragment(), - nodes = [], - i = 0, - l = elems.length; - - for ( ; i < l; i++ ) { - elem = elems[ i ]; - - if ( elem || elem === 0 ) { - - // Add nodes directly - if ( toType( elem ) === "object" ) { - - // Support: Android <=4.0 only, PhantomJS 1 only - // push.apply(_, arraylike) throws on ancient WebKit - jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); - - // Convert non-html into a text node - } else if ( !rhtml.test( elem ) ) { - nodes.push( context.createTextNode( elem ) ); - - // Convert html into DOM nodes - } else { - tmp = tmp || fragment.appendChild( context.createElement( "div" ) ); - - // Deserialize a standard representation - tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); - wrap = wrapMap[ tag ] || wrapMap._default; - tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ]; - - // Descend through wrappers to the right content - j = wrap[ 0 ]; - while ( j-- ) { - tmp = tmp.lastChild; - } - - // Support: Android <=4.0 only, PhantomJS 1 only - // push.apply(_, arraylike) throws on ancient WebKit - jQuery.merge( nodes, tmp.childNodes ); - - // Remember the top-level container - tmp = fragment.firstChild; - - // Ensure the created nodes are orphaned (#12392) - tmp.textContent = ""; - } - } - } - - // Remove wrapper from fragment - fragment.textContent = ""; - - i = 0; - while ( ( elem = nodes[ i++ ] ) ) { - - // Skip elements already in the context collection (trac-4087) - if ( selection && jQuery.inArray( elem, selection ) > -1 ) { - if ( ignored ) { - ignored.push( elem ); - } - continue; - } - - attached = isAttached( elem ); - - // Append to fragment - tmp = getAll( fragment.appendChild( elem ), "script" ); - - // Preserve script evaluation history - if ( attached ) { - setGlobalEval( tmp ); - } - - // Capture executables - if ( scripts ) { - j = 0; - while ( ( elem = tmp[ j++ ] ) ) { - if ( rscriptType.test( elem.type || "" ) ) { - scripts.push( elem ); - } - } - } - } - - return fragment; -} - - -var rtypenamespace = /^([^.]*)(?:\.(.+)|)/; - -function returnTrue() { - return true; -} - -function returnFalse() { - return false; -} - -// Support: IE <=9 - 11+ -// focus() and blur() are asynchronous, except when they are no-op. -// So expect focus to be synchronous when the element is already active, -// and blur to be synchronous when the element is not already active. -// (focus and blur are always synchronous in other supported browsers, -// this just defines when we can count on it). -function expectSync( elem, type ) { - return ( elem === safeActiveElement() ) === ( type === "focus" ); -} - -// Support: IE <=9 only -// Accessing document.activeElement can throw unexpectedly -// https://bugs.jquery.com/ticket/13393 -function safeActiveElement() { - try { - return document.activeElement; - } catch ( err ) { } -} - -function on( elem, types, selector, data, fn, one ) { - var origFn, type; - - // Types can be a map of types/handlers - if ( typeof types === "object" ) { - - // ( types-Object, selector, data ) - if ( typeof selector !== "string" ) { - - // ( types-Object, data ) - data = data || selector; - selector = undefined; - } - for ( type in types ) { - on( elem, type, selector, data, types[ type ], one ); - } - return elem; - } - - if ( data == null && fn == null ) { - - // ( types, fn ) - fn = selector; - data = selector = undefined; - } else if ( fn == null ) { - if ( typeof selector === "string" ) { - - // ( types, selector, fn ) - fn = data; - data = undefined; - } else { - - // ( types, data, fn ) - fn = data; - data = selector; - selector = undefined; - } - } - if ( fn === false ) { - fn = returnFalse; - } else if ( !fn ) { - return elem; - } - - if ( one === 1 ) { - origFn = fn; - fn = function( event ) { - - // Can use an empty set, since event contains the info - jQuery().off( event ); - return origFn.apply( this, arguments ); - }; - - // Use same guid so caller can remove using origFn - fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); - } - return elem.each( function() { - jQuery.event.add( this, types, fn, data, selector ); - } ); -} - -/* - * Helper functions for managing events -- not part of the public interface. - * Props to Dean Edwards' addEvent library for many of the ideas. - */ -jQuery.event = { - - global: {}, - - add: function( elem, types, handler, data, selector ) { - - var handleObjIn, eventHandle, tmp, - events, t, handleObj, - special, handlers, type, namespaces, origType, - elemData = dataPriv.get( elem ); - - // Only attach events to objects that accept data - if ( !acceptData( elem ) ) { - return; - } - - // Caller can pass in an object of custom data in lieu of the handler - if ( handler.handler ) { - handleObjIn = handler; - handler = handleObjIn.handler; - selector = handleObjIn.selector; - } - - // Ensure that invalid selectors throw exceptions at attach time - // Evaluate against documentElement in case elem is a non-element node (e.g., document) - if ( selector ) { - jQuery.find.matchesSelector( documentElement, selector ); - } - - // Make sure that the handler has a unique ID, used to find/remove it later - if ( !handler.guid ) { - handler.guid = jQuery.guid++; - } - - // Init the element's event structure and main handler, if this is the first - if ( !( events = elemData.events ) ) { - events = elemData.events = Object.create( null ); - } - if ( !( eventHandle = elemData.handle ) ) { - eventHandle = elemData.handle = function( e ) { - - // Discard the second event of a jQuery.event.trigger() and - // when an event is called after a page has unloaded - return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ? - jQuery.event.dispatch.apply( elem, arguments ) : undefined; - }; - } - - // Handle multiple events separated by a space - types = ( types || "" ).match( rnothtmlwhite ) || [ "" ]; - t = types.length; - while ( t-- ) { - tmp = rtypenamespace.exec( types[ t ] ) || []; - type = origType = tmp[ 1 ]; - namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); - - // There *must* be a type, no attaching namespace-only handlers - if ( !type ) { - continue; - } - - // If event changes its type, use the special event handlers for the changed type - special = jQuery.event.special[ type ] || {}; - - // If selector defined, determine special event api type, otherwise given type - type = ( selector ? special.delegateType : special.bindType ) || type; - - // Update special based on newly reset type - special = jQuery.event.special[ type ] || {}; - - // handleObj is passed to all event handlers - handleObj = jQuery.extend( { - type: type, - origType: origType, - data: data, - handler: handler, - guid: handler.guid, - selector: selector, - needsContext: selector && jQuery.expr.match.needsContext.test( selector ), - namespace: namespaces.join( "." ) - }, handleObjIn ); - - // Init the event handler queue if we're the first - if ( !( handlers = events[ type ] ) ) { - handlers = events[ type ] = []; - handlers.delegateCount = 0; - - // Only use addEventListener if the special events handler returns false - if ( !special.setup || - special.setup.call( elem, data, namespaces, eventHandle ) === false ) { - - if ( elem.addEventListener ) { - elem.addEventListener( type, eventHandle ); - } - } - } - - if ( special.add ) { - special.add.call( elem, handleObj ); - - if ( !handleObj.handler.guid ) { - handleObj.handler.guid = handler.guid; - } - } - - // Add to the element's handler list, delegates in front - if ( selector ) { - handlers.splice( handlers.delegateCount++, 0, handleObj ); - } else { - handlers.push( handleObj ); - } - - // Keep track of which events have ever been used, for event optimization - jQuery.event.global[ type ] = true; - } - - }, - - // Detach an event or set of events from an element - remove: function( elem, types, handler, selector, mappedTypes ) { - - var j, origCount, tmp, - events, t, handleObj, - special, handlers, type, namespaces, origType, - elemData = dataPriv.hasData( elem ) && dataPriv.get( elem ); - - if ( !elemData || !( events = elemData.events ) ) { - return; - } - - // Once for each type.namespace in types; type may be omitted - types = ( types || "" ).match( rnothtmlwhite ) || [ "" ]; - t = types.length; - while ( t-- ) { - tmp = rtypenamespace.exec( types[ t ] ) || []; - type = origType = tmp[ 1 ]; - namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); - - // Unbind all events (on this namespace, if provided) for the element - if ( !type ) { - for ( type in events ) { - jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); - } - continue; - } - - special = jQuery.event.special[ type ] || {}; - type = ( selector ? special.delegateType : special.bindType ) || type; - handlers = events[ type ] || []; - tmp = tmp[ 2 ] && - new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ); - - // Remove matching events - origCount = j = handlers.length; - while ( j-- ) { - handleObj = handlers[ j ]; - - if ( ( mappedTypes || origType === handleObj.origType ) && - ( !handler || handler.guid === handleObj.guid ) && - ( !tmp || tmp.test( handleObj.namespace ) ) && - ( !selector || selector === handleObj.selector || - selector === "**" && handleObj.selector ) ) { - handlers.splice( j, 1 ); - - if ( handleObj.selector ) { - handlers.delegateCount--; - } - if ( special.remove ) { - special.remove.call( elem, handleObj ); - } - } - } - - // Remove generic event handler if we removed something and no more handlers exist - // (avoids potential for endless recursion during removal of special event handlers) - if ( origCount && !handlers.length ) { - if ( !special.teardown || - special.teardown.call( elem, namespaces, elemData.handle ) === false ) { - - jQuery.removeEvent( elem, type, elemData.handle ); - } - - delete events[ type ]; - } - } - - // Remove data and the expando if it's no longer used - if ( jQuery.isEmptyObject( events ) ) { - dataPriv.remove( elem, "handle events" ); - } - }, - - dispatch: function( nativeEvent ) { - - var i, j, ret, matched, handleObj, handlerQueue, - args = new Array( arguments.length ), - - // Make a writable jQuery.Event from the native event object - event = jQuery.event.fix( nativeEvent ), - - handlers = ( - dataPriv.get( this, "events" ) || Object.create( null ) - )[ event.type ] || [], - special = jQuery.event.special[ event.type ] || {}; - - // Use the fix-ed jQuery.Event rather than the (read-only) native event - args[ 0 ] = event; - - for ( i = 1; i < arguments.length; i++ ) { - args[ i ] = arguments[ i ]; - } - - event.delegateTarget = this; - - // Call the preDispatch hook for the mapped type, and let it bail if desired - if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { - return; - } - - // Determine handlers - handlerQueue = jQuery.event.handlers.call( this, event, handlers ); - - // Run delegates first; they may want to stop propagation beneath us - i = 0; - while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) { - event.currentTarget = matched.elem; - - j = 0; - while ( ( handleObj = matched.handlers[ j++ ] ) && - !event.isImmediatePropagationStopped() ) { - - // If the event is namespaced, then each handler is only invoked if it is - // specially universal or its namespaces are a superset of the event's. - if ( !event.rnamespace || handleObj.namespace === false || - event.rnamespace.test( handleObj.namespace ) ) { - - event.handleObj = handleObj; - event.data = handleObj.data; - - ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle || - handleObj.handler ).apply( matched.elem, args ); - - if ( ret !== undefined ) { - if ( ( event.result = ret ) === false ) { - event.preventDefault(); - event.stopPropagation(); - } - } - } - } - } - - // Call the postDispatch hook for the mapped type - if ( special.postDispatch ) { - special.postDispatch.call( this, event ); - } - - return event.result; - }, - - handlers: function( event, handlers ) { - var i, handleObj, sel, matchedHandlers, matchedSelectors, - handlerQueue = [], - delegateCount = handlers.delegateCount, - cur = event.target; - - // Find delegate handlers - if ( delegateCount && - - // Support: IE <=9 - // Black-hole SVG instance trees (trac-13180) - cur.nodeType && - - // Support: Firefox <=42 - // Suppress spec-violating clicks indicating a non-primary pointer button (trac-3861) - // https://www.w3.org/TR/DOM-Level-3-Events/#event-type-click - // Support: IE 11 only - // ...but not arrow key "clicks" of radio inputs, which can have `button` -1 (gh-2343) - !( event.type === "click" && event.button >= 1 ) ) { - - for ( ; cur !== this; cur = cur.parentNode || this ) { - - // Don't check non-elements (#13208) - // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) - if ( cur.nodeType === 1 && !( event.type === "click" && cur.disabled === true ) ) { - matchedHandlers = []; - matchedSelectors = {}; - for ( i = 0; i < delegateCount; i++ ) { - handleObj = handlers[ i ]; - - // Don't conflict with Object.prototype properties (#13203) - sel = handleObj.selector + " "; - - if ( matchedSelectors[ sel ] === undefined ) { - matchedSelectors[ sel ] = handleObj.needsContext ? - jQuery( sel, this ).index( cur ) > -1 : - jQuery.find( sel, this, null, [ cur ] ).length; - } - if ( matchedSelectors[ sel ] ) { - matchedHandlers.push( handleObj ); - } - } - if ( matchedHandlers.length ) { - handlerQueue.push( { elem: cur, handlers: matchedHandlers } ); - } - } - } - } - - // Add the remaining (directly-bound) handlers - cur = this; - if ( delegateCount < handlers.length ) { - handlerQueue.push( { elem: cur, handlers: handlers.slice( delegateCount ) } ); - } - - return handlerQueue; - }, - - addProp: function( name, hook ) { - Object.defineProperty( jQuery.Event.prototype, name, { - enumerable: true, - configurable: true, - - get: isFunction( hook ) ? - function() { - if ( this.originalEvent ) { - return hook( this.originalEvent ); - } - } : - function() { - if ( this.originalEvent ) { - return this.originalEvent[ name ]; - } - }, - - set: function( value ) { - Object.defineProperty( this, name, { - enumerable: true, - configurable: true, - writable: true, - value: value - } ); - } - } ); - }, - - fix: function( originalEvent ) { - return originalEvent[ jQuery.expando ] ? - originalEvent : - new jQuery.Event( originalEvent ); - }, - - special: { - load: { - - // Prevent triggered image.load events from bubbling to window.load - noBubble: true - }, - click: { - - // Utilize native event to ensure correct state for checkable inputs - setup: function( data ) { - - // For mutual compressibility with _default, replace `this` access with a local var. - // `|| data` is dead code meant only to preserve the variable through minification. - var el = this || data; - - // Claim the first handler - if ( rcheckableType.test( el.type ) && - el.click && nodeName( el, "input" ) ) { - - // dataPriv.set( el, "click", ... ) - leverageNative( el, "click", returnTrue ); - } - - // Return false to allow normal processing in the caller - return false; - }, - trigger: function( data ) { - - // For mutual compressibility with _default, replace `this` access with a local var. - // `|| data` is dead code meant only to preserve the variable through minification. - var el = this || data; - - // Force setup before triggering a click - if ( rcheckableType.test( el.type ) && - el.click && nodeName( el, "input" ) ) { - - leverageNative( el, "click" ); - } - - // Return non-false to allow normal event-path propagation - return true; - }, - - // For cross-browser consistency, suppress native .click() on links - // Also prevent it if we're currently inside a leveraged native-event stack - _default: function( event ) { - var target = event.target; - return rcheckableType.test( target.type ) && - target.click && nodeName( target, "input" ) && - dataPriv.get( target, "click" ) || - nodeName( target, "a" ); - } - }, - - beforeunload: { - postDispatch: function( event ) { - - // Support: Firefox 20+ - // Firefox doesn't alert if the returnValue field is not set. - if ( event.result !== undefined && event.originalEvent ) { - event.originalEvent.returnValue = event.result; - } - } - } - } -}; - -// Ensure the presence of an event listener that handles manually-triggered -// synthetic events by interrupting progress until reinvoked in response to -// *native* events that it fires directly, ensuring that state changes have -// already occurred before other listeners are invoked. -function leverageNative( el, type, expectSync ) { - - // Missing expectSync indicates a trigger call, which must force setup through jQuery.event.add - if ( !expectSync ) { - if ( dataPriv.get( el, type ) === undefined ) { - jQuery.event.add( el, type, returnTrue ); - } - return; - } - - // Register the controller as a special universal handler for all event namespaces - dataPriv.set( el, type, false ); - jQuery.event.add( el, type, { - namespace: false, - handler: function( event ) { - var notAsync, result, - saved = dataPriv.get( this, type ); - - if ( ( event.isTrigger & 1 ) && this[ type ] ) { - - // Interrupt processing of the outer synthetic .trigger()ed event - // Saved data should be false in such cases, but might be a leftover capture object - // from an async native handler (gh-4350) - if ( !saved.length ) { - - // Store arguments for use when handling the inner native event - // There will always be at least one argument (an event object), so this array - // will not be confused with a leftover capture object. - saved = slice.call( arguments ); - dataPriv.set( this, type, saved ); - - // Trigger the native event and capture its result - // Support: IE <=9 - 11+ - // focus() and blur() are asynchronous - notAsync = expectSync( this, type ); - this[ type ](); - result = dataPriv.get( this, type ); - if ( saved !== result || notAsync ) { - dataPriv.set( this, type, false ); - } else { - result = {}; - } - if ( saved !== result ) { - - // Cancel the outer synthetic event - event.stopImmediatePropagation(); - event.preventDefault(); - - // Support: Chrome 86+ - // In Chrome, if an element having a focusout handler is blurred by - // clicking outside of it, it invokes the handler synchronously. If - // that handler calls `.remove()` on the element, the data is cleared, - // leaving `result` undefined. We need to guard against this. - return result && result.value; - } - - // If this is an inner synthetic event for an event with a bubbling surrogate - // (focus or blur), assume that the surrogate already propagated from triggering the - // native event and prevent that from happening again here. - // This technically gets the ordering wrong w.r.t. to `.trigger()` (in which the - // bubbling surrogate propagates *after* the non-bubbling base), but that seems - // less bad than duplication. - } else if ( ( jQuery.event.special[ type ] || {} ).delegateType ) { - event.stopPropagation(); - } - - // If this is a native event triggered above, everything is now in order - // Fire an inner synthetic event with the original arguments - } else if ( saved.length ) { - - // ...and capture the result - dataPriv.set( this, type, { - value: jQuery.event.trigger( - - // Support: IE <=9 - 11+ - // Extend with the prototype to reset the above stopImmediatePropagation() - jQuery.extend( saved[ 0 ], jQuery.Event.prototype ), - saved.slice( 1 ), - this - ) - } ); - - // Abort handling of the native event - event.stopImmediatePropagation(); - } - } - } ); -} - -jQuery.removeEvent = function( elem, type, handle ) { - - // This "if" is needed for plain objects - if ( elem.removeEventListener ) { - elem.removeEventListener( type, handle ); - } -}; - -jQuery.Event = function( src, props ) { - - // Allow instantiation without the 'new' keyword - if ( !( this instanceof jQuery.Event ) ) { - return new jQuery.Event( src, props ); - } - - // Event object - if ( src && src.type ) { - this.originalEvent = src; - this.type = src.type; - - // Events bubbling up the document may have been marked as prevented - // by a handler lower down the tree; reflect the correct value. - this.isDefaultPrevented = src.defaultPrevented || - src.defaultPrevented === undefined && - - // Support: Android <=2.3 only - src.returnValue === false ? - returnTrue : - returnFalse; - - // Create target properties - // Support: Safari <=6 - 7 only - // Target should not be a text node (#504, #13143) - this.target = ( src.target && src.target.nodeType === 3 ) ? - src.target.parentNode : - src.target; - - this.currentTarget = src.currentTarget; - this.relatedTarget = src.relatedTarget; - - // Event type - } else { - this.type = src; - } - - // Put explicitly provided properties onto the event object - if ( props ) { - jQuery.extend( this, props ); - } - - // Create a timestamp if incoming event doesn't have one - this.timeStamp = src && src.timeStamp || Date.now(); - - // Mark it as fixed - this[ jQuery.expando ] = true; -}; - -// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding -// https://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html -jQuery.Event.prototype = { - constructor: jQuery.Event, - isDefaultPrevented: returnFalse, - isPropagationStopped: returnFalse, - isImmediatePropagationStopped: returnFalse, - isSimulated: false, - - preventDefault: function() { - var e = this.originalEvent; - - this.isDefaultPrevented = returnTrue; - - if ( e && !this.isSimulated ) { - e.preventDefault(); - } - }, - stopPropagation: function() { - var e = this.originalEvent; - - this.isPropagationStopped = returnTrue; - - if ( e && !this.isSimulated ) { - e.stopPropagation(); - } - }, - stopImmediatePropagation: function() { - var e = this.originalEvent; - - this.isImmediatePropagationStopped = returnTrue; - - if ( e && !this.isSimulated ) { - e.stopImmediatePropagation(); - } - - this.stopPropagation(); - } -}; - -// Includes all common event props including KeyEvent and MouseEvent specific props -jQuery.each( { - altKey: true, - bubbles: true, - cancelable: true, - changedTouches: true, - ctrlKey: true, - detail: true, - eventPhase: true, - metaKey: true, - pageX: true, - pageY: true, - shiftKey: true, - view: true, - "char": true, - code: true, - charCode: true, - key: true, - keyCode: true, - button: true, - buttons: true, - clientX: true, - clientY: true, - offsetX: true, - offsetY: true, - pointerId: true, - pointerType: true, - screenX: true, - screenY: true, - targetTouches: true, - toElement: true, - touches: true, - which: true -}, jQuery.event.addProp ); - -jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateType ) { - jQuery.event.special[ type ] = { - - // Utilize native event if possible so blur/focus sequence is correct - setup: function() { - - // Claim the first handler - // dataPriv.set( this, "focus", ... ) - // dataPriv.set( this, "blur", ... ) - leverageNative( this, type, expectSync ); - - // Return false to allow normal processing in the caller - return false; - }, - trigger: function() { - - // Force setup before trigger - leverageNative( this, type ); - - // Return non-false to allow normal event-path propagation - return true; - }, - - // Suppress native focus or blur as it's already being fired - // in leverageNative. - _default: function() { - return true; - }, - - delegateType: delegateType - }; -} ); - -// Create mouseenter/leave events using mouseover/out and event-time checks -// so that event delegation works in jQuery. -// Do the same for pointerenter/pointerleave and pointerover/pointerout -// -// Support: Safari 7 only -// Safari sends mouseenter too often; see: -// https://bugs.chromium.org/p/chromium/issues/detail?id=470258 -// for the description of the bug (it existed in older Chrome versions as well). -jQuery.each( { - mouseenter: "mouseover", - mouseleave: "mouseout", - pointerenter: "pointerover", - pointerleave: "pointerout" -}, function( orig, fix ) { - jQuery.event.special[ orig ] = { - delegateType: fix, - bindType: fix, - - handle: function( event ) { - var ret, - target = this, - related = event.relatedTarget, - handleObj = event.handleObj; - - // For mouseenter/leave call the handler if related is outside the target. - // NB: No relatedTarget if the mouse left/entered the browser window - if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) { - event.type = handleObj.origType; - ret = handleObj.handler.apply( this, arguments ); - event.type = fix; - } - return ret; - } - }; -} ); - -jQuery.fn.extend( { - - on: function( types, selector, data, fn ) { - return on( this, types, selector, data, fn ); - }, - one: function( types, selector, data, fn ) { - return on( this, types, selector, data, fn, 1 ); - }, - off: function( types, selector, fn ) { - var handleObj, type; - if ( types && types.preventDefault && types.handleObj ) { - - // ( event ) dispatched jQuery.Event - handleObj = types.handleObj; - jQuery( types.delegateTarget ).off( - handleObj.namespace ? - handleObj.origType + "." + handleObj.namespace : - handleObj.origType, - handleObj.selector, - handleObj.handler - ); - return this; - } - if ( typeof types === "object" ) { - - // ( types-object [, selector] ) - for ( type in types ) { - this.off( type, selector, types[ type ] ); - } - return this; - } - if ( selector === false || typeof selector === "function" ) { - - // ( types [, fn] ) - fn = selector; - selector = undefined; - } - if ( fn === false ) { - fn = returnFalse; - } - return this.each( function() { - jQuery.event.remove( this, types, fn, selector ); - } ); - } -} ); - - -var - - // Support: IE <=10 - 11, Edge 12 - 13 only - // In IE/Edge using regex groups here causes severe slowdowns. - // See https://connect.microsoft.com/IE/feedback/details/1736512/ - rnoInnerhtml = /\s*$/g; - -// Prefer a tbody over its parent table for containing new rows -function manipulationTarget( elem, content ) { - if ( nodeName( elem, "table" ) && - nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ) { - - return jQuery( elem ).children( "tbody" )[ 0 ] || elem; - } - - return elem; -} - -// Replace/restore the type attribute of script elements for safe DOM manipulation -function disableScript( elem ) { - elem.type = ( elem.getAttribute( "type" ) !== null ) + "/" + elem.type; - return elem; -} -function restoreScript( elem ) { - if ( ( elem.type || "" ).slice( 0, 5 ) === "true/" ) { - elem.type = elem.type.slice( 5 ); - } else { - elem.removeAttribute( "type" ); - } - - return elem; -} - -function cloneCopyEvent( src, dest ) { - var i, l, type, pdataOld, udataOld, udataCur, events; - - if ( dest.nodeType !== 1 ) { - return; - } - - // 1. Copy private data: events, handlers, etc. - if ( dataPriv.hasData( src ) ) { - pdataOld = dataPriv.get( src ); - events = pdataOld.events; - - if ( events ) { - dataPriv.remove( dest, "handle events" ); - - for ( type in events ) { - for ( i = 0, l = events[ type ].length; i < l; i++ ) { - jQuery.event.add( dest, type, events[ type ][ i ] ); - } - } - } - } - - // 2. Copy user data - if ( dataUser.hasData( src ) ) { - udataOld = dataUser.access( src ); - udataCur = jQuery.extend( {}, udataOld ); - - dataUser.set( dest, udataCur ); - } -} - -// Fix IE bugs, see support tests -function fixInput( src, dest ) { - var nodeName = dest.nodeName.toLowerCase(); - - // Fails to persist the checked state of a cloned checkbox or radio button. - if ( nodeName === "input" && rcheckableType.test( src.type ) ) { - dest.checked = src.checked; - - // Fails to return the selected option to the default selected state when cloning options - } else if ( nodeName === "input" || nodeName === "textarea" ) { - dest.defaultValue = src.defaultValue; - } -} - -function domManip( collection, args, callback, ignored ) { - - // Flatten any nested arrays - args = flat( args ); - - var fragment, first, scripts, hasScripts, node, doc, - i = 0, - l = collection.length, - iNoClone = l - 1, - value = args[ 0 ], - valueIsFunction = isFunction( value ); - - // We can't cloneNode fragments that contain checked, in WebKit - if ( valueIsFunction || - ( l > 1 && typeof value === "string" && - !support.checkClone && rchecked.test( value ) ) ) { - return collection.each( function( index ) { - var self = collection.eq( index ); - if ( valueIsFunction ) { - args[ 0 ] = value.call( this, index, self.html() ); - } - domManip( self, args, callback, ignored ); - } ); - } - - if ( l ) { - fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored ); - first = fragment.firstChild; - - if ( fragment.childNodes.length === 1 ) { - fragment = first; - } - - // Require either new content or an interest in ignored elements to invoke the callback - if ( first || ignored ) { - scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); - hasScripts = scripts.length; - - // Use the original fragment for the last item - // instead of the first because it can end up - // being emptied incorrectly in certain situations (#8070). - for ( ; i < l; i++ ) { - node = fragment; - - if ( i !== iNoClone ) { - node = jQuery.clone( node, true, true ); - - // Keep references to cloned scripts for later restoration - if ( hasScripts ) { - - // Support: Android <=4.0 only, PhantomJS 1 only - // push.apply(_, arraylike) throws on ancient WebKit - jQuery.merge( scripts, getAll( node, "script" ) ); - } - } - - callback.call( collection[ i ], node, i ); - } - - if ( hasScripts ) { - doc = scripts[ scripts.length - 1 ].ownerDocument; - - // Reenable scripts - jQuery.map( scripts, restoreScript ); - - // Evaluate executable scripts on first document insertion - for ( i = 0; i < hasScripts; i++ ) { - node = scripts[ i ]; - if ( rscriptType.test( node.type || "" ) && - !dataPriv.access( node, "globalEval" ) && - jQuery.contains( doc, node ) ) { - - if ( node.src && ( node.type || "" ).toLowerCase() !== "module" ) { - - // Optional AJAX dependency, but won't run scripts if not present - if ( jQuery._evalUrl && !node.noModule ) { - jQuery._evalUrl( node.src, { - nonce: node.nonce || node.getAttribute( "nonce" ) - }, doc ); - } - } else { - DOMEval( node.textContent.replace( rcleanScript, "" ), node, doc ); - } - } - } - } - } - } - - return collection; -} - -function remove( elem, selector, keepData ) { - var node, - nodes = selector ? jQuery.filter( selector, elem ) : elem, - i = 0; - - for ( ; ( node = nodes[ i ] ) != null; i++ ) { - if ( !keepData && node.nodeType === 1 ) { - jQuery.cleanData( getAll( node ) ); - } - - if ( node.parentNode ) { - if ( keepData && isAttached( node ) ) { - setGlobalEval( getAll( node, "script" ) ); - } - node.parentNode.removeChild( node ); - } - } - - return elem; -} - -jQuery.extend( { - htmlPrefilter: function( html ) { - return html; - }, - - clone: function( elem, dataAndEvents, deepDataAndEvents ) { - var i, l, srcElements, destElements, - clone = elem.cloneNode( true ), - inPage = isAttached( elem ); - - // Fix IE cloning issues - if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) && - !jQuery.isXMLDoc( elem ) ) { - - // We eschew Sizzle here for performance reasons: https://jsperf.com/getall-vs-sizzle/2 - destElements = getAll( clone ); - srcElements = getAll( elem ); - - for ( i = 0, l = srcElements.length; i < l; i++ ) { - fixInput( srcElements[ i ], destElements[ i ] ); - } - } - - // Copy the events from the original to the clone - if ( dataAndEvents ) { - if ( deepDataAndEvents ) { - srcElements = srcElements || getAll( elem ); - destElements = destElements || getAll( clone ); - - for ( i = 0, l = srcElements.length; i < l; i++ ) { - cloneCopyEvent( srcElements[ i ], destElements[ i ] ); - } - } else { - cloneCopyEvent( elem, clone ); - } - } - - // Preserve script evaluation history - destElements = getAll( clone, "script" ); - if ( destElements.length > 0 ) { - setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); - } - - // Return the cloned set - return clone; - }, - - cleanData: function( elems ) { - var data, elem, type, - special = jQuery.event.special, - i = 0; - - for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) { - if ( acceptData( elem ) ) { - if ( ( data = elem[ dataPriv.expando ] ) ) { - if ( data.events ) { - for ( type in data.events ) { - if ( special[ type ] ) { - jQuery.event.remove( elem, type ); - - // This is a shortcut to avoid jQuery.event.remove's overhead - } else { - jQuery.removeEvent( elem, type, data.handle ); - } - } - } - - // Support: Chrome <=35 - 45+ - // Assign undefined instead of using delete, see Data#remove - elem[ dataPriv.expando ] = undefined; - } - if ( elem[ dataUser.expando ] ) { - - // Support: Chrome <=35 - 45+ - // Assign undefined instead of using delete, see Data#remove - elem[ dataUser.expando ] = undefined; - } - } - } - } -} ); - -jQuery.fn.extend( { - detach: function( selector ) { - return remove( this, selector, true ); - }, - - remove: function( selector ) { - return remove( this, selector ); - }, - - text: function( value ) { - return access( this, function( value ) { - return value === undefined ? - jQuery.text( this ) : - this.empty().each( function() { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - this.textContent = value; - } - } ); - }, null, value, arguments.length ); - }, - - append: function() { - return domManip( this, arguments, function( elem ) { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - var target = manipulationTarget( this, elem ); - target.appendChild( elem ); - } - } ); - }, - - prepend: function() { - return domManip( this, arguments, function( elem ) { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - var target = manipulationTarget( this, elem ); - target.insertBefore( elem, target.firstChild ); - } - } ); - }, - - before: function() { - return domManip( this, arguments, function( elem ) { - if ( this.parentNode ) { - this.parentNode.insertBefore( elem, this ); - } - } ); - }, - - after: function() { - return domManip( this, arguments, function( elem ) { - if ( this.parentNode ) { - this.parentNode.insertBefore( elem, this.nextSibling ); - } - } ); - }, - - empty: function() { - var elem, - i = 0; - - for ( ; ( elem = this[ i ] ) != null; i++ ) { - if ( elem.nodeType === 1 ) { - - // Prevent memory leaks - jQuery.cleanData( getAll( elem, false ) ); - - // Remove any remaining nodes - elem.textContent = ""; - } - } - - return this; - }, - - clone: function( dataAndEvents, deepDataAndEvents ) { - dataAndEvents = dataAndEvents == null ? false : dataAndEvents; - deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; - - return this.map( function() { - return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); - } ); - }, - - html: function( value ) { - return access( this, function( value ) { - var elem = this[ 0 ] || {}, - i = 0, - l = this.length; - - if ( value === undefined && elem.nodeType === 1 ) { - return elem.innerHTML; - } - - // See if we can take a shortcut and just use innerHTML - if ( typeof value === "string" && !rnoInnerhtml.test( value ) && - !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) { - - value = jQuery.htmlPrefilter( value ); - - try { - for ( ; i < l; i++ ) { - elem = this[ i ] || {}; - - // Remove element nodes and prevent memory leaks - if ( elem.nodeType === 1 ) { - jQuery.cleanData( getAll( elem, false ) ); - elem.innerHTML = value; - } - } - - elem = 0; - - // If using innerHTML throws an exception, use the fallback method - } catch ( e ) {} - } - - if ( elem ) { - this.empty().append( value ); - } - }, null, value, arguments.length ); - }, - - replaceWith: function() { - var ignored = []; - - // Make the changes, replacing each non-ignored context element with the new content - return domManip( this, arguments, function( elem ) { - var parent = this.parentNode; - - if ( jQuery.inArray( this, ignored ) < 0 ) { - jQuery.cleanData( getAll( this ) ); - if ( parent ) { - parent.replaceChild( elem, this ); - } - } - - // Force callback invocation - }, ignored ); - } -} ); - -jQuery.each( { - appendTo: "append", - prependTo: "prepend", - insertBefore: "before", - insertAfter: "after", - replaceAll: "replaceWith" -}, function( name, original ) { - jQuery.fn[ name ] = function( selector ) { - var elems, - ret = [], - insert = jQuery( selector ), - last = insert.length - 1, - i = 0; - - for ( ; i <= last; i++ ) { - elems = i === last ? this : this.clone( true ); - jQuery( insert[ i ] )[ original ]( elems ); - - // Support: Android <=4.0 only, PhantomJS 1 only - // .get() because push.apply(_, arraylike) throws on ancient WebKit - push.apply( ret, elems.get() ); - } - - return this.pushStack( ret ); - }; -} ); -var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" ); - -var getStyles = function( elem ) { - - // Support: IE <=11 only, Firefox <=30 (#15098, #14150) - // IE throws on elements created in popups - // FF meanwhile throws on frame elements through "defaultView.getComputedStyle" - var view = elem.ownerDocument.defaultView; - - if ( !view || !view.opener ) { - view = window; - } - - return view.getComputedStyle( elem ); - }; - -var swap = function( elem, options, callback ) { - var ret, name, - old = {}; - - // Remember the old values, and insert the new ones - for ( name in options ) { - old[ name ] = elem.style[ name ]; - elem.style[ name ] = options[ name ]; - } - - ret = callback.call( elem ); - - // Revert the old values - for ( name in options ) { - elem.style[ name ] = old[ name ]; - } - - return ret; -}; - - -var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" ); - - - -( function() { - - // Executing both pixelPosition & boxSizingReliable tests require only one layout - // so they're executed at the same time to save the second computation. - function computeStyleTests() { - - // This is a singleton, we need to execute it only once - if ( !div ) { - return; - } - - container.style.cssText = "position:absolute;left:-11111px;width:60px;" + - "margin-top:1px;padding:0;border:0"; - div.style.cssText = - "position:relative;display:block;box-sizing:border-box;overflow:scroll;" + - "margin:auto;border:1px;padding:1px;" + - "width:60%;top:1%"; - documentElement.appendChild( container ).appendChild( div ); - - var divStyle = window.getComputedStyle( div ); - pixelPositionVal = divStyle.top !== "1%"; - - // Support: Android 4.0 - 4.3 only, Firefox <=3 - 44 - reliableMarginLeftVal = roundPixelMeasures( divStyle.marginLeft ) === 12; - - // Support: Android 4.0 - 4.3 only, Safari <=9.1 - 10.1, iOS <=7.0 - 9.3 - // Some styles come back with percentage values, even though they shouldn't - div.style.right = "60%"; - pixelBoxStylesVal = roundPixelMeasures( divStyle.right ) === 36; - - // Support: IE 9 - 11 only - // Detect misreporting of content dimensions for box-sizing:border-box elements - boxSizingReliableVal = roundPixelMeasures( divStyle.width ) === 36; - - // Support: IE 9 only - // Detect overflow:scroll screwiness (gh-3699) - // Support: Chrome <=64 - // Don't get tricked when zoom affects offsetWidth (gh-4029) - div.style.position = "absolute"; - scrollboxSizeVal = roundPixelMeasures( div.offsetWidth / 3 ) === 12; - - documentElement.removeChild( container ); - - // Nullify the div so it wouldn't be stored in the memory and - // it will also be a sign that checks already performed - div = null; - } - - function roundPixelMeasures( measure ) { - return Math.round( parseFloat( measure ) ); - } - - var pixelPositionVal, boxSizingReliableVal, scrollboxSizeVal, pixelBoxStylesVal, - reliableTrDimensionsVal, reliableMarginLeftVal, - container = document.createElement( "div" ), - div = document.createElement( "div" ); - - // Finish early in limited (non-browser) environments - if ( !div.style ) { - return; - } - - // Support: IE <=9 - 11 only - // Style of cloned element affects source element cloned (#8908) - div.style.backgroundClip = "content-box"; - div.cloneNode( true ).style.backgroundClip = ""; - support.clearCloneStyle = div.style.backgroundClip === "content-box"; - - jQuery.extend( support, { - boxSizingReliable: function() { - computeStyleTests(); - return boxSizingReliableVal; - }, - pixelBoxStyles: function() { - computeStyleTests(); - return pixelBoxStylesVal; - }, - pixelPosition: function() { - computeStyleTests(); - return pixelPositionVal; - }, - reliableMarginLeft: function() { - computeStyleTests(); - return reliableMarginLeftVal; - }, - scrollboxSize: function() { - computeStyleTests(); - return scrollboxSizeVal; - }, - - // Support: IE 9 - 11+, Edge 15 - 18+ - // IE/Edge misreport `getComputedStyle` of table rows with width/height - // set in CSS while `offset*` properties report correct values. - // Behavior in IE 9 is more subtle than in newer versions & it passes - // some versions of this test; make sure not to make it pass there! - // - // Support: Firefox 70+ - // Only Firefox includes border widths - // in computed dimensions. (gh-4529) - reliableTrDimensions: function() { - var table, tr, trChild, trStyle; - if ( reliableTrDimensionsVal == null ) { - table = document.createElement( "table" ); - tr = document.createElement( "tr" ); - trChild = document.createElement( "div" ); - - table.style.cssText = "position:absolute;left:-11111px;border-collapse:separate"; - tr.style.cssText = "border:1px solid"; - - // Support: Chrome 86+ - // Height set through cssText does not get applied. - // Computed height then comes back as 0. - tr.style.height = "1px"; - trChild.style.height = "9px"; - - // Support: Android 8 Chrome 86+ - // In our bodyBackground.html iframe, - // display for all div elements is set to "inline", - // which causes a problem only in Android 8 Chrome 86. - // Ensuring the div is display: block - // gets around this issue. - trChild.style.display = "block"; - - documentElement - .appendChild( table ) - .appendChild( tr ) - .appendChild( trChild ); - - trStyle = window.getComputedStyle( tr ); - reliableTrDimensionsVal = ( parseInt( trStyle.height, 10 ) + - parseInt( trStyle.borderTopWidth, 10 ) + - parseInt( trStyle.borderBottomWidth, 10 ) ) === tr.offsetHeight; - - documentElement.removeChild( table ); - } - return reliableTrDimensionsVal; - } - } ); -} )(); - - -function curCSS( elem, name, computed ) { - var width, minWidth, maxWidth, ret, - - // Support: Firefox 51+ - // Retrieving style before computed somehow - // fixes an issue with getting wrong values - // on detached elements - style = elem.style; - - computed = computed || getStyles( elem ); - - // getPropertyValue is needed for: - // .css('filter') (IE 9 only, #12537) - // .css('--customProperty) (#3144) - if ( computed ) { - ret = computed.getPropertyValue( name ) || computed[ name ]; - - if ( ret === "" && !isAttached( elem ) ) { - ret = jQuery.style( elem, name ); - } - - // A tribute to the "awesome hack by Dean Edwards" - // Android Browser returns percentage for some values, - // but width seems to be reliably pixels. - // This is against the CSSOM draft spec: - // https://drafts.csswg.org/cssom/#resolved-values - if ( !support.pixelBoxStyles() && rnumnonpx.test( ret ) && rboxStyle.test( name ) ) { - - // Remember the original values - width = style.width; - minWidth = style.minWidth; - maxWidth = style.maxWidth; - - // Put in the new values to get a computed value out - style.minWidth = style.maxWidth = style.width = ret; - ret = computed.width; - - // Revert the changed values - style.width = width; - style.minWidth = minWidth; - style.maxWidth = maxWidth; - } - } - - return ret !== undefined ? - - // Support: IE <=9 - 11 only - // IE returns zIndex value as an integer. - ret + "" : - ret; -} - - -function addGetHookIf( conditionFn, hookFn ) { - - // Define the hook, we'll check on the first run if it's really needed. - return { - get: function() { - if ( conditionFn() ) { - - // Hook not needed (or it's not possible to use it due - // to missing dependency), remove it. - delete this.get; - return; - } - - // Hook needed; redefine it so that the support test is not executed again. - return ( this.get = hookFn ).apply( this, arguments ); - } - }; -} - - -var cssPrefixes = [ "Webkit", "Moz", "ms" ], - emptyStyle = document.createElement( "div" ).style, - vendorProps = {}; - -// Return a vendor-prefixed property or undefined -function vendorPropName( name ) { - - // Check for vendor prefixed names - var capName = name[ 0 ].toUpperCase() + name.slice( 1 ), - i = cssPrefixes.length; - - while ( i-- ) { - name = cssPrefixes[ i ] + capName; - if ( name in emptyStyle ) { - return name; - } - } -} - -// Return a potentially-mapped jQuery.cssProps or vendor prefixed property -function finalPropName( name ) { - var final = jQuery.cssProps[ name ] || vendorProps[ name ]; - - if ( final ) { - return final; - } - if ( name in emptyStyle ) { - return name; - } - return vendorProps[ name ] = vendorPropName( name ) || name; -} - - -var - - // Swappable if display is none or starts with table - // except "table", "table-cell", or "table-caption" - // See here for display values: https://developer.mozilla.org/en-US/docs/CSS/display - rdisplayswap = /^(none|table(?!-c[ea]).+)/, - rcustomProp = /^--/, - cssShow = { position: "absolute", visibility: "hidden", display: "block" }, - cssNormalTransform = { - letterSpacing: "0", - fontWeight: "400" - }; - -function setPositiveNumber( _elem, value, subtract ) { - - // Any relative (+/-) values have already been - // normalized at this point - var matches = rcssNum.exec( value ); - return matches ? - - // Guard against undefined "subtract", e.g., when used as in cssHooks - Math.max( 0, matches[ 2 ] - ( subtract || 0 ) ) + ( matches[ 3 ] || "px" ) : - value; -} - -function boxModelAdjustment( elem, dimension, box, isBorderBox, styles, computedVal ) { - var i = dimension === "width" ? 1 : 0, - extra = 0, - delta = 0; - - // Adjustment may not be necessary - if ( box === ( isBorderBox ? "border" : "content" ) ) { - return 0; - } - - for ( ; i < 4; i += 2 ) { - - // Both box models exclude margin - if ( box === "margin" ) { - delta += jQuery.css( elem, box + cssExpand[ i ], true, styles ); - } - - // If we get here with a content-box, we're seeking "padding" or "border" or "margin" - if ( !isBorderBox ) { - - // Add padding - delta += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); - - // For "border" or "margin", add border - if ( box !== "padding" ) { - delta += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); - - // But still keep track of it otherwise - } else { - extra += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); - } - - // If we get here with a border-box (content + padding + border), we're seeking "content" or - // "padding" or "margin" - } else { - - // For "content", subtract padding - if ( box === "content" ) { - delta -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); - } - - // For "content" or "padding", subtract border - if ( box !== "margin" ) { - delta -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); - } - } - } - - // Account for positive content-box scroll gutter when requested by providing computedVal - if ( !isBorderBox && computedVal >= 0 ) { - - // offsetWidth/offsetHeight is a rounded sum of content, padding, scroll gutter, and border - // Assuming integer scroll gutter, subtract the rest and round down - delta += Math.max( 0, Math.ceil( - elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - - computedVal - - delta - - extra - - 0.5 - - // If offsetWidth/offsetHeight is unknown, then we can't determine content-box scroll gutter - // Use an explicit zero to avoid NaN (gh-3964) - ) ) || 0; - } - - return delta; -} - -function getWidthOrHeight( elem, dimension, extra ) { - - // Start with computed style - var styles = getStyles( elem ), - - // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-4322). - // Fake content-box until we know it's needed to know the true value. - boxSizingNeeded = !support.boxSizingReliable() || extra, - isBorderBox = boxSizingNeeded && - jQuery.css( elem, "boxSizing", false, styles ) === "border-box", - valueIsBorderBox = isBorderBox, - - val = curCSS( elem, dimension, styles ), - offsetProp = "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ); - - // Support: Firefox <=54 - // Return a confounding non-pixel value or feign ignorance, as appropriate. - if ( rnumnonpx.test( val ) ) { - if ( !extra ) { - return val; - } - val = "auto"; - } - - - // Support: IE 9 - 11 only - // Use offsetWidth/offsetHeight for when box sizing is unreliable. - // In those cases, the computed value can be trusted to be border-box. - if ( ( !support.boxSizingReliable() && isBorderBox || - - // Support: IE 10 - 11+, Edge 15 - 18+ - // IE/Edge misreport `getComputedStyle` of table rows with width/height - // set in CSS while `offset*` properties report correct values. - // Interestingly, in some cases IE 9 doesn't suffer from this issue. - !support.reliableTrDimensions() && nodeName( elem, "tr" ) || - - // Fall back to offsetWidth/offsetHeight when value is "auto" - // This happens for inline elements with no explicit setting (gh-3571) - val === "auto" || - - // Support: Android <=4.1 - 4.3 only - // Also use offsetWidth/offsetHeight for misreported inline dimensions (gh-3602) - !parseFloat( val ) && jQuery.css( elem, "display", false, styles ) === "inline" ) && - - // Make sure the element is visible & connected - elem.getClientRects().length ) { - - isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; - - // Where available, offsetWidth/offsetHeight approximate border box dimensions. - // Where not available (e.g., SVG), assume unreliable box-sizing and interpret the - // retrieved value as a content box dimension. - valueIsBorderBox = offsetProp in elem; - if ( valueIsBorderBox ) { - val = elem[ offsetProp ]; - } - } - - // Normalize "" and auto - val = parseFloat( val ) || 0; - - // Adjust for the element's box model - return ( val + - boxModelAdjustment( - elem, - dimension, - extra || ( isBorderBox ? "border" : "content" ), - valueIsBorderBox, - styles, - - // Provide the current computed size to request scroll gutter calculation (gh-3589) - val - ) - ) + "px"; -} - -jQuery.extend( { - - // Add in style property hooks for overriding the default - // behavior of getting and setting a style property - cssHooks: { - opacity: { - get: function( elem, computed ) { - if ( computed ) { - - // We should always get a number back from opacity - var ret = curCSS( elem, "opacity" ); - return ret === "" ? "1" : ret; - } - } - } - }, - - // Don't automatically add "px" to these possibly-unitless properties - cssNumber: { - "animationIterationCount": true, - "columnCount": true, - "fillOpacity": true, - "flexGrow": true, - "flexShrink": true, - "fontWeight": true, - "gridArea": true, - "gridColumn": true, - "gridColumnEnd": true, - "gridColumnStart": true, - "gridRow": true, - "gridRowEnd": true, - "gridRowStart": true, - "lineHeight": true, - "opacity": true, - "order": true, - "orphans": true, - "widows": true, - "zIndex": true, - "zoom": true - }, - - // Add in properties whose names you wish to fix before - // setting or getting the value - cssProps: {}, - - // Get and set the style property on a DOM Node - style: function( elem, name, value, extra ) { - - // Don't set styles on text and comment nodes - if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { - return; - } - - // Make sure that we're working with the right name - var ret, type, hooks, - origName = camelCase( name ), - isCustomProp = rcustomProp.test( name ), - style = elem.style; - - // Make sure that we're working with the right name. We don't - // want to query the value if it is a CSS custom property - // since they are user-defined. - if ( !isCustomProp ) { - name = finalPropName( origName ); - } - - // Gets hook for the prefixed version, then unprefixed version - hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; - - // Check if we're setting a value - if ( value !== undefined ) { - type = typeof value; - - // Convert "+=" or "-=" to relative numbers (#7345) - if ( type === "string" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) { - value = adjustCSS( elem, name, ret ); - - // Fixes bug #9237 - type = "number"; - } - - // Make sure that null and NaN values aren't set (#7116) - if ( value == null || value !== value ) { - return; - } - - // If a number was passed in, add the unit (except for certain CSS properties) - // The isCustomProp check can be removed in jQuery 4.0 when we only auto-append - // "px" to a few hardcoded values. - if ( type === "number" && !isCustomProp ) { - value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "" : "px" ); - } - - // background-* props affect original clone's values - if ( !support.clearCloneStyle && value === "" && name.indexOf( "background" ) === 0 ) { - style[ name ] = "inherit"; - } - - // If a hook was provided, use that value, otherwise just set the specified value - if ( !hooks || !( "set" in hooks ) || - ( value = hooks.set( elem, value, extra ) ) !== undefined ) { - - if ( isCustomProp ) { - style.setProperty( name, value ); - } else { - style[ name ] = value; - } - } - - } else { - - // If a hook was provided get the non-computed value from there - if ( hooks && "get" in hooks && - ( ret = hooks.get( elem, false, extra ) ) !== undefined ) { - - return ret; - } - - // Otherwise just get the value from the style object - return style[ name ]; - } - }, - - css: function( elem, name, extra, styles ) { - var val, num, hooks, - origName = camelCase( name ), - isCustomProp = rcustomProp.test( name ); - - // Make sure that we're working with the right name. We don't - // want to modify the value if it is a CSS custom property - // since they are user-defined. - if ( !isCustomProp ) { - name = finalPropName( origName ); - } - - // Try prefixed name followed by the unprefixed name - hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; - - // If a hook was provided get the computed value from there - if ( hooks && "get" in hooks ) { - val = hooks.get( elem, true, extra ); - } - - // Otherwise, if a way to get the computed value exists, use that - if ( val === undefined ) { - val = curCSS( elem, name, styles ); - } - - // Convert "normal" to computed value - if ( val === "normal" && name in cssNormalTransform ) { - val = cssNormalTransform[ name ]; - } - - // Make numeric if forced or a qualifier was provided and val looks numeric - if ( extra === "" || extra ) { - num = parseFloat( val ); - return extra === true || isFinite( num ) ? num || 0 : val; - } - - return val; - } -} ); - -jQuery.each( [ "height", "width" ], function( _i, dimension ) { - jQuery.cssHooks[ dimension ] = { - get: function( elem, computed, extra ) { - if ( computed ) { - - // Certain elements can have dimension info if we invisibly show them - // but it must have a current display style that would benefit - return rdisplayswap.test( jQuery.css( elem, "display" ) ) && - - // Support: Safari 8+ - // Table columns in Safari have non-zero offsetWidth & zero - // getBoundingClientRect().width unless display is changed. - // Support: IE <=11 only - // Running getBoundingClientRect on a disconnected node - // in IE throws an error. - ( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ? - swap( elem, cssShow, function() { - return getWidthOrHeight( elem, dimension, extra ); - } ) : - getWidthOrHeight( elem, dimension, extra ); - } - }, - - set: function( elem, value, extra ) { - var matches, - styles = getStyles( elem ), - - // Only read styles.position if the test has a chance to fail - // to avoid forcing a reflow. - scrollboxSizeBuggy = !support.scrollboxSize() && - styles.position === "absolute", - - // To avoid forcing a reflow, only fetch boxSizing if we need it (gh-3991) - boxSizingNeeded = scrollboxSizeBuggy || extra, - isBorderBox = boxSizingNeeded && - jQuery.css( elem, "boxSizing", false, styles ) === "border-box", - subtract = extra ? - boxModelAdjustment( - elem, - dimension, - extra, - isBorderBox, - styles - ) : - 0; - - // Account for unreliable border-box dimensions by comparing offset* to computed and - // faking a content-box to get border and padding (gh-3699) - if ( isBorderBox && scrollboxSizeBuggy ) { - subtract -= Math.ceil( - elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - - parseFloat( styles[ dimension ] ) - - boxModelAdjustment( elem, dimension, "border", false, styles ) - - 0.5 - ); - } - - // Convert to pixels if value adjustment is needed - if ( subtract && ( matches = rcssNum.exec( value ) ) && - ( matches[ 3 ] || "px" ) !== "px" ) { - - elem.style[ dimension ] = value; - value = jQuery.css( elem, dimension ); - } - - return setPositiveNumber( elem, value, subtract ); - } - }; -} ); - -jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft, - function( elem, computed ) { - if ( computed ) { - return ( parseFloat( curCSS( elem, "marginLeft" ) ) || - elem.getBoundingClientRect().left - - swap( elem, { marginLeft: 0 }, function() { - return elem.getBoundingClientRect().left; - } ) - ) + "px"; - } - } -); - -// These hooks are used by animate to expand properties -jQuery.each( { - margin: "", - padding: "", - border: "Width" -}, function( prefix, suffix ) { - jQuery.cssHooks[ prefix + suffix ] = { - expand: function( value ) { - var i = 0, - expanded = {}, - - // Assumes a single number if not a string - parts = typeof value === "string" ? value.split( " " ) : [ value ]; - - for ( ; i < 4; i++ ) { - expanded[ prefix + cssExpand[ i ] + suffix ] = - parts[ i ] || parts[ i - 2 ] || parts[ 0 ]; - } - - return expanded; - } - }; - - if ( prefix !== "margin" ) { - jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber; - } -} ); - -jQuery.fn.extend( { - css: function( name, value ) { - return access( this, function( elem, name, value ) { - var styles, len, - map = {}, - i = 0; - - if ( Array.isArray( name ) ) { - styles = getStyles( elem ); - len = name.length; - - for ( ; i < len; i++ ) { - map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); - } - - return map; - } - - return value !== undefined ? - jQuery.style( elem, name, value ) : - jQuery.css( elem, name ); - }, name, value, arguments.length > 1 ); - } -} ); - - -function Tween( elem, options, prop, end, easing ) { - return new Tween.prototype.init( elem, options, prop, end, easing ); -} -jQuery.Tween = Tween; - -Tween.prototype = { - constructor: Tween, - init: function( elem, options, prop, end, easing, unit ) { - this.elem = elem; - this.prop = prop; - this.easing = easing || jQuery.easing._default; - this.options = options; - this.start = this.now = this.cur(); - this.end = end; - this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" ); - }, - cur: function() { - var hooks = Tween.propHooks[ this.prop ]; - - return hooks && hooks.get ? - hooks.get( this ) : - Tween.propHooks._default.get( this ); - }, - run: function( percent ) { - var eased, - hooks = Tween.propHooks[ this.prop ]; - - if ( this.options.duration ) { - this.pos = eased = jQuery.easing[ this.easing ]( - percent, this.options.duration * percent, 0, 1, this.options.duration - ); - } else { - this.pos = eased = percent; - } - this.now = ( this.end - this.start ) * eased + this.start; - - if ( this.options.step ) { - this.options.step.call( this.elem, this.now, this ); - } - - if ( hooks && hooks.set ) { - hooks.set( this ); - } else { - Tween.propHooks._default.set( this ); - } - return this; - } -}; - -Tween.prototype.init.prototype = Tween.prototype; - -Tween.propHooks = { - _default: { - get: function( tween ) { - var result; - - // Use a property on the element directly when it is not a DOM element, - // or when there is no matching style property that exists. - if ( tween.elem.nodeType !== 1 || - tween.elem[ tween.prop ] != null && tween.elem.style[ tween.prop ] == null ) { - return tween.elem[ tween.prop ]; - } - - // Passing an empty string as a 3rd parameter to .css will automatically - // attempt a parseFloat and fallback to a string if the parse fails. - // Simple values such as "10px" are parsed to Float; - // complex values such as "rotate(1rad)" are returned as-is. - result = jQuery.css( tween.elem, tween.prop, "" ); - - // Empty strings, null, undefined and "auto" are converted to 0. - return !result || result === "auto" ? 0 : result; - }, - set: function( tween ) { - - // Use step hook for back compat. - // Use cssHook if its there. - // Use .style if available and use plain properties where available. - if ( jQuery.fx.step[ tween.prop ] ) { - jQuery.fx.step[ tween.prop ]( tween ); - } else if ( tween.elem.nodeType === 1 && ( - jQuery.cssHooks[ tween.prop ] || - tween.elem.style[ finalPropName( tween.prop ) ] != null ) ) { - jQuery.style( tween.elem, tween.prop, tween.now + tween.unit ); - } else { - tween.elem[ tween.prop ] = tween.now; - } - } - } -}; - -// Support: IE <=9 only -// Panic based approach to setting things on disconnected nodes -Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = { - set: function( tween ) { - if ( tween.elem.nodeType && tween.elem.parentNode ) { - tween.elem[ tween.prop ] = tween.now; - } - } -}; - -jQuery.easing = { - linear: function( p ) { - return p; - }, - swing: function( p ) { - return 0.5 - Math.cos( p * Math.PI ) / 2; - }, - _default: "swing" -}; - -jQuery.fx = Tween.prototype.init; - -// Back compat <1.8 extension point -jQuery.fx.step = {}; - - - - -var - fxNow, inProgress, - rfxtypes = /^(?:toggle|show|hide)$/, - rrun = /queueHooks$/; - -function schedule() { - if ( inProgress ) { - if ( document.hidden === false && window.requestAnimationFrame ) { - window.requestAnimationFrame( schedule ); - } else { - window.setTimeout( schedule, jQuery.fx.interval ); - } - - jQuery.fx.tick(); - } -} - -// Animations created synchronously will run synchronously -function createFxNow() { - window.setTimeout( function() { - fxNow = undefined; - } ); - return ( fxNow = Date.now() ); -} - -// Generate parameters to create a standard animation -function genFx( type, includeWidth ) { - var which, - i = 0, - attrs = { height: type }; - - // If we include width, step value is 1 to do all cssExpand values, - // otherwise step value is 2 to skip over Left and Right - includeWidth = includeWidth ? 1 : 0; - for ( ; i < 4; i += 2 - includeWidth ) { - which = cssExpand[ i ]; - attrs[ "margin" + which ] = attrs[ "padding" + which ] = type; - } - - if ( includeWidth ) { - attrs.opacity = attrs.width = type; - } - - return attrs; -} - -function createTween( value, prop, animation ) { - var tween, - collection = ( Animation.tweeners[ prop ] || [] ).concat( Animation.tweeners[ "*" ] ), - index = 0, - length = collection.length; - for ( ; index < length; index++ ) { - if ( ( tween = collection[ index ].call( animation, prop, value ) ) ) { - - // We're done with this property - return tween; - } - } -} - -function defaultPrefilter( elem, props, opts ) { - var prop, value, toggle, hooks, oldfire, propTween, restoreDisplay, display, - isBox = "width" in props || "height" in props, - anim = this, - orig = {}, - style = elem.style, - hidden = elem.nodeType && isHiddenWithinTree( elem ), - dataShow = dataPriv.get( elem, "fxshow" ); - - // Queue-skipping animations hijack the fx hooks - if ( !opts.queue ) { - hooks = jQuery._queueHooks( elem, "fx" ); - if ( hooks.unqueued == null ) { - hooks.unqueued = 0; - oldfire = hooks.empty.fire; - hooks.empty.fire = function() { - if ( !hooks.unqueued ) { - oldfire(); - } - }; - } - hooks.unqueued++; - - anim.always( function() { - - // Ensure the complete handler is called before this completes - anim.always( function() { - hooks.unqueued--; - if ( !jQuery.queue( elem, "fx" ).length ) { - hooks.empty.fire(); - } - } ); - } ); - } - - // Detect show/hide animations - for ( prop in props ) { - value = props[ prop ]; - if ( rfxtypes.test( value ) ) { - delete props[ prop ]; - toggle = toggle || value === "toggle"; - if ( value === ( hidden ? "hide" : "show" ) ) { - - // Pretend to be hidden if this is a "show" and - // there is still data from a stopped show/hide - if ( value === "show" && dataShow && dataShow[ prop ] !== undefined ) { - hidden = true; - - // Ignore all other no-op show/hide data - } else { - continue; - } - } - orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop ); - } - } - - // Bail out if this is a no-op like .hide().hide() - propTween = !jQuery.isEmptyObject( props ); - if ( !propTween && jQuery.isEmptyObject( orig ) ) { - return; - } - - // Restrict "overflow" and "display" styles during box animations - if ( isBox && elem.nodeType === 1 ) { - - // Support: IE <=9 - 11, Edge 12 - 15 - // Record all 3 overflow attributes because IE does not infer the shorthand - // from identically-valued overflowX and overflowY and Edge just mirrors - // the overflowX value there. - opts.overflow = [ style.overflow, style.overflowX, style.overflowY ]; - - // Identify a display type, preferring old show/hide data over the CSS cascade - restoreDisplay = dataShow && dataShow.display; - if ( restoreDisplay == null ) { - restoreDisplay = dataPriv.get( elem, "display" ); - } - display = jQuery.css( elem, "display" ); - if ( display === "none" ) { - if ( restoreDisplay ) { - display = restoreDisplay; - } else { - - // Get nonempty value(s) by temporarily forcing visibility - showHide( [ elem ], true ); - restoreDisplay = elem.style.display || restoreDisplay; - display = jQuery.css( elem, "display" ); - showHide( [ elem ] ); - } - } - - // Animate inline elements as inline-block - if ( display === "inline" || display === "inline-block" && restoreDisplay != null ) { - if ( jQuery.css( elem, "float" ) === "none" ) { - - // Restore the original display value at the end of pure show/hide animations - if ( !propTween ) { - anim.done( function() { - style.display = restoreDisplay; - } ); - if ( restoreDisplay == null ) { - display = style.display; - restoreDisplay = display === "none" ? "" : display; - } - } - style.display = "inline-block"; - } - } - } - - if ( opts.overflow ) { - style.overflow = "hidden"; - anim.always( function() { - style.overflow = opts.overflow[ 0 ]; - style.overflowX = opts.overflow[ 1 ]; - style.overflowY = opts.overflow[ 2 ]; - } ); - } - - // Implement show/hide animations - propTween = false; - for ( prop in orig ) { - - // General show/hide setup for this element animation - if ( !propTween ) { - if ( dataShow ) { - if ( "hidden" in dataShow ) { - hidden = dataShow.hidden; - } - } else { - dataShow = dataPriv.access( elem, "fxshow", { display: restoreDisplay } ); - } - - // Store hidden/visible for toggle so `.stop().toggle()` "reverses" - if ( toggle ) { - dataShow.hidden = !hidden; - } - - // Show elements before animating them - if ( hidden ) { - showHide( [ elem ], true ); - } - - /* eslint-disable no-loop-func */ - - anim.done( function() { - - /* eslint-enable no-loop-func */ - - // The final step of a "hide" animation is actually hiding the element - if ( !hidden ) { - showHide( [ elem ] ); - } - dataPriv.remove( elem, "fxshow" ); - for ( prop in orig ) { - jQuery.style( elem, prop, orig[ prop ] ); - } - } ); - } - - // Per-property setup - propTween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim ); - if ( !( prop in dataShow ) ) { - dataShow[ prop ] = propTween.start; - if ( hidden ) { - propTween.end = propTween.start; - propTween.start = 0; - } - } - } -} - -function propFilter( props, specialEasing ) { - var index, name, easing, value, hooks; - - // camelCase, specialEasing and expand cssHook pass - for ( index in props ) { - name = camelCase( index ); - easing = specialEasing[ name ]; - value = props[ index ]; - if ( Array.isArray( value ) ) { - easing = value[ 1 ]; - value = props[ index ] = value[ 0 ]; - } - - if ( index !== name ) { - props[ name ] = value; - delete props[ index ]; - } - - hooks = jQuery.cssHooks[ name ]; - if ( hooks && "expand" in hooks ) { - value = hooks.expand( value ); - delete props[ name ]; - - // Not quite $.extend, this won't overwrite existing keys. - // Reusing 'index' because we have the correct "name" - for ( index in value ) { - if ( !( index in props ) ) { - props[ index ] = value[ index ]; - specialEasing[ index ] = easing; - } - } - } else { - specialEasing[ name ] = easing; - } - } -} - -function Animation( elem, properties, options ) { - var result, - stopped, - index = 0, - length = Animation.prefilters.length, - deferred = jQuery.Deferred().always( function() { - - // Don't match elem in the :animated selector - delete tick.elem; - } ), - tick = function() { - if ( stopped ) { - return false; - } - var currentTime = fxNow || createFxNow(), - remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ), - - // Support: Android 2.3 only - // Archaic crash bug won't allow us to use `1 - ( 0.5 || 0 )` (#12497) - temp = remaining / animation.duration || 0, - percent = 1 - temp, - index = 0, - length = animation.tweens.length; - - for ( ; index < length; index++ ) { - animation.tweens[ index ].run( percent ); - } - - deferred.notifyWith( elem, [ animation, percent, remaining ] ); - - // If there's more to do, yield - if ( percent < 1 && length ) { - return remaining; - } - - // If this was an empty animation, synthesize a final progress notification - if ( !length ) { - deferred.notifyWith( elem, [ animation, 1, 0 ] ); - } - - // Resolve the animation and report its conclusion - deferred.resolveWith( elem, [ animation ] ); - return false; - }, - animation = deferred.promise( { - elem: elem, - props: jQuery.extend( {}, properties ), - opts: jQuery.extend( true, { - specialEasing: {}, - easing: jQuery.easing._default - }, options ), - originalProperties: properties, - originalOptions: options, - startTime: fxNow || createFxNow(), - duration: options.duration, - tweens: [], - createTween: function( prop, end ) { - var tween = jQuery.Tween( elem, animation.opts, prop, end, - animation.opts.specialEasing[ prop ] || animation.opts.easing ); - animation.tweens.push( tween ); - return tween; - }, - stop: function( gotoEnd ) { - var index = 0, - - // If we are going to the end, we want to run all the tweens - // otherwise we skip this part - length = gotoEnd ? animation.tweens.length : 0; - if ( stopped ) { - return this; - } - stopped = true; - for ( ; index < length; index++ ) { - animation.tweens[ index ].run( 1 ); - } - - // Resolve when we played the last frame; otherwise, reject - if ( gotoEnd ) { - deferred.notifyWith( elem, [ animation, 1, 0 ] ); - deferred.resolveWith( elem, [ animation, gotoEnd ] ); - } else { - deferred.rejectWith( elem, [ animation, gotoEnd ] ); - } - return this; - } - } ), - props = animation.props; - - propFilter( props, animation.opts.specialEasing ); - - for ( ; index < length; index++ ) { - result = Animation.prefilters[ index ].call( animation, elem, props, animation.opts ); - if ( result ) { - if ( isFunction( result.stop ) ) { - jQuery._queueHooks( animation.elem, animation.opts.queue ).stop = - result.stop.bind( result ); - } - return result; - } - } - - jQuery.map( props, createTween, animation ); - - if ( isFunction( animation.opts.start ) ) { - animation.opts.start.call( elem, animation ); - } - - // Attach callbacks from options - animation - .progress( animation.opts.progress ) - .done( animation.opts.done, animation.opts.complete ) - .fail( animation.opts.fail ) - .always( animation.opts.always ); - - jQuery.fx.timer( - jQuery.extend( tick, { - elem: elem, - anim: animation, - queue: animation.opts.queue - } ) - ); - - return animation; -} - -jQuery.Animation = jQuery.extend( Animation, { - - tweeners: { - "*": [ function( prop, value ) { - var tween = this.createTween( prop, value ); - adjustCSS( tween.elem, prop, rcssNum.exec( value ), tween ); - return tween; - } ] - }, - - tweener: function( props, callback ) { - if ( isFunction( props ) ) { - callback = props; - props = [ "*" ]; - } else { - props = props.match( rnothtmlwhite ); - } - - var prop, - index = 0, - length = props.length; - - for ( ; index < length; index++ ) { - prop = props[ index ]; - Animation.tweeners[ prop ] = Animation.tweeners[ prop ] || []; - Animation.tweeners[ prop ].unshift( callback ); - } - }, - - prefilters: [ defaultPrefilter ], - - prefilter: function( callback, prepend ) { - if ( prepend ) { - Animation.prefilters.unshift( callback ); - } else { - Animation.prefilters.push( callback ); - } - } -} ); - -jQuery.speed = function( speed, easing, fn ) { - var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { - complete: fn || !fn && easing || - isFunction( speed ) && speed, - duration: speed, - easing: fn && easing || easing && !isFunction( easing ) && easing - }; - - // Go to the end state if fx are off - if ( jQuery.fx.off ) { - opt.duration = 0; - - } else { - if ( typeof opt.duration !== "number" ) { - if ( opt.duration in jQuery.fx.speeds ) { - opt.duration = jQuery.fx.speeds[ opt.duration ]; - - } else { - opt.duration = jQuery.fx.speeds._default; - } - } - } - - // Normalize opt.queue - true/undefined/null -> "fx" - if ( opt.queue == null || opt.queue === true ) { - opt.queue = "fx"; - } - - // Queueing - opt.old = opt.complete; - - opt.complete = function() { - if ( isFunction( opt.old ) ) { - opt.old.call( this ); - } - - if ( opt.queue ) { - jQuery.dequeue( this, opt.queue ); - } - }; - - return opt; -}; - -jQuery.fn.extend( { - fadeTo: function( speed, to, easing, callback ) { - - // Show any hidden elements after setting opacity to 0 - return this.filter( isHiddenWithinTree ).css( "opacity", 0 ).show() - - // Animate to the value specified - .end().animate( { opacity: to }, speed, easing, callback ); - }, - animate: function( prop, speed, easing, callback ) { - var empty = jQuery.isEmptyObject( prop ), - optall = jQuery.speed( speed, easing, callback ), - doAnimation = function() { - - // Operate on a copy of prop so per-property easing won't be lost - var anim = Animation( this, jQuery.extend( {}, prop ), optall ); - - // Empty animations, or finishing resolves immediately - if ( empty || dataPriv.get( this, "finish" ) ) { - anim.stop( true ); - } - }; - - doAnimation.finish = doAnimation; - - return empty || optall.queue === false ? - this.each( doAnimation ) : - this.queue( optall.queue, doAnimation ); - }, - stop: function( type, clearQueue, gotoEnd ) { - var stopQueue = function( hooks ) { - var stop = hooks.stop; - delete hooks.stop; - stop( gotoEnd ); - }; - - if ( typeof type !== "string" ) { - gotoEnd = clearQueue; - clearQueue = type; - type = undefined; - } - if ( clearQueue ) { - this.queue( type || "fx", [] ); - } - - return this.each( function() { - var dequeue = true, - index = type != null && type + "queueHooks", - timers = jQuery.timers, - data = dataPriv.get( this ); - - if ( index ) { - if ( data[ index ] && data[ index ].stop ) { - stopQueue( data[ index ] ); - } - } else { - for ( index in data ) { - if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) { - stopQueue( data[ index ] ); - } - } - } - - for ( index = timers.length; index--; ) { - if ( timers[ index ].elem === this && - ( type == null || timers[ index ].queue === type ) ) { - - timers[ index ].anim.stop( gotoEnd ); - dequeue = false; - timers.splice( index, 1 ); - } - } - - // Start the next in the queue if the last step wasn't forced. - // Timers currently will call their complete callbacks, which - // will dequeue but only if they were gotoEnd. - if ( dequeue || !gotoEnd ) { - jQuery.dequeue( this, type ); - } - } ); - }, - finish: function( type ) { - if ( type !== false ) { - type = type || "fx"; - } - return this.each( function() { - var index, - data = dataPriv.get( this ), - queue = data[ type + "queue" ], - hooks = data[ type + "queueHooks" ], - timers = jQuery.timers, - length = queue ? queue.length : 0; - - // Enable finishing flag on private data - data.finish = true; - - // Empty the queue first - jQuery.queue( this, type, [] ); - - if ( hooks && hooks.stop ) { - hooks.stop.call( this, true ); - } - - // Look for any active animations, and finish them - for ( index = timers.length; index--; ) { - if ( timers[ index ].elem === this && timers[ index ].queue === type ) { - timers[ index ].anim.stop( true ); - timers.splice( index, 1 ); - } - } - - // Look for any animations in the old queue and finish them - for ( index = 0; index < length; index++ ) { - if ( queue[ index ] && queue[ index ].finish ) { - queue[ index ].finish.call( this ); - } - } - - // Turn off finishing flag - delete data.finish; - } ); - } -} ); - -jQuery.each( [ "toggle", "show", "hide" ], function( _i, name ) { - var cssFn = jQuery.fn[ name ]; - jQuery.fn[ name ] = function( speed, easing, callback ) { - return speed == null || typeof speed === "boolean" ? - cssFn.apply( this, arguments ) : - this.animate( genFx( name, true ), speed, easing, callback ); - }; -} ); - -// Generate shortcuts for custom animations -jQuery.each( { - slideDown: genFx( "show" ), - slideUp: genFx( "hide" ), - slideToggle: genFx( "toggle" ), - fadeIn: { opacity: "show" }, - fadeOut: { opacity: "hide" }, - fadeToggle: { opacity: "toggle" } -}, function( name, props ) { - jQuery.fn[ name ] = function( speed, easing, callback ) { - return this.animate( props, speed, easing, callback ); - }; -} ); - -jQuery.timers = []; -jQuery.fx.tick = function() { - var timer, - i = 0, - timers = jQuery.timers; - - fxNow = Date.now(); - - for ( ; i < timers.length; i++ ) { - timer = timers[ i ]; - - // Run the timer and safely remove it when done (allowing for external removal) - if ( !timer() && timers[ i ] === timer ) { - timers.splice( i--, 1 ); - } - } - - if ( !timers.length ) { - jQuery.fx.stop(); - } - fxNow = undefined; -}; - -jQuery.fx.timer = function( timer ) { - jQuery.timers.push( timer ); - jQuery.fx.start(); -}; - -jQuery.fx.interval = 13; -jQuery.fx.start = function() { - if ( inProgress ) { - return; - } - - inProgress = true; - schedule(); -}; - -jQuery.fx.stop = function() { - inProgress = null; -}; - -jQuery.fx.speeds = { - slow: 600, - fast: 200, - - // Default speed - _default: 400 -}; - - -// Based off of the plugin by Clint Helfers, with permission. -// https://web.archive.org/web/20100324014747/http://blindsignals.com/index.php/2009/07/jquery-delay/ -jQuery.fn.delay = function( time, type ) { - time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; - type = type || "fx"; - - return this.queue( type, function( next, hooks ) { - var timeout = window.setTimeout( next, time ); - hooks.stop = function() { - window.clearTimeout( timeout ); - }; - } ); -}; - - -( function() { - var input = document.createElement( "input" ), - select = document.createElement( "select" ), - opt = select.appendChild( document.createElement( "option" ) ); - - input.type = "checkbox"; - - // Support: Android <=4.3 only - // Default value for a checkbox should be "on" - support.checkOn = input.value !== ""; - - // Support: IE <=11 only - // Must access selectedIndex to make default options select - support.optSelected = opt.selected; - - // Support: IE <=11 only - // An input loses its value after becoming a radio - input = document.createElement( "input" ); - input.value = "t"; - input.type = "radio"; - support.radioValue = input.value === "t"; -} )(); - - -var boolHook, - attrHandle = jQuery.expr.attrHandle; - -jQuery.fn.extend( { - attr: function( name, value ) { - return access( this, jQuery.attr, name, value, arguments.length > 1 ); - }, - - removeAttr: function( name ) { - return this.each( function() { - jQuery.removeAttr( this, name ); - } ); - } -} ); - -jQuery.extend( { - attr: function( elem, name, value ) { - var ret, hooks, - nType = elem.nodeType; - - // Don't get/set attributes on text, comment and attribute nodes - if ( nType === 3 || nType === 8 || nType === 2 ) { - return; - } - - // Fallback to prop when attributes are not supported - if ( typeof elem.getAttribute === "undefined" ) { - return jQuery.prop( elem, name, value ); - } - - // Attribute hooks are determined by the lowercase version - // Grab necessary hook if one is defined - if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { - hooks = jQuery.attrHooks[ name.toLowerCase() ] || - ( jQuery.expr.match.bool.test( name ) ? boolHook : undefined ); - } - - if ( value !== undefined ) { - if ( value === null ) { - jQuery.removeAttr( elem, name ); - return; - } - - if ( hooks && "set" in hooks && - ( ret = hooks.set( elem, value, name ) ) !== undefined ) { - return ret; - } - - elem.setAttribute( name, value + "" ); - return value; - } - - if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { - return ret; - } - - ret = jQuery.find.attr( elem, name ); - - // Non-existent attributes return null, we normalize to undefined - return ret == null ? undefined : ret; - }, - - attrHooks: { - type: { - set: function( elem, value ) { - if ( !support.radioValue && value === "radio" && - nodeName( elem, "input" ) ) { - var val = elem.value; - elem.setAttribute( "type", value ); - if ( val ) { - elem.value = val; - } - return value; - } - } - } - }, - - removeAttr: function( elem, value ) { - var name, - i = 0, - - // Attribute names can contain non-HTML whitespace characters - // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 - attrNames = value && value.match( rnothtmlwhite ); - - if ( attrNames && elem.nodeType === 1 ) { - while ( ( name = attrNames[ i++ ] ) ) { - elem.removeAttribute( name ); - } - } - } -} ); - -// Hooks for boolean attributes -boolHook = { - set: function( elem, value, name ) { - if ( value === false ) { - - // Remove boolean attributes when set to false - jQuery.removeAttr( elem, name ); - } else { - elem.setAttribute( name, name ); - } - return name; - } -}; - -jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( _i, name ) { - var getter = attrHandle[ name ] || jQuery.find.attr; - - attrHandle[ name ] = function( elem, name, isXML ) { - var ret, handle, - lowercaseName = name.toLowerCase(); - - if ( !isXML ) { - - // Avoid an infinite loop by temporarily removing this function from the getter - handle = attrHandle[ lowercaseName ]; - attrHandle[ lowercaseName ] = ret; - ret = getter( elem, name, isXML ) != null ? - lowercaseName : - null; - attrHandle[ lowercaseName ] = handle; - } - return ret; - }; -} ); - - - - -var rfocusable = /^(?:input|select|textarea|button)$/i, - rclickable = /^(?:a|area)$/i; - -jQuery.fn.extend( { - prop: function( name, value ) { - return access( this, jQuery.prop, name, value, arguments.length > 1 ); - }, - - removeProp: function( name ) { - return this.each( function() { - delete this[ jQuery.propFix[ name ] || name ]; - } ); - } -} ); - -jQuery.extend( { - prop: function( elem, name, value ) { - var ret, hooks, - nType = elem.nodeType; - - // Don't get/set properties on text, comment and attribute nodes - if ( nType === 3 || nType === 8 || nType === 2 ) { - return; - } - - if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { - - // Fix name and attach hooks - name = jQuery.propFix[ name ] || name; - hooks = jQuery.propHooks[ name ]; - } - - if ( value !== undefined ) { - if ( hooks && "set" in hooks && - ( ret = hooks.set( elem, value, name ) ) !== undefined ) { - return ret; - } - - return ( elem[ name ] = value ); - } - - if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { - return ret; - } - - return elem[ name ]; - }, - - propHooks: { - tabIndex: { - get: function( elem ) { - - // Support: IE <=9 - 11 only - // elem.tabIndex doesn't always return the - // correct value when it hasn't been explicitly set - // https://web.archive.org/web/20141116233347/http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ - // Use proper attribute retrieval(#12072) - var tabindex = jQuery.find.attr( elem, "tabindex" ); - - if ( tabindex ) { - return parseInt( tabindex, 10 ); - } - - if ( - rfocusable.test( elem.nodeName ) || - rclickable.test( elem.nodeName ) && - elem.href - ) { - return 0; - } - - return -1; - } - } - }, - - propFix: { - "for": "htmlFor", - "class": "className" - } -} ); - -// Support: IE <=11 only -// Accessing the selectedIndex property -// forces the browser to respect setting selected -// on the option -// The getter ensures a default option is selected -// when in an optgroup -// eslint rule "no-unused-expressions" is disabled for this code -// since it considers such accessions noop -if ( !support.optSelected ) { - jQuery.propHooks.selected = { - get: function( elem ) { - - /* eslint no-unused-expressions: "off" */ - - var parent = elem.parentNode; - if ( parent && parent.parentNode ) { - parent.parentNode.selectedIndex; - } - return null; - }, - set: function( elem ) { - - /* eslint no-unused-expressions: "off" */ - - var parent = elem.parentNode; - if ( parent ) { - parent.selectedIndex; - - if ( parent.parentNode ) { - parent.parentNode.selectedIndex; - } - } - } - }; -} - -jQuery.each( [ - "tabIndex", - "readOnly", - "maxLength", - "cellSpacing", - "cellPadding", - "rowSpan", - "colSpan", - "useMap", - "frameBorder", - "contentEditable" -], function() { - jQuery.propFix[ this.toLowerCase() ] = this; -} ); - - - - - // Strip and collapse whitespace according to HTML spec - // https://infra.spec.whatwg.org/#strip-and-collapse-ascii-whitespace - function stripAndCollapse( value ) { - var tokens = value.match( rnothtmlwhite ) || []; - return tokens.join( " " ); - } - - -function getClass( elem ) { - return elem.getAttribute && elem.getAttribute( "class" ) || ""; -} - -function classesToArray( value ) { - if ( Array.isArray( value ) ) { - return value; - } - if ( typeof value === "string" ) { - return value.match( rnothtmlwhite ) || []; - } - return []; -} - -jQuery.fn.extend( { - addClass: function( value ) { - var classes, elem, cur, curValue, clazz, j, finalValue, - i = 0; - - if ( isFunction( value ) ) { - return this.each( function( j ) { - jQuery( this ).addClass( value.call( this, j, getClass( this ) ) ); - } ); - } - - classes = classesToArray( value ); - - if ( classes.length ) { - while ( ( elem = this[ i++ ] ) ) { - curValue = getClass( elem ); - cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); - - if ( cur ) { - j = 0; - while ( ( clazz = classes[ j++ ] ) ) { - if ( cur.indexOf( " " + clazz + " " ) < 0 ) { - cur += clazz + " "; - } - } - - // Only assign if different to avoid unneeded rendering. - finalValue = stripAndCollapse( cur ); - if ( curValue !== finalValue ) { - elem.setAttribute( "class", finalValue ); - } - } - } - } - - return this; - }, - - removeClass: function( value ) { - var classes, elem, cur, curValue, clazz, j, finalValue, - i = 0; - - if ( isFunction( value ) ) { - return this.each( function( j ) { - jQuery( this ).removeClass( value.call( this, j, getClass( this ) ) ); - } ); - } - - if ( !arguments.length ) { - return this.attr( "class", "" ); - } - - classes = classesToArray( value ); - - if ( classes.length ) { - while ( ( elem = this[ i++ ] ) ) { - curValue = getClass( elem ); - - // This expression is here for better compressibility (see addClass) - cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); - - if ( cur ) { - j = 0; - while ( ( clazz = classes[ j++ ] ) ) { - - // Remove *all* instances - while ( cur.indexOf( " " + clazz + " " ) > -1 ) { - cur = cur.replace( " " + clazz + " ", " " ); - } - } - - // Only assign if different to avoid unneeded rendering. - finalValue = stripAndCollapse( cur ); - if ( curValue !== finalValue ) { - elem.setAttribute( "class", finalValue ); - } - } - } - } - - return this; - }, - - toggleClass: function( value, stateVal ) { - var type = typeof value, - isValidValue = type === "string" || Array.isArray( value ); - - if ( typeof stateVal === "boolean" && isValidValue ) { - return stateVal ? this.addClass( value ) : this.removeClass( value ); - } - - if ( isFunction( value ) ) { - return this.each( function( i ) { - jQuery( this ).toggleClass( - value.call( this, i, getClass( this ), stateVal ), - stateVal - ); - } ); - } - - return this.each( function() { - var className, i, self, classNames; - - if ( isValidValue ) { - - // Toggle individual class names - i = 0; - self = jQuery( this ); - classNames = classesToArray( value ); - - while ( ( className = classNames[ i++ ] ) ) { - - // Check each className given, space separated list - if ( self.hasClass( className ) ) { - self.removeClass( className ); - } else { - self.addClass( className ); - } - } - - // Toggle whole class name - } else if ( value === undefined || type === "boolean" ) { - className = getClass( this ); - if ( className ) { - - // Store className if set - dataPriv.set( this, "__className__", className ); - } - - // If the element has a class name or if we're passed `false`, - // then remove the whole classname (if there was one, the above saved it). - // Otherwise bring back whatever was previously saved (if anything), - // falling back to the empty string if nothing was stored. - if ( this.setAttribute ) { - this.setAttribute( "class", - className || value === false ? - "" : - dataPriv.get( this, "__className__" ) || "" - ); - } - } - } ); - }, - - hasClass: function( selector ) { - var className, elem, - i = 0; - - className = " " + selector + " "; - while ( ( elem = this[ i++ ] ) ) { - if ( elem.nodeType === 1 && - ( " " + stripAndCollapse( getClass( elem ) ) + " " ).indexOf( className ) > -1 ) { - return true; - } - } - - return false; - } -} ); - - - - -var rreturn = /\r/g; - -jQuery.fn.extend( { - val: function( value ) { - var hooks, ret, valueIsFunction, - elem = this[ 0 ]; - - if ( !arguments.length ) { - if ( elem ) { - hooks = jQuery.valHooks[ elem.type ] || - jQuery.valHooks[ elem.nodeName.toLowerCase() ]; - - if ( hooks && - "get" in hooks && - ( ret = hooks.get( elem, "value" ) ) !== undefined - ) { - return ret; - } - - ret = elem.value; - - // Handle most common string cases - if ( typeof ret === "string" ) { - return ret.replace( rreturn, "" ); - } - - // Handle cases where value is null/undef or number - return ret == null ? "" : ret; - } - - return; - } - - valueIsFunction = isFunction( value ); - - return this.each( function( i ) { - var val; - - if ( this.nodeType !== 1 ) { - return; - } - - if ( valueIsFunction ) { - val = value.call( this, i, jQuery( this ).val() ); - } else { - val = value; - } - - // Treat null/undefined as ""; convert numbers to string - if ( val == null ) { - val = ""; - - } else if ( typeof val === "number" ) { - val += ""; - - } else if ( Array.isArray( val ) ) { - val = jQuery.map( val, function( value ) { - return value == null ? "" : value + ""; - } ); - } - - hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; - - // If set returns undefined, fall back to normal setting - if ( !hooks || !( "set" in hooks ) || hooks.set( this, val, "value" ) === undefined ) { - this.value = val; - } - } ); - } -} ); - -jQuery.extend( { - valHooks: { - option: { - get: function( elem ) { - - var val = jQuery.find.attr( elem, "value" ); - return val != null ? - val : - - // Support: IE <=10 - 11 only - // option.text throws exceptions (#14686, #14858) - // Strip and collapse whitespace - // https://html.spec.whatwg.org/#strip-and-collapse-whitespace - stripAndCollapse( jQuery.text( elem ) ); - } - }, - select: { - get: function( elem ) { - var value, option, i, - options = elem.options, - index = elem.selectedIndex, - one = elem.type === "select-one", - values = one ? null : [], - max = one ? index + 1 : options.length; - - if ( index < 0 ) { - i = max; - - } else { - i = one ? index : 0; - } - - // Loop through all the selected options - for ( ; i < max; i++ ) { - option = options[ i ]; - - // Support: IE <=9 only - // IE8-9 doesn't update selected after form reset (#2551) - if ( ( option.selected || i === index ) && - - // Don't return options that are disabled or in a disabled optgroup - !option.disabled && - ( !option.parentNode.disabled || - !nodeName( option.parentNode, "optgroup" ) ) ) { - - // Get the specific value for the option - value = jQuery( option ).val(); - - // We don't need an array for one selects - if ( one ) { - return value; - } - - // Multi-Selects return an array - values.push( value ); - } - } - - return values; - }, - - set: function( elem, value ) { - var optionSet, option, - options = elem.options, - values = jQuery.makeArray( value ), - i = options.length; - - while ( i-- ) { - option = options[ i ]; - - /* eslint-disable no-cond-assign */ - - if ( option.selected = - jQuery.inArray( jQuery.valHooks.option.get( option ), values ) > -1 - ) { - optionSet = true; - } - - /* eslint-enable no-cond-assign */ - } - - // Force browsers to behave consistently when non-matching value is set - if ( !optionSet ) { - elem.selectedIndex = -1; - } - return values; - } - } - } -} ); - -// Radios and checkboxes getter/setter -jQuery.each( [ "radio", "checkbox" ], function() { - jQuery.valHooks[ this ] = { - set: function( elem, value ) { - if ( Array.isArray( value ) ) { - return ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 ); - } - } - }; - if ( !support.checkOn ) { - jQuery.valHooks[ this ].get = function( elem ) { - return elem.getAttribute( "value" ) === null ? "on" : elem.value; - }; - } -} ); - - - - -// Return jQuery for attributes-only inclusion - - -support.focusin = "onfocusin" in window; - - -var rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, - stopPropagationCallback = function( e ) { - e.stopPropagation(); - }; - -jQuery.extend( jQuery.event, { - - trigger: function( event, data, elem, onlyHandlers ) { - - var i, cur, tmp, bubbleType, ontype, handle, special, lastElement, - eventPath = [ elem || document ], - type = hasOwn.call( event, "type" ) ? event.type : event, - namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split( "." ) : []; - - cur = lastElement = tmp = elem = elem || document; - - // Don't do events on text and comment nodes - if ( elem.nodeType === 3 || elem.nodeType === 8 ) { - return; - } - - // focus/blur morphs to focusin/out; ensure we're not firing them right now - if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { - return; - } - - if ( type.indexOf( "." ) > -1 ) { - - // Namespaced trigger; create a regexp to match event type in handle() - namespaces = type.split( "." ); - type = namespaces.shift(); - namespaces.sort(); - } - ontype = type.indexOf( ":" ) < 0 && "on" + type; - - // Caller can pass in a jQuery.Event object, Object, or just an event type string - event = event[ jQuery.expando ] ? - event : - new jQuery.Event( type, typeof event === "object" && event ); - - // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) - event.isTrigger = onlyHandlers ? 2 : 3; - event.namespace = namespaces.join( "." ); - event.rnamespace = event.namespace ? - new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ) : - null; - - // Clean up the event in case it is being reused - event.result = undefined; - if ( !event.target ) { - event.target = elem; - } - - // Clone any incoming data and prepend the event, creating the handler arg list - data = data == null ? - [ event ] : - jQuery.makeArray( data, [ event ] ); - - // Allow special events to draw outside the lines - special = jQuery.event.special[ type ] || {}; - if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { - return; - } - - // Determine event propagation path in advance, per W3C events spec (#9951) - // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) - if ( !onlyHandlers && !special.noBubble && !isWindow( elem ) ) { - - bubbleType = special.delegateType || type; - if ( !rfocusMorph.test( bubbleType + type ) ) { - cur = cur.parentNode; - } - for ( ; cur; cur = cur.parentNode ) { - eventPath.push( cur ); - tmp = cur; - } - - // Only add window if we got to document (e.g., not plain obj or detached DOM) - if ( tmp === ( elem.ownerDocument || document ) ) { - eventPath.push( tmp.defaultView || tmp.parentWindow || window ); - } - } - - // Fire handlers on the event path - i = 0; - while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) { - lastElement = cur; - event.type = i > 1 ? - bubbleType : - special.bindType || type; - - // jQuery handler - handle = ( dataPriv.get( cur, "events" ) || Object.create( null ) )[ event.type ] && - dataPriv.get( cur, "handle" ); - if ( handle ) { - handle.apply( cur, data ); - } - - // Native handler - handle = ontype && cur[ ontype ]; - if ( handle && handle.apply && acceptData( cur ) ) { - event.result = handle.apply( cur, data ); - if ( event.result === false ) { - event.preventDefault(); - } - } - } - event.type = type; - - // If nobody prevented the default action, do it now - if ( !onlyHandlers && !event.isDefaultPrevented() ) { - - if ( ( !special._default || - special._default.apply( eventPath.pop(), data ) === false ) && - acceptData( elem ) ) { - - // Call a native DOM method on the target with the same name as the event. - // Don't do default actions on window, that's where global variables be (#6170) - if ( ontype && isFunction( elem[ type ] ) && !isWindow( elem ) ) { - - // Don't re-trigger an onFOO event when we call its FOO() method - tmp = elem[ ontype ]; - - if ( tmp ) { - elem[ ontype ] = null; - } - - // Prevent re-triggering of the same event, since we already bubbled it above - jQuery.event.triggered = type; - - if ( event.isPropagationStopped() ) { - lastElement.addEventListener( type, stopPropagationCallback ); - } - - elem[ type ](); - - if ( event.isPropagationStopped() ) { - lastElement.removeEventListener( type, stopPropagationCallback ); - } - - jQuery.event.triggered = undefined; - - if ( tmp ) { - elem[ ontype ] = tmp; - } - } - } - } - - return event.result; - }, - - // Piggyback on a donor event to simulate a different one - // Used only for `focus(in | out)` events - simulate: function( type, elem, event ) { - var e = jQuery.extend( - new jQuery.Event(), - event, - { - type: type, - isSimulated: true - } - ); - - jQuery.event.trigger( e, null, elem ); - } - -} ); - -jQuery.fn.extend( { - - trigger: function( type, data ) { - return this.each( function() { - jQuery.event.trigger( type, data, this ); - } ); - }, - triggerHandler: function( type, data ) { - var elem = this[ 0 ]; - if ( elem ) { - return jQuery.event.trigger( type, data, elem, true ); - } - } -} ); - - -// Support: Firefox <=44 -// Firefox doesn't have focus(in | out) events -// Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787 -// -// Support: Chrome <=48 - 49, Safari <=9.0 - 9.1 -// focus(in | out) events fire after focus & blur events, -// which is spec violation - http://www.w3.org/TR/DOM-Level-3-Events/#events-focusevent-event-order -// Related ticket - https://bugs.chromium.org/p/chromium/issues/detail?id=449857 -if ( !support.focusin ) { - jQuery.each( { focus: "focusin", blur: "focusout" }, function( orig, fix ) { - - // Attach a single capturing handler on the document while someone wants focusin/focusout - var handler = function( event ) { - jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ) ); - }; - - jQuery.event.special[ fix ] = { - setup: function() { - - // Handle: regular nodes (via `this.ownerDocument`), window - // (via `this.document`) & document (via `this`). - var doc = this.ownerDocument || this.document || this, - attaches = dataPriv.access( doc, fix ); - - if ( !attaches ) { - doc.addEventListener( orig, handler, true ); - } - dataPriv.access( doc, fix, ( attaches || 0 ) + 1 ); - }, - teardown: function() { - var doc = this.ownerDocument || this.document || this, - attaches = dataPriv.access( doc, fix ) - 1; - - if ( !attaches ) { - doc.removeEventListener( orig, handler, true ); - dataPriv.remove( doc, fix ); - - } else { - dataPriv.access( doc, fix, attaches ); - } - } - }; - } ); -} -var location = window.location; - -var nonce = { guid: Date.now() }; - -var rquery = ( /\?/ ); - - - -// Cross-browser xml parsing -jQuery.parseXML = function( data ) { - var xml, parserErrorElem; - if ( !data || typeof data !== "string" ) { - return null; - } - - // Support: IE 9 - 11 only - // IE throws on parseFromString with invalid input. - try { - xml = ( new window.DOMParser() ).parseFromString( data, "text/xml" ); - } catch ( e ) {} - - parserErrorElem = xml && xml.getElementsByTagName( "parsererror" )[ 0 ]; - if ( !xml || parserErrorElem ) { - jQuery.error( "Invalid XML: " + ( - parserErrorElem ? - jQuery.map( parserErrorElem.childNodes, function( el ) { - return el.textContent; - } ).join( "\n" ) : - data - ) ); - } - return xml; -}; - - -var - rbracket = /\[\]$/, - rCRLF = /\r?\n/g, - rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i, - rsubmittable = /^(?:input|select|textarea|keygen)/i; - -function buildParams( prefix, obj, traditional, add ) { - var name; - - if ( Array.isArray( obj ) ) { - - // Serialize array item. - jQuery.each( obj, function( i, v ) { - if ( traditional || rbracket.test( prefix ) ) { - - // Treat each array item as a scalar. - add( prefix, v ); - - } else { - - // Item is non-scalar (array or object), encode its numeric index. - buildParams( - prefix + "[" + ( typeof v === "object" && v != null ? i : "" ) + "]", - v, - traditional, - add - ); - } - } ); - - } else if ( !traditional && toType( obj ) === "object" ) { - - // Serialize object item. - for ( name in obj ) { - buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add ); - } - - } else { - - // Serialize scalar item. - add( prefix, obj ); - } -} - -// Serialize an array of form elements or a set of -// key/values into a query string -jQuery.param = function( a, traditional ) { - var prefix, - s = [], - add = function( key, valueOrFunction ) { - - // If value is a function, invoke it and use its return value - var value = isFunction( valueOrFunction ) ? - valueOrFunction() : - valueOrFunction; - - s[ s.length ] = encodeURIComponent( key ) + "=" + - encodeURIComponent( value == null ? "" : value ); - }; - - if ( a == null ) { - return ""; - } - - // If an array was passed in, assume that it is an array of form elements. - if ( Array.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { - - // Serialize the form elements - jQuery.each( a, function() { - add( this.name, this.value ); - } ); - - } else { - - // If traditional, encode the "old" way (the way 1.3.2 or older - // did it), otherwise encode params recursively. - for ( prefix in a ) { - buildParams( prefix, a[ prefix ], traditional, add ); - } - } - - // Return the resulting serialization - return s.join( "&" ); -}; - -jQuery.fn.extend( { - serialize: function() { - return jQuery.param( this.serializeArray() ); - }, - serializeArray: function() { - return this.map( function() { - - // Can add propHook for "elements" to filter or add form elements - var elements = jQuery.prop( this, "elements" ); - return elements ? jQuery.makeArray( elements ) : this; - } ).filter( function() { - var type = this.type; - - // Use .is( ":disabled" ) so that fieldset[disabled] works - return this.name && !jQuery( this ).is( ":disabled" ) && - rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) && - ( this.checked || !rcheckableType.test( type ) ); - } ).map( function( _i, elem ) { - var val = jQuery( this ).val(); - - if ( val == null ) { - return null; - } - - if ( Array.isArray( val ) ) { - return jQuery.map( val, function( val ) { - return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; - } ); - } - - return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; - } ).get(); - } -} ); - - -var - r20 = /%20/g, - rhash = /#.*$/, - rantiCache = /([?&])_=[^&]*/, - rheaders = /^(.*?):[ \t]*([^\r\n]*)$/mg, - - // #7653, #8125, #8152: local protocol detection - rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/, - rnoContent = /^(?:GET|HEAD)$/, - rprotocol = /^\/\//, - - /* Prefilters - * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example) - * 2) These are called: - * - BEFORE asking for a transport - * - AFTER param serialization (s.data is a string if s.processData is true) - * 3) key is the dataType - * 4) the catchall symbol "*" can be used - * 5) execution will start with transport dataType and THEN continue down to "*" if needed - */ - prefilters = {}, - - /* Transports bindings - * 1) key is the dataType - * 2) the catchall symbol "*" can be used - * 3) selection will start with transport dataType and THEN go to "*" if needed - */ - transports = {}, - - // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression - allTypes = "*/".concat( "*" ), - - // Anchor tag for parsing the document origin - originAnchor = document.createElement( "a" ); - -originAnchor.href = location.href; - -// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport -function addToPrefiltersOrTransports( structure ) { - - // dataTypeExpression is optional and defaults to "*" - return function( dataTypeExpression, func ) { - - if ( typeof dataTypeExpression !== "string" ) { - func = dataTypeExpression; - dataTypeExpression = "*"; - } - - var dataType, - i = 0, - dataTypes = dataTypeExpression.toLowerCase().match( rnothtmlwhite ) || []; - - if ( isFunction( func ) ) { - - // For each dataType in the dataTypeExpression - while ( ( dataType = dataTypes[ i++ ] ) ) { - - // Prepend if requested - if ( dataType[ 0 ] === "+" ) { - dataType = dataType.slice( 1 ) || "*"; - ( structure[ dataType ] = structure[ dataType ] || [] ).unshift( func ); - - // Otherwise append - } else { - ( structure[ dataType ] = structure[ dataType ] || [] ).push( func ); - } - } - } - }; -} - -// Base inspection function for prefilters and transports -function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) { - - var inspected = {}, - seekingTransport = ( structure === transports ); - - function inspect( dataType ) { - var selected; - inspected[ dataType ] = true; - jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) { - var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR ); - if ( typeof dataTypeOrTransport === "string" && - !seekingTransport && !inspected[ dataTypeOrTransport ] ) { - - options.dataTypes.unshift( dataTypeOrTransport ); - inspect( dataTypeOrTransport ); - return false; - } else if ( seekingTransport ) { - return !( selected = dataTypeOrTransport ); - } - } ); - return selected; - } - - return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" ); -} - -// A special extend for ajax options -// that takes "flat" options (not to be deep extended) -// Fixes #9887 -function ajaxExtend( target, src ) { - var key, deep, - flatOptions = jQuery.ajaxSettings.flatOptions || {}; - - for ( key in src ) { - if ( src[ key ] !== undefined ) { - ( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ]; - } - } - if ( deep ) { - jQuery.extend( true, target, deep ); - } - - return target; -} - -/* Handles responses to an ajax request: - * - finds the right dataType (mediates between content-type and expected dataType) - * - returns the corresponding response - */ -function ajaxHandleResponses( s, jqXHR, responses ) { - - var ct, type, finalDataType, firstDataType, - contents = s.contents, - dataTypes = s.dataTypes; - - // Remove auto dataType and get content-type in the process - while ( dataTypes[ 0 ] === "*" ) { - dataTypes.shift(); - if ( ct === undefined ) { - ct = s.mimeType || jqXHR.getResponseHeader( "Content-Type" ); - } - } - - // Check if we're dealing with a known content-type - if ( ct ) { - for ( type in contents ) { - if ( contents[ type ] && contents[ type ].test( ct ) ) { - dataTypes.unshift( type ); - break; - } - } - } - - // Check to see if we have a response for the expected dataType - if ( dataTypes[ 0 ] in responses ) { - finalDataType = dataTypes[ 0 ]; - } else { - - // Try convertible dataTypes - for ( type in responses ) { - if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[ 0 ] ] ) { - finalDataType = type; - break; - } - if ( !firstDataType ) { - firstDataType = type; - } - } - - // Or just use first one - finalDataType = finalDataType || firstDataType; - } - - // If we found a dataType - // We add the dataType to the list if needed - // and return the corresponding response - if ( finalDataType ) { - if ( finalDataType !== dataTypes[ 0 ] ) { - dataTypes.unshift( finalDataType ); - } - return responses[ finalDataType ]; - } -} - -/* Chain conversions given the request and the original response - * Also sets the responseXXX fields on the jqXHR instance - */ -function ajaxConvert( s, response, jqXHR, isSuccess ) { - var conv2, current, conv, tmp, prev, - converters = {}, - - // Work with a copy of dataTypes in case we need to modify it for conversion - dataTypes = s.dataTypes.slice(); - - // Create converters map with lowercased keys - if ( dataTypes[ 1 ] ) { - for ( conv in s.converters ) { - converters[ conv.toLowerCase() ] = s.converters[ conv ]; - } - } - - current = dataTypes.shift(); - - // Convert to each sequential dataType - while ( current ) { - - if ( s.responseFields[ current ] ) { - jqXHR[ s.responseFields[ current ] ] = response; - } - - // Apply the dataFilter if provided - if ( !prev && isSuccess && s.dataFilter ) { - response = s.dataFilter( response, s.dataType ); - } - - prev = current; - current = dataTypes.shift(); - - if ( current ) { - - // There's only work to do if current dataType is non-auto - if ( current === "*" ) { - - current = prev; - - // Convert response if prev dataType is non-auto and differs from current - } else if ( prev !== "*" && prev !== current ) { - - // Seek a direct converter - conv = converters[ prev + " " + current ] || converters[ "* " + current ]; - - // If none found, seek a pair - if ( !conv ) { - for ( conv2 in converters ) { - - // If conv2 outputs current - tmp = conv2.split( " " ); - if ( tmp[ 1 ] === current ) { - - // If prev can be converted to accepted input - conv = converters[ prev + " " + tmp[ 0 ] ] || - converters[ "* " + tmp[ 0 ] ]; - if ( conv ) { - - // Condense equivalence converters - if ( conv === true ) { - conv = converters[ conv2 ]; - - // Otherwise, insert the intermediate dataType - } else if ( converters[ conv2 ] !== true ) { - current = tmp[ 0 ]; - dataTypes.unshift( tmp[ 1 ] ); - } - break; - } - } - } - } - - // Apply converter (if not an equivalence) - if ( conv !== true ) { - - // Unless errors are allowed to bubble, catch and return them - if ( conv && s.throws ) { - response = conv( response ); - } else { - try { - response = conv( response ); - } catch ( e ) { - return { - state: "parsererror", - error: conv ? e : "No conversion from " + prev + " to " + current - }; - } - } - } - } - } - } - - return { state: "success", data: response }; -} - -jQuery.extend( { - - // Counter for holding the number of active queries - active: 0, - - // Last-Modified header cache for next request - lastModified: {}, - etag: {}, - - ajaxSettings: { - url: location.href, - type: "GET", - isLocal: rlocalProtocol.test( location.protocol ), - global: true, - processData: true, - async: true, - contentType: "application/x-www-form-urlencoded; charset=UTF-8", - - /* - timeout: 0, - data: null, - dataType: null, - username: null, - password: null, - cache: null, - throws: false, - traditional: false, - headers: {}, - */ - - accepts: { - "*": allTypes, - text: "text/plain", - html: "text/html", - xml: "application/xml, text/xml", - json: "application/json, text/javascript" - }, - - contents: { - xml: /\bxml\b/, - html: /\bhtml/, - json: /\bjson\b/ - }, - - responseFields: { - xml: "responseXML", - text: "responseText", - json: "responseJSON" - }, - - // Data converters - // Keys separate source (or catchall "*") and destination types with a single space - converters: { - - // Convert anything to text - "* text": String, - - // Text to html (true = no transformation) - "text html": true, - - // Evaluate text as a json expression - "text json": JSON.parse, - - // Parse text as xml - "text xml": jQuery.parseXML - }, - - // For options that shouldn't be deep extended: - // you can add your own custom options here if - // and when you create one that shouldn't be - // deep extended (see ajaxExtend) - flatOptions: { - url: true, - context: true - } - }, - - // Creates a full fledged settings object into target - // with both ajaxSettings and settings fields. - // If target is omitted, writes into ajaxSettings. - ajaxSetup: function( target, settings ) { - return settings ? - - // Building a settings object - ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) : - - // Extending ajaxSettings - ajaxExtend( jQuery.ajaxSettings, target ); - }, - - ajaxPrefilter: addToPrefiltersOrTransports( prefilters ), - ajaxTransport: addToPrefiltersOrTransports( transports ), - - // Main method - ajax: function( url, options ) { - - // If url is an object, simulate pre-1.5 signature - if ( typeof url === "object" ) { - options = url; - url = undefined; - } - - // Force options to be an object - options = options || {}; - - var transport, - - // URL without anti-cache param - cacheURL, - - // Response headers - responseHeadersString, - responseHeaders, - - // timeout handle - timeoutTimer, - - // Url cleanup var - urlAnchor, - - // Request state (becomes false upon send and true upon completion) - completed, - - // To know if global events are to be dispatched - fireGlobals, - - // Loop variable - i, - - // uncached part of the url - uncached, - - // Create the final options object - s = jQuery.ajaxSetup( {}, options ), - - // Callbacks context - callbackContext = s.context || s, - - // Context for global events is callbackContext if it is a DOM node or jQuery collection - globalEventContext = s.context && - ( callbackContext.nodeType || callbackContext.jquery ) ? - jQuery( callbackContext ) : - jQuery.event, - - // Deferreds - deferred = jQuery.Deferred(), - completeDeferred = jQuery.Callbacks( "once memory" ), - - // Status-dependent callbacks - statusCode = s.statusCode || {}, - - // Headers (they are sent all at once) - requestHeaders = {}, - requestHeadersNames = {}, - - // Default abort message - strAbort = "canceled", - - // Fake xhr - jqXHR = { - readyState: 0, - - // Builds headers hashtable if needed - getResponseHeader: function( key ) { - var match; - if ( completed ) { - if ( !responseHeaders ) { - responseHeaders = {}; - while ( ( match = rheaders.exec( responseHeadersString ) ) ) { - responseHeaders[ match[ 1 ].toLowerCase() + " " ] = - ( responseHeaders[ match[ 1 ].toLowerCase() + " " ] || [] ) - .concat( match[ 2 ] ); - } - } - match = responseHeaders[ key.toLowerCase() + " " ]; - } - return match == null ? null : match.join( ", " ); - }, - - // Raw string - getAllResponseHeaders: function() { - return completed ? responseHeadersString : null; - }, - - // Caches the header - setRequestHeader: function( name, value ) { - if ( completed == null ) { - name = requestHeadersNames[ name.toLowerCase() ] = - requestHeadersNames[ name.toLowerCase() ] || name; - requestHeaders[ name ] = value; - } - return this; - }, - - // Overrides response content-type header - overrideMimeType: function( type ) { - if ( completed == null ) { - s.mimeType = type; - } - return this; - }, - - // Status-dependent callbacks - statusCode: function( map ) { - var code; - if ( map ) { - if ( completed ) { - - // Execute the appropriate callbacks - jqXHR.always( map[ jqXHR.status ] ); - } else { - - // Lazy-add the new callbacks in a way that preserves old ones - for ( code in map ) { - statusCode[ code ] = [ statusCode[ code ], map[ code ] ]; - } - } - } - return this; - }, - - // Cancel the request - abort: function( statusText ) { - var finalText = statusText || strAbort; - if ( transport ) { - transport.abort( finalText ); - } - done( 0, finalText ); - return this; - } - }; - - // Attach deferreds - deferred.promise( jqXHR ); - - // Add protocol if not provided (prefilters might expect it) - // Handle falsy url in the settings object (#10093: consistency with old signature) - // We also use the url parameter if available - s.url = ( ( url || s.url || location.href ) + "" ) - .replace( rprotocol, location.protocol + "//" ); - - // Alias method option to type as per ticket #12004 - s.type = options.method || options.type || s.method || s.type; - - // Extract dataTypes list - s.dataTypes = ( s.dataType || "*" ).toLowerCase().match( rnothtmlwhite ) || [ "" ]; - - // A cross-domain request is in order when the origin doesn't match the current origin. - if ( s.crossDomain == null ) { - urlAnchor = document.createElement( "a" ); - - // Support: IE <=8 - 11, Edge 12 - 15 - // IE throws exception on accessing the href property if url is malformed, - // e.g. http://example.com:80x/ - try { - urlAnchor.href = s.url; - - // Support: IE <=8 - 11 only - // Anchor's host property isn't correctly set when s.url is relative - urlAnchor.href = urlAnchor.href; - s.crossDomain = originAnchor.protocol + "//" + originAnchor.host !== - urlAnchor.protocol + "//" + urlAnchor.host; - } catch ( e ) { - - // If there is an error parsing the URL, assume it is crossDomain, - // it can be rejected by the transport if it is invalid - s.crossDomain = true; - } - } - - // Convert data if not already a string - if ( s.data && s.processData && typeof s.data !== "string" ) { - s.data = jQuery.param( s.data, s.traditional ); - } - - // Apply prefilters - inspectPrefiltersOrTransports( prefilters, s, options, jqXHR ); - - // If request was aborted inside a prefilter, stop there - if ( completed ) { - return jqXHR; - } - - // We can fire global events as of now if asked to - // Don't fire events if jQuery.event is undefined in an AMD-usage scenario (#15118) - fireGlobals = jQuery.event && s.global; - - // Watch for a new set of requests - if ( fireGlobals && jQuery.active++ === 0 ) { - jQuery.event.trigger( "ajaxStart" ); - } - - // Uppercase the type - s.type = s.type.toUpperCase(); - - // Determine if request has content - s.hasContent = !rnoContent.test( s.type ); - - // Save the URL in case we're toying with the If-Modified-Since - // and/or If-None-Match header later on - // Remove hash to simplify url manipulation - cacheURL = s.url.replace( rhash, "" ); - - // More options handling for requests with no content - if ( !s.hasContent ) { - - // Remember the hash so we can put it back - uncached = s.url.slice( cacheURL.length ); - - // If data is available and should be processed, append data to url - if ( s.data && ( s.processData || typeof s.data === "string" ) ) { - cacheURL += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data; - - // #9682: remove data so that it's not used in an eventual retry - delete s.data; - } - - // Add or update anti-cache param if needed - if ( s.cache === false ) { - cacheURL = cacheURL.replace( rantiCache, "$1" ); - uncached = ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ( nonce.guid++ ) + - uncached; - } - - // Put hash and anti-cache on the URL that will be requested (gh-1732) - s.url = cacheURL + uncached; - - // Change '%20' to '+' if this is encoded form body content (gh-2658) - } else if ( s.data && s.processData && - ( s.contentType || "" ).indexOf( "application/x-www-form-urlencoded" ) === 0 ) { - s.data = s.data.replace( r20, "+" ); - } - - // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. - if ( s.ifModified ) { - if ( jQuery.lastModified[ cacheURL ] ) { - jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] ); - } - if ( jQuery.etag[ cacheURL ] ) { - jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] ); - } - } - - // Set the correct header, if data is being sent - if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) { - jqXHR.setRequestHeader( "Content-Type", s.contentType ); - } - - // Set the Accepts header for the server, depending on the dataType - jqXHR.setRequestHeader( - "Accept", - s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[ 0 ] ] ? - s.accepts[ s.dataTypes[ 0 ] ] + - ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) : - s.accepts[ "*" ] - ); - - // Check for headers option - for ( i in s.headers ) { - jqXHR.setRequestHeader( i, s.headers[ i ] ); - } - - // Allow custom headers/mimetypes and early abort - if ( s.beforeSend && - ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || completed ) ) { - - // Abort if not done already and return - return jqXHR.abort(); - } - - // Aborting is no longer a cancellation - strAbort = "abort"; - - // Install callbacks on deferreds - completeDeferred.add( s.complete ); - jqXHR.done( s.success ); - jqXHR.fail( s.error ); - - // Get transport - transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR ); - - // If no transport, we auto-abort - if ( !transport ) { - done( -1, "No Transport" ); - } else { - jqXHR.readyState = 1; - - // Send global event - if ( fireGlobals ) { - globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] ); - } - - // If request was aborted inside ajaxSend, stop there - if ( completed ) { - return jqXHR; - } - - // Timeout - if ( s.async && s.timeout > 0 ) { - timeoutTimer = window.setTimeout( function() { - jqXHR.abort( "timeout" ); - }, s.timeout ); - } - - try { - completed = false; - transport.send( requestHeaders, done ); - } catch ( e ) { - - // Rethrow post-completion exceptions - if ( completed ) { - throw e; - } - - // Propagate others as results - done( -1, e ); - } - } - - // Callback for when everything is done - function done( status, nativeStatusText, responses, headers ) { - var isSuccess, success, error, response, modified, - statusText = nativeStatusText; - - // Ignore repeat invocations - if ( completed ) { - return; - } - - completed = true; - - // Clear timeout if it exists - if ( timeoutTimer ) { - window.clearTimeout( timeoutTimer ); - } - - // Dereference transport for early garbage collection - // (no matter how long the jqXHR object will be used) - transport = undefined; - - // Cache response headers - responseHeadersString = headers || ""; - - // Set readyState - jqXHR.readyState = status > 0 ? 4 : 0; - - // Determine if successful - isSuccess = status >= 200 && status < 300 || status === 304; - - // Get response data - if ( responses ) { - response = ajaxHandleResponses( s, jqXHR, responses ); - } - - // Use a noop converter for missing script but not if jsonp - if ( !isSuccess && - jQuery.inArray( "script", s.dataTypes ) > -1 && - jQuery.inArray( "json", s.dataTypes ) < 0 ) { - s.converters[ "text script" ] = function() {}; - } - - // Convert no matter what (that way responseXXX fields are always set) - response = ajaxConvert( s, response, jqXHR, isSuccess ); - - // If successful, handle type chaining - if ( isSuccess ) { - - // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. - if ( s.ifModified ) { - modified = jqXHR.getResponseHeader( "Last-Modified" ); - if ( modified ) { - jQuery.lastModified[ cacheURL ] = modified; - } - modified = jqXHR.getResponseHeader( "etag" ); - if ( modified ) { - jQuery.etag[ cacheURL ] = modified; - } - } - - // if no content - if ( status === 204 || s.type === "HEAD" ) { - statusText = "nocontent"; - - // if not modified - } else if ( status === 304 ) { - statusText = "notmodified"; - - // If we have data, let's convert it - } else { - statusText = response.state; - success = response.data; - error = response.error; - isSuccess = !error; - } - } else { - - // Extract error from statusText and normalize for non-aborts - error = statusText; - if ( status || !statusText ) { - statusText = "error"; - if ( status < 0 ) { - status = 0; - } - } - } - - // Set data for the fake xhr object - jqXHR.status = status; - jqXHR.statusText = ( nativeStatusText || statusText ) + ""; - - // Success/Error - if ( isSuccess ) { - deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] ); - } else { - deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] ); - } - - // Status-dependent callbacks - jqXHR.statusCode( statusCode ); - statusCode = undefined; - - if ( fireGlobals ) { - globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError", - [ jqXHR, s, isSuccess ? success : error ] ); - } - - // Complete - completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] ); - - if ( fireGlobals ) { - globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] ); - - // Handle the global AJAX counter - if ( !( --jQuery.active ) ) { - jQuery.event.trigger( "ajaxStop" ); - } - } - } - - return jqXHR; - }, - - getJSON: function( url, data, callback ) { - return jQuery.get( url, data, callback, "json" ); - }, - - getScript: function( url, callback ) { - return jQuery.get( url, undefined, callback, "script" ); - } -} ); - -jQuery.each( [ "get", "post" ], function( _i, method ) { - jQuery[ method ] = function( url, data, callback, type ) { - - // Shift arguments if data argument was omitted - if ( isFunction( data ) ) { - type = type || callback; - callback = data; - data = undefined; - } - - // The url can be an options object (which then must have .url) - return jQuery.ajax( jQuery.extend( { - url: url, - type: method, - dataType: type, - data: data, - success: callback - }, jQuery.isPlainObject( url ) && url ) ); - }; -} ); - -jQuery.ajaxPrefilter( function( s ) { - var i; - for ( i in s.headers ) { - if ( i.toLowerCase() === "content-type" ) { - s.contentType = s.headers[ i ] || ""; - } - } -} ); - - -jQuery._evalUrl = function( url, options, doc ) { - return jQuery.ajax( { - url: url, - - // Make this explicit, since user can override this through ajaxSetup (#11264) - type: "GET", - dataType: "script", - cache: true, - async: false, - global: false, - - // Only evaluate the response if it is successful (gh-4126) - // dataFilter is not invoked for failure responses, so using it instead - // of the default converter is kludgy but it works. - converters: { - "text script": function() {} - }, - dataFilter: function( response ) { - jQuery.globalEval( response, options, doc ); - } - } ); -}; - - -jQuery.fn.extend( { - wrapAll: function( html ) { - var wrap; - - if ( this[ 0 ] ) { - if ( isFunction( html ) ) { - html = html.call( this[ 0 ] ); - } - - // The elements to wrap the target around - wrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true ); - - if ( this[ 0 ].parentNode ) { - wrap.insertBefore( this[ 0 ] ); - } - - wrap.map( function() { - var elem = this; - - while ( elem.firstElementChild ) { - elem = elem.firstElementChild; - } - - return elem; - } ).append( this ); - } - - return this; - }, - - wrapInner: function( html ) { - if ( isFunction( html ) ) { - return this.each( function( i ) { - jQuery( this ).wrapInner( html.call( this, i ) ); - } ); - } - - return this.each( function() { - var self = jQuery( this ), - contents = self.contents(); - - if ( contents.length ) { - contents.wrapAll( html ); - - } else { - self.append( html ); - } - } ); - }, - - wrap: function( html ) { - var htmlIsFunction = isFunction( html ); - - return this.each( function( i ) { - jQuery( this ).wrapAll( htmlIsFunction ? html.call( this, i ) : html ); - } ); - }, - - unwrap: function( selector ) { - this.parent( selector ).not( "body" ).each( function() { - jQuery( this ).replaceWith( this.childNodes ); - } ); - return this; - } -} ); - - -jQuery.expr.pseudos.hidden = function( elem ) { - return !jQuery.expr.pseudos.visible( elem ); -}; -jQuery.expr.pseudos.visible = function( elem ) { - return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); -}; - - - - -jQuery.ajaxSettings.xhr = function() { - try { - return new window.XMLHttpRequest(); - } catch ( e ) {} -}; - -var xhrSuccessStatus = { - - // File protocol always yields status code 0, assume 200 - 0: 200, - - // Support: IE <=9 only - // #1450: sometimes IE returns 1223 when it should be 204 - 1223: 204 - }, - xhrSupported = jQuery.ajaxSettings.xhr(); - -support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported ); -support.ajax = xhrSupported = !!xhrSupported; - -jQuery.ajaxTransport( function( options ) { - var callback, errorCallback; - - // Cross domain only allowed if supported through XMLHttpRequest - if ( support.cors || xhrSupported && !options.crossDomain ) { - return { - send: function( headers, complete ) { - var i, - xhr = options.xhr(); - - xhr.open( - options.type, - options.url, - options.async, - options.username, - options.password - ); - - // Apply custom fields if provided - if ( options.xhrFields ) { - for ( i in options.xhrFields ) { - xhr[ i ] = options.xhrFields[ i ]; - } - } - - // Override mime type if needed - if ( options.mimeType && xhr.overrideMimeType ) { - xhr.overrideMimeType( options.mimeType ); - } - - // X-Requested-With header - // For cross-domain requests, seeing as conditions for a preflight are - // akin to a jigsaw puzzle, we simply never set it to be sure. - // (it can always be set on a per-request basis or even using ajaxSetup) - // For same-domain requests, won't change header if already provided. - if ( !options.crossDomain && !headers[ "X-Requested-With" ] ) { - headers[ "X-Requested-With" ] = "XMLHttpRequest"; - } - - // Set headers - for ( i in headers ) { - xhr.setRequestHeader( i, headers[ i ] ); - } - - // Callback - callback = function( type ) { - return function() { - if ( callback ) { - callback = errorCallback = xhr.onload = - xhr.onerror = xhr.onabort = xhr.ontimeout = - xhr.onreadystatechange = null; - - if ( type === "abort" ) { - xhr.abort(); - } else if ( type === "error" ) { - - // Support: IE <=9 only - // On a manual native abort, IE9 throws - // errors on any property access that is not readyState - if ( typeof xhr.status !== "number" ) { - complete( 0, "error" ); - } else { - complete( - - // File: protocol always yields status 0; see #8605, #14207 - xhr.status, - xhr.statusText - ); - } - } else { - complete( - xhrSuccessStatus[ xhr.status ] || xhr.status, - xhr.statusText, - - // Support: IE <=9 only - // IE9 has no XHR2 but throws on binary (trac-11426) - // For XHR2 non-text, let the caller handle it (gh-2498) - ( xhr.responseType || "text" ) !== "text" || - typeof xhr.responseText !== "string" ? - { binary: xhr.response } : - { text: xhr.responseText }, - xhr.getAllResponseHeaders() - ); - } - } - }; - }; - - // Listen to events - xhr.onload = callback(); - errorCallback = xhr.onerror = xhr.ontimeout = callback( "error" ); - - // Support: IE 9 only - // Use onreadystatechange to replace onabort - // to handle uncaught aborts - if ( xhr.onabort !== undefined ) { - xhr.onabort = errorCallback; - } else { - xhr.onreadystatechange = function() { - - // Check readyState before timeout as it changes - if ( xhr.readyState === 4 ) { - - // Allow onerror to be called first, - // but that will not handle a native abort - // Also, save errorCallback to a variable - // as xhr.onerror cannot be accessed - window.setTimeout( function() { - if ( callback ) { - errorCallback(); - } - } ); - } - }; - } - - // Create the abort callback - callback = callback( "abort" ); - - try { - - // Do send the request (this may raise an exception) - xhr.send( options.hasContent && options.data || null ); - } catch ( e ) { - - // #14683: Only rethrow if this hasn't been notified as an error yet - if ( callback ) { - throw e; - } - } - }, - - abort: function() { - if ( callback ) { - callback(); - } - } - }; - } -} ); - - - - -// Prevent auto-execution of scripts when no explicit dataType was provided (See gh-2432) -jQuery.ajaxPrefilter( function( s ) { - if ( s.crossDomain ) { - s.contents.script = false; - } -} ); - -// Install script dataType -jQuery.ajaxSetup( { - accepts: { - script: "text/javascript, application/javascript, " + - "application/ecmascript, application/x-ecmascript" - }, - contents: { - script: /\b(?:java|ecma)script\b/ - }, - converters: { - "text script": function( text ) { - jQuery.globalEval( text ); - return text; - } - } -} ); - -// Handle cache's special case and crossDomain -jQuery.ajaxPrefilter( "script", function( s ) { - if ( s.cache === undefined ) { - s.cache = false; - } - if ( s.crossDomain ) { - s.type = "GET"; - } -} ); - -// Bind script tag hack transport -jQuery.ajaxTransport( "script", function( s ) { - - // This transport only deals with cross domain or forced-by-attrs requests - if ( s.crossDomain || s.scriptAttrs ) { - var script, callback; - return { - send: function( _, complete ) { - script = jQuery( " - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

Abstract

- ---- - - - - - - - - - - - - - - - - - - - - - - - - - - -

TaskManager(task, data[, pipeline, ...])

Base class for all Task Managers.

Model()

Base class for all models.

Pipeline(task, **kwargs)

Base class for all pipelines.

PipelineElement()

Base class for all pipeline elements.

Learner(task, **kwargs)

Subclass of PipelineElement.

Processor()

Subclass of PipelineElement.

ONNXConvertible()

Base class for all models/pipeline_elements/pipelines that can be converted to onnx.

OptunaMixin()

Abstract mixin that should be used in order to indicate the compatibility of the model with OptunaLearner.

-
-
-
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/docs_build/abstract/learner.html b/docs/docs_build/abstract/learner.html deleted file mode 100644 index caf86d7..0000000 --- a/docs/docs_build/abstract/learner.html +++ /dev/null @@ -1,251 +0,0 @@ - - - - - - - Learner — Falcon documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

Learner

-
-
-class falcon.abstract.Learner(task: str, **kwargs: Any)
-

Subclass of PipelineElement. -Learners are task aware pipeline elements that act as wrappers around models and responsible for tuning of the hyperparameters.

-
-
-__init__(task: str, **kwargs: Any) None
-
-
Parameters
-

task (str) – current ML task

-
-
-
- -
-
-abstract fit(X: ndarray[Any, dtype[ScalarType]], y: ndarray[Any, dtype[ScalarType]], *args: Any, **kwargs: Any) Any
-
-
Parameters
-
    -
  • X (npt.NDArray) – features

  • -
  • y (npt.NDArray) – targets

  • -
-
-
Returns
-

usually None

-
-
Return type
-

Any

-
-
-
- -
-
-fit_pipe(X: Any, y: Any, *args: Any, **kwargs: Any) Any
-

Equivalent of fit method that is used for elements chaining inisde pipeline during training.

-
-
Parameters
-
    -
  • X (Any) – features

  • -
  • y (Any) – targets

  • -
-
-
Returns
-

usually None

-
-
Return type
-

Any

-
-
-
- -
-
-forward(X: Any, *args: Any, **kwargs: Any) Any
-

Equivalent of predict method that is used for elements chaining inside pipeline during inference.

-
-
Parameters
-

X (Any) – featrues

-
-
Returns
-

predictions

-
-
Return type
-

Any

-
-
-
- -
-
-abstract get_input_type() Type
-
-
Returns
-

Input types

-
-
Return type
-

Type

-
-
-
- -
-
-abstract get_output_type() Type
-
-
Returns
-

Output types

-
-
Return type
-

Type

-
-
-
- -
-
-abstract predict(X: ndarray[Any, dtype[ScalarType]], *args: Any, **kwargs: Any) ndarray[Any, dtype[ScalarType]]
-
-
Parameters
-

X (npt.NDArray) – features

-
-
Returns
-

predictions

-
-
Return type
-

npt.NDArray

-
-
-
- -
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/docs_build/abstract/model.html b/docs/docs_build/abstract/model.html deleted file mode 100644 index da79dbd..0000000 --- a/docs/docs_build/abstract/model.html +++ /dev/null @@ -1,177 +0,0 @@ - - - - - - - Model — Falcon documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

Model

-
-
-class falcon.abstract.Model
-

Base class for all models.

-
-
-abstract fit(X: ndarray[Any, dtype[ScalarType]], y: ndarray[Any, dtype[ScalarType]], *args: Any, **kwargs: Any) Any
-
-
Parameters
-
    -
  • X (npt.NDArray) – features

  • -
  • y (npt.NDArray) – targets

  • -
-
-
Returns
-

usually None

-
-
Return type
-

Any

-
-
-
- -
-
-abstract predict(X: ndarray[Any, dtype[ScalarType]], *args: Any, **kwargs: Any) ndarray[Any, dtype[ScalarType]]
-
-
Parameters
-

X (npt.NDArray) – features

-
-
Returns
-

predictions

-
-
Return type
-

npt.NDArray

-
-
-
- -
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/docs_build/abstract/onnx.html b/docs/docs_build/abstract/onnx.html deleted file mode 100644 index 44de614..0000000 --- a/docs/docs_build/abstract/onnx.html +++ /dev/null @@ -1,153 +0,0 @@ - - - - - - - ONNXConvertible — Falcon documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

ONNXConvertible

-
-
-class falcon.abstract.ONNXConvertible
-

Base class for all models/pipeline_elements/pipelines that can be converted to onnx.

-
-
-abstract to_onnx() SerializedModelRepr
-

Converted model

-
-
Return type
-

SerializedModelRepr

-
-
-
- -
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/docs_build/abstract/optuna.html b/docs/docs_build/abstract/optuna.html deleted file mode 100644 index d93e1d2..0000000 --- a/docs/docs_build/abstract/optuna.html +++ /dev/null @@ -1,163 +0,0 @@ - - - - - - - OptunaMixin — Falcon documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

OptunaMixin

-
-
-class falcon.abstract.OptunaMixin
-

Abstract mixin that should be used in order to indicate the compatibility of the model with OptunaLearner.

-
-
-abstract classmethod get_search_space(X: Any, y: Any) Union[Callable, Dict]
-

A class method that provides an optuna search space for the model. -Optionally, the search space can be adjusted based on the provided training data.

-
-
Parameters
-
    -
  • X (Any) – features

  • -
  • y (Any) – targets

  • -
-
-
Returns
-

dictionary that describes the search space, or custom objective function

-
-
Return type
-

Union[Callable, Dict]

-
-
-
- -
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/docs_build/abstract/pipeline.html b/docs/docs_build/abstract/pipeline.html deleted file mode 100644 index 1fc221b..0000000 --- a/docs/docs_build/abstract/pipeline.html +++ /dev/null @@ -1,207 +0,0 @@ - - - - - - - Pipeline — Falcon documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

Pipeline

-
-
-class falcon.abstract.Pipeline(task: str, **kwargs: Any)
-

Base class for all pipelines.

-
-
-__init__(task: str, **kwargs: Any) None
-
- -
-
-add_element(element: PipelineElement) None
-

Adds element to pipeline. The input type of added element should match the output type of the last element in the pipeline.

-
-
Parameters
-

element (PipelineElement) – element to be added to the end of the pipeline

-
-
-
- -
-
-abstract fit(X: ndarray[Any, dtype[ScalarType]], y: ndarray[Any, dtype[ScalarType]], *args: Any, **kwargs: Any) Any
-
-
Parameters
-
    -
  • X (npt.NDArray) – features

  • -
  • y (npt.NDArray) – targets

  • -
-
-
Returns
-

usually None

-
-
Return type
-

Any

-
-
-
- -
-
-abstract predict(X: ndarray[Any, dtype[ScalarType]], *args: Any, **kwargs: Any) ndarray[Any, dtype[ScalarType]]
-
-
Parameters
-

X (npt.NDArray) – features

-
-
Returns
-

predictions

-
-
Return type
-

npt.NDArray

-
-
-
- -
-
-save() ModelProto
-

Exports the pipeline to ONNX ModelProto

-
-
Returns
-

Pipeline as ONNX ModelProto

-
-
Return type
-

ModelProto

-
-
-
- -
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/docs_build/abstract/pipeline_element.html b/docs/docs_build/abstract/pipeline_element.html deleted file mode 100644 index 23b0f1d..0000000 --- a/docs/docs_build/abstract/pipeline_element.html +++ /dev/null @@ -1,240 +0,0 @@ - - - - - - - PipelineElement — Falcon documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

PipelineElement

-
-
-class falcon.abstract.PipelineElement
-

Base class for all pipeline elements.

-
-
-abstract fit(X: ndarray[Any, dtype[ScalarType]], y: ndarray[Any, dtype[ScalarType]], *args: Any, **kwargs: Any) Any
-
-
Parameters
-
    -
  • X (npt.NDArray) – features

  • -
  • y (npt.NDArray) – targets

  • -
-
-
Returns
-

usually None

-
-
Return type
-

Any

-
-
-
- -
-
-fit_pipe(X: Any, y: Any, *args: Any, **kwargs: Any) Any
-

Equivalent of fit method that is used for elements chaining inisde pipeline during training.

-
-
Parameters
-
    -
  • X (Any) – features

  • -
  • y (Any) – targets

  • -
-
-
Returns
-

usually None

-
-
Return type
-

Any

-
-
-
- -
-
-forward(X: Any, *args: Any, **kwargs: Any) Any
-

Equivalent of predict method that is used for elements chaining inside pipeline during inference.

-
-
Parameters
-

X (Any) – featrues

-
-
Returns
-

predictions

-
-
Return type
-

Any

-
-
-
- -
-
-abstract get_input_type() Type
-
-
Returns
-

Input types

-
-
Return type
-

Type

-
-
-
- -
-
-abstract get_output_type() Type
-
-
Returns
-

Output types

-
-
Return type
-

Type

-
-
-
- -
-
-abstract predict(X: ndarray[Any, dtype[ScalarType]], *args: Any, **kwargs: Any) ndarray[Any, dtype[ScalarType]]
-
-
Parameters
-

X (npt.NDArray) – features

-
-
Returns
-

predictions

-
-
Return type
-

npt.NDArray

-
-
-
- -
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/docs_build/abstract/processor.html b/docs/docs_build/abstract/processor.html deleted file mode 100644 index 79bfc64..0000000 --- a/docs/docs_build/abstract/processor.html +++ /dev/null @@ -1,257 +0,0 @@ - - - - - - - Processor — Falcon documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

Processor

-
-
-class falcon.abstract.Processor
-

Subclass of PipelineElement. Used for data pre and post processing (e.g. data scaling).

-
-
-abstract fit(X: ndarray[Any, dtype[ScalarType]], y: ndarray[Any, dtype[ScalarType]], *args: Any, **kwargs: Any) Any
-
-
Parameters
-
    -
  • X (npt.NDArray) – features

  • -
  • y (npt.NDArray) – targets

  • -
-
-
Returns
-

usually None

-
-
Return type
-

Any

-
-
-
- -
-
-fit_pipe(X: Any, y: Any, *args: Any, **kwargs: Any) Any
-

Equivalent of fit method that is used for elements chaining inisde pipeline during training.

-
-
Parameters
-
    -
  • X (Any) – features

  • -
  • y (Any) – targets

  • -
-
-
Returns
-

usually None

-
-
Return type
-

Any

-
-
-
- -
-
-forward(X: Any, *args: Any, **kwargs: Any) Any
-

Equivalent of predict method that is used for elements chaining inside pipeline during inference.

-
-
Parameters
-

X (Any) – featrues

-
-
Returns
-

predictions

-
-
Return type
-

Any

-
-
-
- -
-
-abstract get_input_type() Type
-
-
Returns
-

Input types

-
-
Return type
-

Type

-
-
-
- -
-
-abstract get_output_type() Type
-
-
Returns
-

Output types

-
-
Return type
-

Type

-
-
-
- -
-
-abstract predict(X: ndarray[Any, dtype[ScalarType]], *args: Any, **kwargs: Any) ndarray[Any, dtype[ScalarType]]
-
-
Parameters
-

X (npt.NDArray) – features

-
-
Returns
-

predictions

-
-
Return type
-

npt.NDArray

-
-
-
- -
-
-transform(X: ndarray[Any, dtype[ScalarType]], *args: Any, **kwargs: Any) ndarray[Any, dtype[ScalarType]]
-

Equivalent of self.predict(X)

-
-
Parameters
-

X (npt.NDArray) – features

-
-
Returns
-

transformed features

-
-
Return type
-

npt.NDArray

-
-
-
- -
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/docs_build/abstract/task_manager.html b/docs/docs_build/abstract/task_manager.html deleted file mode 100644 index 4d2790a..0000000 --- a/docs/docs_build/abstract/task_manager.html +++ /dev/null @@ -1,286 +0,0 @@ - - - - - - - TaskManager — Falcon documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

TaskManager

-
-
-class falcon.abstract.TaskManager(task: str, data: Any, pipeline: Optional[Type[Pipeline]] = None, pipeline_options: Optional[Dict] = None, extra_pipeline_options: Optional[Dict] = None, features: Optional[Any] = None, target: Optional[Any] = None)
-

Base class for all Task Managers.

-
-
-__init__(task: str, data: Any, pipeline: Optional[Type[Pipeline]] = None, pipeline_options: Optional[Dict] = None, extra_pipeline_options: Optional[Dict] = None, features: Optional[Any] = None, target: Optional[Any] = None)
-
-
Parameters
-
    -
  • task (str) – current task

  • -
  • data (Any) – data to be used for training

  • -
  • pipeline (Optional[Type[Pipeline]], optional) – pipeline class to be used, by default None

  • -
  • pipeline_options (Optional[Dict], optional) – arguments to be passed to pipeline instead of default ones, by default None

  • -
  • extra_pipeline_options (Optional[Dict], optional) – arguments to be passed to pipeline in addition to default ones, by default None

  • -
  • features (Any, optional) – featrues to be used for training, by default None

  • -
  • target (Any, optional) – targets to be used for training, by default None

  • -
-
-
-
- -
-
-_create_pipeline(pipeline: Optional[Type[Pipeline]], options: Optional[Dict]) None
-

Initializes the pipeline.

-
-
Parameters
-
    -
  • pipeline (Optional[Type[Pipeline]]) – pipeline class

  • -
  • options (Optional[Dict]) – pipeline options

  • -
-
-
-
- -
-
-abstract _prepare_data(data: Any) Any
-

Initial data preparation (e.g. reading from file). -Warning: initial data preparation (e.g. reading, cleaning) and data preprocessing (e.g. scaling, encoding) are two distinct steps. The later one is performed inside the pipeline.

-
-
Parameters
-

data (Any) – training data

-
-
Returns
-

prepared data

-
-
Return type
-

Any

-
-
-
- -
-
-abstract property default_pipeline: Type[Pipeline]
-

Default pipeline class. Can be chosen dynamically.

-
- -
-
-abstract property default_pipeline_options: Dict
-

Default pipeline options. Can be chosen dynamically.

-
- -
-
-abstract evaluate(test_data: Any) Any
-

Evaluates the performance of a trained pipeline.

-
-
Parameters
-

test_data (Any) – data to be used for evaluation

-
-
Returns
-

evaluation metric or None

-
-
Return type
-

Any

-
-
-
- -
-
-abstract performance_summary(test_data: Any) Any
-

Prints the performance summary of the trained pipeline.

-
-
Parameters
-

test_data (Any) – test set, optional

-
-
Returns
-

relevant metrics or None

-
-
Return type
-

Any

-
-
-
- -
-
-predict(X: Any) Any
-

Calls predict methods of the pipeline.

-
-
Parameters
-

X (Any) – features

-
-
Returns
-

predictions

-
-
Return type
-

Any

-
-
-
- -
-
-save_model(filename: Optional[str] = None, **kwargs: Any) ModelProto
-

Serializes and saves the model.

-
-
Parameters
-

filename (Optional[str], optional) – filename for the model file, by default None. If filename is not specified, the model is not saved on disk and only returned as bytes object

-
-
Returns
-

ONNX ModelProto of the model

-
-
Return type
-

ModelProto

-
-
-
- -
-
-abstract train(**kwargs: Any) TaskManager
-

Trains the underlying pipeline.

-
-
Returns
-

self

-
-
Return type
-

TaskManager

-
-
-
- -
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/docs_build/api.html b/docs/docs_build/api.html deleted file mode 100644 index 1c5c9b5..0000000 --- a/docs/docs_build/api.html +++ /dev/null @@ -1,172 +0,0 @@ - - - - - - - API reference — Falcon documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- - -
-
- - - - \ No newline at end of file diff --git a/docs/docs_build/available_configurations.html b/docs/docs_build/available_configurations.html deleted file mode 100644 index 1d647c0..0000000 --- a/docs/docs_build/available_configurations.html +++ /dev/null @@ -1,242 +0,0 @@ - - - - - - - Available Configurations — Falcon documentation - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

Available Configurations

-

The tables below list both main and additional configurations that can be used. -Additional configurations should be used with caution as they may not be suitable for certain datasets. It is reccomended to always choose one of the main configurations.

-
-

Configurations for tabular_regression/tabular_classification tasks

- ----- - - - - - - - - - - - - - - - - - - - - -

Name

Extension

Description

SuperLearner

-
Uses SuperLearner to build a stacking ensemble of base estimators.
-
SuperLearner combines multiple individual estimators to make predictions with greater accuracy than any of the individual estimators alone.
-
Additionaly, it learns to weigh the predictions of each individual model, optimizing the combination to maximize performance on the given task.
-
SuperLearner is more suitable for smaller datasets, but the produced models tend to be relatively large.
-
-

OptunaLearner

- -
It builds a model and optimizes its hyperparameters using Optuna framework; HistGradientBoostingClassifier/HistGradientBoostingRegressor is used as a default model.
-
Since OptunaLearner focuses on finetuning a single model, the produced model is not very large in size, but the optimization procedure can be very long.
-
-

PlainLearner

- -
It builds a model using default hyperparameters; HistGradientBoostingClassifier/HistGradientBoostingRegressor is used as a default model.
-
PlainLearner is very fast, thus it is a good choice for building initial baselines or automizing preprocessing steps.
-
-
-
- -Additional configurations
-
-
-
-
- ----- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Name

Extension

Description

SuperLearner.mini

-
Uses SuperLearner with a config for small datasets.
-
The dataset is considered small when the number of cells after preprocessing [n_rows*n_columns] is < 80k.
-
-

SuperLearner.mid

-
Uses SuperLearner with a config for mid datasets.
-
The dataset is considered small when the number of cells after preprocessing [n_rows*n_columns] is < 4kk.
-
-

SuperLearner.large

-
Uses SuperLearner with a config for large datasets.
-
The dataset is considered small when the number of cells after preprocessing [n_rows*n_columns] is < 16kk.
-
-

SuperLearner.xlarge

-
Uses SuperLearner with a config for x-large datasets.
-
The dataset is considered small when the number of cells after preprocessing [n_rows*n_columns] is >= 16kk.
-
-

OptunaLearner.hgbt

- -
It builds a HistGradientBoostingClassifier/HistGradientBoostingRegressor model with hyperparameters optimized by Optuna framework.
-
-

PlainLearner.hgbt

- -
It builds a HistGradientBoostingClassifier/HistGradientBoostingRegressor model with default hyperparameters.
-
-
-
-
-
- - -
-
-
- -
- -
-

© Copyright 2022, Oleg Kostromin, Marco Pasini, Iryna Kondrashchenko.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - \ No newline at end of file diff --git a/docs/docs_build/genindex.html b/docs/docs_build/genindex.html deleted file mode 100644 index e964557..0000000 --- a/docs/docs_build/genindex.html +++ /dev/null @@ -1,628 +0,0 @@ - - - - - - Index — Falcon documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - -
  • -
  • -
-
-
-
-
- - -

Index

- -
- _ - | A - | D - | E - | F - | G - | H - | I - | L - | M - | O - | P - | R - | S - | T - -
-

_

- - - -
- -

A

- - - -
- -

D

- - - -
- -

E

- - -
- -

F

- - - -
- -

G

- - - -
- -

H

- - - -
- -

I

- - - -
- -

L

- - - -
- -

M

- - - -
- -

O

- - - -
- -

P

- - - -
- -

R

- - - -
- -

S

- - - -
- -

T

- - - -
- - - -
-
-
- -
- -
-

© Copyright 2022, Oleg Kostromin, Marco Pasini, Iryna Kondrashchenko.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - \ No newline at end of file diff --git a/docs/docs_build/high_level_api.html b/docs/docs_build/high_level_api.html deleted file mode 100644 index 6bd81a6..0000000 --- a/docs/docs_build/high_level_api.html +++ /dev/null @@ -1,209 +0,0 @@ - - - - - - - High level API — Falcon documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

High level API

-
-
-falcon.AutoML(task: str, train_data: Any, test_data: Optional[Any] = None, features: Optional[Any] = None, target: Optional[Any] = None, manager_configuration: Optional[Union[Dict, str]] = None, config: Optional[Union[Dict, str]] = None) TaskManager
-

High level API for one line model training and evaluation.

-
-
When calling the following steps will be executed:
    -
  1. task manager object will be initialized;

  2. -
  3. the model will be trained;

  4. -
  5. performance summary table is printed (if test set is not provided, random split is done);

  6. -
  7. the model is saved as an onnx file.

  8. -
-
-
-
-
Parameters
-
    -
  • task (str) – type of the task, currently supported tasks are [tabular_classification, tabular_regression]

  • -
  • train_data (Any) – data to be used for training, for tabular classification and regression this can be: path to .csv or .parquet file, pandas dataframe, numpy array, tuple (X,y)

  • -
  • test_data (Any, optional) – data to be used for evaluation, for tabular classification and regression this can be: path to .csv or .parquet file, pandas dataframe, numpy array, tuple (X,y)

  • -
  • features (Any, optional) – features to be used for training, for tabular classification and regression this can be: list of column names or indexes, by default None

  • -
  • target (Any, optional) – target to be used for training, for tabular classification and regression this can be: column name or index, by default None

  • -
  • manager_configuration (Union[Dict, str], optional) – task manager configuration to be used (can be used to replace pipeline/learner and/or their arguments), by default None

  • -
  • config (Union[Dict, str], optional) – alias for manager_configuration argument

  • -
-
-
Returns
-

Task Manager object for the corresponding task.

-
-
Return type
-

TaskManager

-
-
-
- -
-
-falcon.initialize(task: str, data: Any, pipeline: Optional[Type[Pipeline]] = None, pipeline_options: Optional[Dict] = None, extra_pipeline_options: Optional[Dict] = None, features: Optional[Any] = None, target: Optional[Any] = None, **options: Any) TaskManager
-

Initializes and returns a task manager object for a given task.

-
-
Parameters
-
    -
  • task (str) – type of the task

  • -
  • data (Any) – data to be used for training

  • -
  • pipeline (Optional[Type[Pipeline]], optional) – class to be used as pipeline, by default None

  • -
  • pipeline_options (Optional[Dict], optional) – arguments to be passed to the pipeline, by default None. -These options will overwrite the ones from default_pipeline_options attribute

  • -
  • extra_pipeline_options (Optional[Dict], optional) – arguments to be passed to the pipeline, by default None. -These options will be passed in addition to the ones from default_pipeline_options attribute. -This argument is ignored if pipeline_options is not None

  • -
  • features (Any, optional) – features to be used for training, by default None

  • -
  • target (Any, optional) – target to be used for training, by default None

  • -
-
-
Returns
-

Initialized task manager object

-
-
Return type
-

TaskManager

-
-
-
- -
-
-falcon.run_model(model_path: str, X: ndarray[Any, dtype[ScalarType]]) Union[List[ndarray[Any, dtype[ScalarType]]], ndarray]
-

Runs input data through the saved model.

-
-
Parameters
-
    -
  • model_path (str) – model path

  • -
  • X (npt.NDArray) – model inputs

  • -
-
-
Returns
-

model predictions

-
-
Return type
-

Union[List[npt.NDArray], np.ndarray]

-
-
-
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/docs_build/index.html b/docs/docs_build/index.html deleted file mode 100644 index cd27e77..0000000 --- a/docs/docs_build/index.html +++ /dev/null @@ -1,171 +0,0 @@ - - - - - - - Welcome to Falcon’s documentation! — Falcon documentation - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
- -
- -
- -
-

© Copyright 2022, Oleg Kostromin, Marco Pasini, Iryna Kondrashchenko.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - \ No newline at end of file diff --git a/docs/docs_build/intro.html b/docs/docs_build/intro.html deleted file mode 100644 index 1d901ff..0000000 --- a/docs/docs_build/intro.html +++ /dev/null @@ -1,251 +0,0 @@ - - - - - - - Getting started — Falcon documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

Getting started

-

Train a powerful Machine Learning model in a single line of code with Falcon!

-

Falcon is a simple and lightweight AutoML library designed for people who want to train a model on a custom dataset in an instant even without specific data-science knowledge. Simply give Falcon your dataset and specify which feature you want the ML model to predict. Falcon will do the rest!

-

Falcon allows the trained models to be immediately used in production by saving them in the widely used ONNX format. No need to write custom code to save complicated models to ONNX anymore!

-
-
-

Installation

-

Stable release from PyPi

-
pip install falcon-ml
-
-
-

Latest version from GitHub

-
pip install git+https://github.com/OKUA1/falcon
-
-
-

Installing some of the dependencies on Apple Silicon Macs might not work, the workaround is to create an X86 environment using Conda

-
conda create -n falcon_env
-conda activate falcon_env
-conda config --env --set subdir osx-64
-conda install python=3.9
-pip3 install falcon-ml
-
-
-
-
-

Usage

-

Currently, Falcon supports only tabular datasets and two tasks: ‘tabular_classification’ and ‘tabular_regression’.

-

The easiest way to use the library is by using the highest level API as shown below:

-
from falcon import AutoML
-
-AutoML(task = 'tabular_classification', train_data = 'titanic.csv')
-
-
-

This single line of code will read and prepare the dataset, scale/encode the features, encode the labels, train the model and save it as ONNX file for future inference.

-

Additionally, it is also possible to explicitly specify the feature/target columns (otherwise the last column will be used as target and all other as features) and test data (otherwise 25% of training set will be kept) for evaluation report.

-
from falcon import AutoML
-
-manager = AutoML(task = 'tabular_classification', train_data = 'titanic.csv', test_data = 'titanic_test.csv', features = ['sex', 'gender', 'class', 'age'], target = 'survived')
-
-
-

It is also possible to provide train/test data as a pandas dataframe, numpy array, or tuple containing X and y. In order to do that, simply pass the required object as an argument. This might be relevant in cases when custom data preparation is needed or data itself comes from non-conventional source.

-
from falcon import AutoML
-import pandas as pd
-
-df = pd.read_csv('titanic.csv')
-X_test = pd.read_csv('X_test.csv')
-y_test = pd.read_csv('y_test.csv')
-
-manager = AutoML(task = 'tabular_classification', train_data = df, test_data = (X_test, y_test), features = ['sex', 'gender', 'class', 'age'], target = 'survived')
-
-
-

While AutoML function enables extremely fast experimentation, it does not provide enough control over the training steps and might be not flexible enough for more advanced users. As an alternative, it is possible to use the relevant TaskManager class either directly or by using initialize helper function.

-
from falcon import initialize
-import pandas as pd
-
-test_df = pd.read_csv('titanic_test.csv')
-
-manager = initialize(task='tabular_classification', data='titanic.csv')
-manager.train(make_eval_subset = True)
-manager.performance_summary(test_df)
-
-
-

When using initialize function it is also possible to provide a custom configuration or even a custom pipeline. For more details please check the API reference section.

-
-
-

Demo datasets

-

You can try out falcon using one of the built-in demo datasets.

-
from falcon import AutoML
-from falcon.datasets import load_churn_dataset, load_insurance_dataset # churn -> classification; insurance -> regression
-
-df = load_churn_dataset()
-
-AutoML(task = 'tabular_classification', train_data = df)
-
-
-
-
-

Making predictions with trained models

-

There are 2 ways to make a prediction using a trained model. If the input/unlabeled data is available right away, the same manager object that was used for training the model can be used. -An important thing to notice is that the input data should have the same structure as the training set (the same number, order and type of the features). This is assumed by the model, but not explicitly checked during runtime. -The recommended approach is to provide the data as a numpy array.

-
from falcon import AutoML
-import pandas as pd
-
-df = pd.read_csv('training_data.csv')
-manager = AutoML(task = 'tabular_classification', train_data = df)
-
-unlabeled_data = pd.read_csv('unlabeled_data.csv').to_numpy()
-predictions = manager.predict(unlabeled_data)
-print(predictions)
-
-
-

While this solution is straight-forward, in real-world applications the new/unlabeled data is not always available right away. Therefore, it is desirable to train a model and reuse it in the future.

-

One of the key features of falcon is native ONNX support. ONNX (Open Neural Network Exchange) is an open standard for representing machine learning algorithms. This means that once the model is exported to ONNX, it can be run on any platform with available ONNX implementation. -For example, Microsoft ONNX Rutime (ORT) is available for Python, C, C++, Java, JavaScript and multiple other languages which allows to run the model virtually everywhere. There are also alternative implementations, but there is a high chance they do not support all the required operators.

-

In order to simplify the interaction with ONNX Runtime, falcon provides a run_model function that takes the path to the ONNX model, the input data as a numpy array and returns the predictions.

-
from falcon import run_model
-import pandas as pd
-
-unlabeled_data = pd.read_csv('unlabeled_data.csv').to_numpy() # ONLY NUMPY ARRAYS ARE ACCEPTED AS INPUT !!!
-
-predictions = run_model(model_path = "/path/to/model.onnx", X = unlabeled_data)
-
-print(predictions)
-
-
-

Below is the complete example of model training and inference using the built-in datasets.

-
############################################ training.py ###########################################################
-from falcon import AutoML
-from falcon.datasets import load_churn_dataset
-
-df = load_churn_dataset(mode = "training")
-AutoML(task = "tabular_classification", train_data = df)
-# onnx model name will be printed after the training is done, use it instead of <FILENAME> during infernce
-
-############################################ inference.py ##########################################################
-from falcon import run_model
-from falcon.datasets import load_churn_dataset
-
-X = load_churn_dataset(mode = "inference") # for this example we are reusing training dataset but without labels
-predictions = run_model(model_path = "<FILENAME>.onnx", X = X)
-print(predictions)
-
-
-
-
-

Manually selecting a configuration

-

All of the examples in the previous sections demonstrated how to train falcon models using the default configuration. -However, there are several configurations available and it is easily possible to switch between them by providing a single additional argument.

-

For tabular classification task, by default, falcon will use a SuperLearner and the sub-configuration (e.g. list of base estimators) will be chosen automatically based on the dataset size. -But if we want to specify that a ‘mini’ sub-configuration of the learner is to be used, we can do it by adding config = ‘SuperLearner.mini’.

-
AutoML(task = "tabular_classification", train_data = df, config = 'SuperLearner.mini') # SuperLearner.mini config is used
-
-
-

Similarly, instead of SuperLearner which builds a stacking ensemble of base estimators, it is possible to use OptunaLearner which uses a single model and performs hyperparameter optimization using the Optuna framework.

-
AutoML(task = "tabular_classification", train_data = df, config = 'OptunaLearner') # OptunaLearner config is used
-
-
-

All the available configurations can be found here.

-
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/docs_build/objects.inv b/docs/docs_build/objects.inv deleted file mode 100644 index 6afa664..0000000 Binary files a/docs/docs_build/objects.inv and /dev/null differ diff --git a/docs/docs_build/registry.html b/docs/docs_build/registry.html deleted file mode 100644 index fc3a588..0000000 --- a/docs/docs_build/registry.html +++ /dev/null @@ -1,263 +0,0 @@ - - - - - - - Task Registry — Falcon documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

Task Registry

-
-
-class falcon.task_configurations.TaskConfigurationRegistry
-

Central registry holding pre-defined configurations for the tasks.

-
-
-classmethod get_configuration(task: str, configuration_name: str, allow_extensions_discovery: bool = True) Dict
-
-
Parameters
-
    -
  • task (str) – the name of the task

  • -
  • configuration_name (str) – the name of the configuration

  • -
  • allow_extensions_discovery (bool, optional) – if True falcon will try to import an extension module for a given config (config module is determined based on config name), by default True

  • -
-
-
Returns
-

task configuration

-
-
Return type
-

Dict

-
-
-
- -
-
-classmethod get_registered_config_names(task: str) List[str]
-
-
Parameters
-

task (str) – the name of the task

-
-
Returns
-

a list of registered configuration names for a given task

-
-
Return type
-

List[str]

-
-
-
- -
-
-classmethod get_registered_tasks() List[str]
-

Returns the list of registered tasks.

-
-
Returns
-

list of registered tasks

-
-
Return type
-

List[str]

-
-
-
- -
-
-classmethod get_task_manager(task: str) Type[TaskManager]
-
-
Parameters
-

task (str) – the name of the task

-
-
Returns
-

TaskNanager class for the given task

-
-
Return type
-

Type[TaskManager]

-
-
-
- -
-
-classmethod is_known_task(task: str) bool
-
-
Parameters
-

task (str) – the name of the task

-
-
Returns
-

True if the task is registered, else False

-
-
Return type
-

bool

-
-
-
- -
-
-classmethod load_extension(extension_name: str) None
-

Imports the extension module, module name should follow the naming scheme falcon_ml_<extension_name>.

-
-
Parameters
-

extension_name (str) – the name of the extension

-
-
-
- -
-
-classmethod register_configurations(task: str, config: Dict, silent: bool = False) None
-

Register configuration for the task.

-
-
Parameters
-
    -
  • task (str) – the name of the task

  • -
  • config (Dict) – the name of the configuration, should follow the naming scheme EXTENSION_NAME::config_name

  • -
  • silent (bool, optional) – prints config name on registration if True, by default False

  • -
-
-
-
- -
-
-classmethod register_task(task: str, task_manager: Type[TaskManager]) None
-

Registers a new task.

-
-
Parameters
-
    -
  • task (str) – name of the task (e.g. tabular_regression)

  • -
  • task_manager (Type[TaskManager]) – TaskManager responsible for handling the task

  • -
-
-
-
- -
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/docs_build/search.html b/docs/docs_build/search.html deleted file mode 100644 index cb4caf2..0000000 --- a/docs/docs_build/search.html +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - Search — Falcon documentation - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - -
  • -
  • -
-
-
-
-
- - - - -
- -
- -
-
-
- -
- -
-

© Copyright 2022, Oleg Kostromin, Marco Pasini, Iryna Kondrashchenko.

-
- - Built with Sphinx using a - theme - provided by Read the Docs. - - -
-
-
-
-
- - - - - - - - - \ No newline at end of file diff --git a/docs/docs_build/searchindex.js b/docs/docs_build/searchindex.js deleted file mode 100644 index f6a43ff..0000000 --- a/docs/docs_build/searchindex.js +++ /dev/null @@ -1 +0,0 @@ -Search.setIndex({"docnames": ["abstract/index", "abstract/learner", "abstract/model", "abstract/onnx", "abstract/optuna", "abstract/pipeline", "abstract/pipeline_element", "abstract/processor", "abstract/task_manager", "api", "available_configurations", "high_level_api", "index", "intro", "registry", "sklearn_api", "tabular/index", "tabular/learners/optuna_learner", "tabular/learners/plain_learner", "tabular/learners/super_learner", "tabular/models/hgbt_clf", "tabular/models/hgbt_regr", "tabular/models/stacking_clf", "tabular/models/stacking_regr", "tabular/pipelines/simple_pipeline", "tabular/processors/label_decoder", "tabular/processors/mm_encoder", "tabular/processors/scaler_and_encoder", "tabular/tab_manager"], "filenames": ["abstract/index.rst", "abstract/learner.rst", "abstract/model.rst", "abstract/onnx.rst", "abstract/optuna.rst", "abstract/pipeline.rst", "abstract/pipeline_element.rst", "abstract/processor.rst", "abstract/task_manager.rst", "api.rst", "available_configurations.rst", "high_level_api.rst", "index.rst", "intro.rst", "registry.rst", "sklearn_api.rst", "tabular/index.rst", "tabular/learners/optuna_learner.rst", "tabular/learners/plain_learner.rst", "tabular/learners/super_learner.rst", "tabular/models/hgbt_clf.rst", "tabular/models/hgbt_regr.rst", "tabular/models/stacking_clf.rst", "tabular/models/stacking_regr.rst", "tabular/pipelines/simple_pipeline.rst", "tabular/processors/label_decoder.rst", "tabular/processors/mm_encoder.rst", "tabular/processors/scaler_and_encoder.rst", "tabular/tab_manager.rst"], "titles": ["Abstract", "Learner", "Model", "ONNXConvertible", "OptunaMixin", "Pipeline", "PipelineElement", "Processor", "TaskManager", "API reference", "Available Configurations", "High level API", "Welcome to Falcon\u2019s documentation!", "Getting started", "Task Registry", "Scikit-learn API", "Tabular", "OptunaLearner", "PlainLearner", "SuperLearner", "HistGradientBoostingClassifier", "HistGradientBoostingRegressor", "StackingClassifier", "StackingRegressor", "SimpleTabularPipeline", "LabelDecoder", "MultiModalEncoder", "ScalerAndEncoder", "TabularTaskManager"], "terms": {"class": [1, 2, 3, 4, 5, 6, 7, 8, 11, 13, 14, 15, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28], "falcon": [1, 2, 3, 4, 5, 6, 7, 8, 11, 13, 14, 15, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28], "abstract": [1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 24], "task": [1, 5, 8, 9, 11, 12, 13, 15, 17, 18, 19, 24, 28], "str": [1, 5, 8, 11, 14, 15, 17, 18, 19, 22, 23, 24, 25, 28], "kwarg": [1, 2, 5, 6, 7, 8, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28], "ani": [1, 2, 4, 5, 6, 7, 8, 10, 11, 13, 15, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28], "subclass": [1, 7], "pipelineel": [1, 5, 7, 24], "ar": [1, 8, 11, 13, 15, 22, 24, 28], "awar": 1, "pipelin": [1, 3, 6, 7, 8, 11, 13, 15, 18, 24, 25, 26, 27, 28], "element": [1, 5, 6, 7, 18, 24, 26, 27, 28], "act": 1, "wrapper": [1, 15, 20, 21, 22, 23], "around": [1, 20, 21, 22, 23], "model": [1, 3, 4, 8, 10, 11, 12, 15, 17, 18, 19, 20, 21, 22, 23, 24, 28], "respons": [1, 14], "tune": 1, "hyperparamet": [1, 10, 13, 17, 18, 19], "__init__": [1, 5, 8, 9, 12, 15, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28], "none": [1, 2, 5, 6, 7, 8, 11, 14, 15, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28], "paramet": [1, 2, 4, 5, 6, 7, 8, 11, 14, 15, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28], "current": [1, 8, 11, 13], "ml": [1, 13], "fit": [1, 2, 5, 6, 7, 9, 12, 15, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27], "x": [1, 2, 4, 5, 6, 7, 8, 10, 11, 13, 15, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28], "ndarrai": [1, 2, 5, 6, 7, 11, 15, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28], "dtype": [1, 2, 5, 6, 7, 11, 15, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28], "scalartyp": [1, 2, 5, 6, 7, 11, 15, 20, 21, 24, 25, 26, 27, 28], "y": [1, 2, 4, 5, 6, 7, 11, 13, 15, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28], "arg": [1, 2, 5, 6, 7, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27], "npt": [1, 2, 5, 6, 7, 11, 15, 17, 18, 20, 21, 24, 25, 26, 27, 28], "featur": [1, 2, 4, 5, 6, 7, 8, 11, 13, 15, 17, 18, 19, 20, 21, 22, 23, 24, 26, 27, 28], "target": [1, 2, 4, 5, 6, 7, 8, 11, 13, 17, 18, 19, 20, 21, 22, 23, 24, 26, 27, 28], "return": [1, 2, 3, 4, 5, 6, 7, 8, 11, 13, 14, 15, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28], "usual": [1, 2, 5, 6, 7, 26, 27], "type": [1, 2, 3, 4, 5, 6, 7, 8, 11, 13, 14, 15, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28], "fit_pip": [1, 6, 7, 17, 18, 19, 24, 25, 26, 27], "equival": [1, 6, 7, 17, 18, 19, 25, 26, 27], "method": [1, 4, 6, 7, 8, 15, 17, 18, 19, 20, 21, 22, 24, 25, 26, 27], "i": [1, 6, 7, 8, 10, 11, 13, 14, 15, 17, 18, 19, 22, 23, 24, 26, 27, 28], "us": [1, 4, 6, 7, 8, 10, 11, 13, 15, 17, 18, 19, 24, 25, 26, 27, 28], "chain": [1, 6, 7, 18, 24, 26, 27], "inisd": [1, 6, 7, 26, 27], "dure": [1, 6, 7, 13, 18, 26, 27, 28], "train": [1, 4, 6, 7, 8, 11, 12, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28], "forward": [1, 6, 7, 13, 17, 18, 19, 25, 26, 27], "predict": [1, 2, 5, 6, 7, 8, 10, 11, 12, 15, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28], "insid": [1, 6, 7, 8, 18], "infer": [1, 6, 7, 13, 18], "featru": [1, 6, 7, 8, 18, 22, 23, 24], "get_input_typ": [1, 6, 7, 17, 18, 19, 25, 26, 27], "input": [1, 5, 6, 7, 11, 13, 15, 22, 23, 24, 26, 27], "get_output_typ": [1, 6, 7, 17, 18, 19, 25, 26, 27], "output": [1, 5, 6, 7, 24], "base": [2, 3, 4, 5, 6, 8, 10, 13, 14, 17, 19, 20, 21, 22, 23], "all": [2, 3, 5, 6, 8, 13, 15, 24, 28], "pipeline_el": 3, "can": [3, 4, 8, 10, 11, 13, 15, 20, 21, 24], "convert": [3, 25], "onnx": [3, 5, 8, 11, 13, 15, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28], "to_onnx": [3, 17, 18, 19, 20, 21, 22, 23, 25, 26, 27], "serializedmodelrepr": [3, 17, 18, 19, 20, 21, 22, 23, 25, 26, 27], "mixin": 4, "should": [4, 5, 10, 13, 14, 15, 24, 25, 28], "order": [4, 13], "indic": [4, 24, 28], "compat": [4, 26, 27], "optunalearn": [4, 10, 13], "classmethod": [4, 14, 20, 21], "get_search_spac": [4, 20, 21], "union": [4, 11, 15, 17, 18, 19, 20, 21, 28], "callabl": [4, 19, 20, 21], "dict": [4, 8, 11, 14, 15, 18, 19, 20, 21, 24, 28], "A": [4, 15, 20, 21], "provid": [4, 11, 13, 18, 20, 21, 26, 27, 28], "an": [4, 11, 13, 14, 15, 20, 21, 28], "optuna": [4, 10, 13, 17, 20, 21], "search": [4, 20, 21], "space": [4, 20, 21], "option": [4, 8, 11, 14, 15, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28], "adjust": [4, 20, 21], "data": [4, 7, 8, 11, 13, 15, 20, 21, 22, 23, 24, 26, 27, 28], "dictionari": [4, 18, 20, 21], "describ": [4, 20, 21], "custom": [4, 13, 19, 20, 21, 22, 23], "object": [4, 8, 11, 13, 15, 19, 20, 21, 22, 23, 26, 27, 28], "function": [4, 13, 20, 21], "add_el": [5, 24], "add": [5, 24], "The": [5, 8, 10, 13, 15, 19, 24, 28], "ad": [5, 13, 24], "match": [5, 24], "last": [5, 13, 24, 28], "end": [5, 24], "save": [5, 8, 11, 13, 15, 24, 28], "modelproto": [5, 8, 24, 28], "export": [5, 13, 24], "pre": [7, 14, 19, 28], "post": 7, "process": [7, 26, 27], "e": [7, 8, 13, 14, 17, 18, 19], "g": [7, 8, 13, 14], "scale": [7, 8, 13, 24], "transform": [7, 25, 26, 27], "self": [7, 8, 15, 26, 27, 28], "pipeline_opt": [8, 11, 28], "extra_pipeline_opt": [8, 11, 28], "manag": [8, 11, 13, 28], "default": [8, 10, 11, 13, 14, 15, 17, 18, 19, 20, 21, 22, 23, 24, 25, 28], "argument": [8, 11, 13, 24, 25, 26, 27, 28], "pass": [8, 11, 13, 19, 24, 28], "instead": [8, 13, 15], "ones": [8, 11, 28], "addit": [8, 10, 11, 13, 22, 23, 28], "_create_pipelin": [8, 28], "initi": [8, 9, 10, 11, 12, 13, 25, 28], "_prepare_data": [8, 28], "prepar": [8, 13, 28], "read": [8, 13, 28], "from": [8, 11, 13, 15, 28], "file": [8, 11, 13, 28], "warn": 8, "clean": [8, 28], "preprocess": [8, 10], "encod": [8, 13, 24, 25, 26, 27], "two": [8, 13], "distinct": 8, "step": [8, 10, 11, 13], "later": 8, "one": [8, 10, 11, 13, 24, 28], "perform": [8, 10, 11, 13, 19, 22, 28], "properti": [8, 28], "default_pipelin": [8, 28], "chosen": [8, 13, 17], "dynam": [8, 17], "default_pipeline_opt": [8, 11, 28], "evalu": [8, 11, 13, 15, 28], "test_data": [8, 11, 13, 28], "metric": [8, 15, 28], "performance_summari": [8, 13, 28], "print": [8, 11, 13, 14, 28], "summari": [8, 11, 28], "test": [8, 11, 13, 15, 28], "set": [8, 11, 13, 15, 19, 28], "relev": [8, 13], "call": [8, 11, 15, 17, 18, 19, 22, 24], "save_model": [8, 9, 12, 15, 28], "filenam": [8, 13, 15, 28], "serial": [8, 17, 18, 19, 20, 21, 22, 23, 25, 26, 27, 28], "If": [8, 13, 15, 28], "specifi": [8, 13, 28], "disk": [8, 28], "onli": [8, 13, 28], "byte": [8, 28], "underli": [8, 17, 18, 19, 22, 23, 28], "high": [9, 12, 13, 24, 27], "level": [9, 12, 13, 22, 23, 24], "automl": [9, 11, 12, 13], "run_model": [9, 11, 12, 13], "scikit": [9, 12], "learn": [9, 10, 12, 13, 20, 21], "falcontabularclassifi": [9, 12, 15], "get_param": [9, 12, 15], "score": [9, 12, 15], "set_param": [9, 12, 15], "falcontabularregressor": [9, 12, 15], "tabular": [9, 11, 12, 13, 15, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28], "registri": [9, 12], "taskconfigurationregistri": [9, 12, 14], "get_configur": [9, 12, 14], "get_registered_config_nam": [9, 12, 14], "get_registered_task": [9, 12, 14], "get_task_manag": [9, 12, 14], "is_known_task": [9, 12, 14], "load_extens": [9, 12, 14], "register_configur": [9, 12, 14], "register_task": [9, 12, 14], "avail": [9, 12, 13, 28], "configur": [9, 11, 12, 14, 15], "name": [10, 11, 13, 14, 15, 28], "descript": [10, 22, 23], "superlearn": [10, 13, 15, 24], "build": [10, 13], "stack": [10, 13], "ensembl": [10, 13, 20, 21, 22, 23], "estim": [10, 13, 15, 19, 22, 23, 28], "sub": 13, "config": [10, 11, 13, 14, 15], "determin": [14, 15, 19, 28], "dataset": [10, 12, 17, 18, 19, 26, 27, 28], "size": [10, 13, 17, 19], "mini": [10, 13], "small": [10, 22, 23, 28], "consid": 10, "when": [10, 11, 13, 15, 19, 22, 23], "number": [10, 13, 15, 17, 19, 20, 21, 22, 23], "cell": 10, "after": [10, 13], "n_row": 10, "n_column": 10, "80k": 10, "mid": 10, "4kk": 10, "larg": [10, 28], "16kk": 10, "xlarg": 10, "optim": [10, 13, 17], "framework": [10, 13, 17], "histgradientboost": [17, 18], "hgbt": 10, "alia": [11, 15], "explicitli": [13, 28], "reli": [], "choic": 10, "train_data": [11, 13], "manager_configur": 11, "taskmanag": [11, 13, 14], "line": [11, 13], "follow": [11, 14], "execut": 11, "tabl": [10, 11], "random": [11, 22, 28], "split": [11, 28], "done": [11, 13, 28], "support": [11, 13], "tabular_classif": [9, 11, 12, 13, 17, 18, 19, 24, 28], "tabular_regress": [9, 11, 12, 13, 14, 17, 18, 19, 24, 28], "classif": [11, 13, 15, 17, 18, 19, 24], "regress": [11, 13, 15, 17, 18, 19], "thi": [11, 13, 15, 24, 25, 28], "path": [11, 13, 28], "csv": [11, 13], "parquet": 11, "panda": [11, 13, 28], "datafram": [11, 13, 15, 28], "numpi": [11, 13, 28], "arrai": [11, 13, 15, 28], "tupl": [11, 13, 19, 22, 23, 28], "list": [10, 11, 13, 14, 15, 19, 22, 23, 24, 26, 27, 28], "column": [11, 13, 26, 27, 28], "index": [11, 26, 27, 28], "replac": 11, "learner": [11, 13, 17, 18, 19, 24], "correspond": 11, "given": [10, 11, 14, 15, 17, 19, 22, 23, 26, 27, 28], "These": [11, 28], "overwrit": [11, 28], "attribut": [11, 28], "ignor": [11, 28], "model_path": [11, 13], "run": [11, 13], "through": 11, "np": 11, "get": [12, 15], "start": 12, "instal": 12, "usag": 12, "demo": 12, "make": [10, 12, 19, 28], "manual": 12, "select": [12, 17], "api": [12, 13], "refer": [12, 13, 22, 23], "power": 13, "machin": 13, "singl": [10, 13], "code": 13, "simpl": [13, 15], "lightweight": 13, "librari": 13, "design": 13, "peopl": 13, "who": 13, "want": 13, "instant": 13, "even": 13, "without": 13, "specif": 13, "scienc": 13, "knowledg": 13, "simpli": [13, 24], "give": 13, "your": 13, "which": [13, 15, 19, 24, 28], "you": [13, 15], "do": 13, "rest": 13, "allow": 13, "immedi": 13, "product": 13, "them": [13, 17, 18], "wide": 13, "format": [13, 15], "No": 13, "need": 13, "write": 13, "complic": 13, "anymor": 13, "stabl": 13, "releas": 13, "pypi": 13, "pip": 13, "latest": 13, "version": [13, 15], "github": 13, "git": 13, "http": 13, "com": 13, "okua1": 13, "some": [13, 15], "depend": 13, "appl": 13, "silicon": 13, "mac": 13, "might": [13, 24, 28], "work": [13, 15], "workaround": 13, "creat": [13, 15, 28], "x86": 13, "environ": 13, "conda": 13, "n": 13, "falcon_env": 13, "activ": 13, "env": 13, "subdir": 13, "osx": 13, "64": 13, "python": 13, "3": [13, 28], "9": 13, "pip3": 13, "easiest": 13, "wai": 13, "highest": 13, "shown": 13, "below": [10, 13, 19], "import": [13, 14], "titan": 13, "label": [13, 15, 24, 25], "futur": 13, "addition": 13, "also": [13, 24], "possibl": [13, 15], "otherwis": 13, "other": 13, "25": [13, 28], "kept": 13, "report": [13, 28], "titanic_test": 13, "sex": 13, "gender": 13, "ag": 13, "surviv": 13, "It": [10, 13], "contain": [13, 15], "In": [13, 15], "requir": [13, 15], "case": 13, "itself": 13, "come": 13, "non": [13, 28], "convent": 13, "sourc": 13, "pd": [13, 15, 28], "df": 13, "read_csv": 13, "x_test": 13, "y_test": 13, "while": [13, 24], "enabl": 13, "extrem": 13, "fast": [10, 13], "experiment": 13, "doe": [13, 25], "enough": 13, "control": [13, 28], "over": 13, "flexibl": 13, "more": [10, 13, 22, 23], "advanc": 13, "user": 13, "As": 13, "altern": [13, 15], "either": 13, "directli": 13, "helper": 13, "test_df": 13, "make_eval_subset": [13, 28], "true": [13, 14, 15, 19, 22, 23, 25, 28], "For": [13, 15, 17, 18, 19, 22, 23, 24], "detail": [13, 22, 23], "pleas": [13, 22, 23], "check": 13, "section": 13, "try": [13, 14], "out": 13, "built": 13, "load_churn_dataset": 13, "load_insurance_dataset": 13, "churn": 13, "insur": 13, "There": 13, "2": [13, 15, 24, 28], "unlabel": 13, "right": 13, "awai": 13, "same": 13, "wa": [13, 28], "thing": 13, "notic": 13, "have": [13, 15], "structur": 13, "assum": [13, 28], "runtim": 13, "recommend": 13, "approach": [13, 24], "training_data": 13, "unlabeled_data": 13, "to_numpi": 13, "solut": 13, "straight": 13, "real": 13, "world": 13, "applic": 13, "new": [13, 14, 28], "alwai": [10, 13, 15, 28], "therefor": [13, 28], "desir": 13, "reus": 13, "One": 13, "kei": 13, "nativ": 13, "open": 13, "neural": 13, "network": 13, "exchang": 13, "standard": 13, "repres": 13, "algorithm": 13, "mean": [13, 15, 24], "onc": 13, "platform": 13, "implement": 13, "exampl": 13, "microsoft": 13, "rutim": 13, "ort": 13, "c": 13, "java": 13, "javascript": 13, "multipl": [10, 13], "languag": 13, "virtual": 13, "everywher": 13, "chanc": 13, "thei": [10, 13], "oper": 13, "simplifi": 13, "interact": 13, "take": [13, 25], "accept": 13, "AS": 13, "complet": 13, "py": 13, "mode": 13, "infernc": 13, "we": 13, "previou": 13, "demonstr": 13, "how": 13, "howev": 13, "sever": 13, "easili": 13, "switch": 13, "between": 13, "automat": [13, 19], "But": 13, "similarli": 13, "found": 13, "here": 13, "task_configur": 14, "central": 14, "hold": 14, "defin": [14, 15, 24], "configuration_nam": 14, "allow_extensions_discoveri": 14, "bool": [14, 15, 19, 22, 23, 25, 28], "extens": [10, 14], "modul": 14, "regist": 14, "tasknanag": 14, "els": [14, 25], "fals": [14, 15, 22, 23, 28], "extension_nam": 14, "scheme": 14, "falcon_ml_": 14, "silent": [14, 28], "config_nam": 14, "registr": 14, "task_manag": 14, "handl": 14, "sklapi": 15, "make_eval_set": 15, "sklearn": [15, 20, 21, 22, 23], "falconclassifi": 15, "_falconbaseestim": 15, "classifi": 15, "deep": 15, "subobject": 15, "param": 15, "map": [15, 26, 27], "valu": [15, 28], "sample_weight": 15, "accuraci": [10, 15], "multi": 15, "subset": [15, 19, 28], "harsh": 15, "sinc": [10, 15, 25], "each": [10, 15, 22, 24, 26, 27, 28], "sampl": [15, 20, 21], "correctli": 15, "like": 15, "shape": 15, "n_sampl": 15, "n_featur": 15, "n_output": 15, "weight": 15, "wrt": 15, "float": [15, 19, 20, 21], "well": [15, 28], "nest": 15, "latter": 15, "form": 15, "compon": 15, "__": 15, "so": 15, "": 15, "updat": 15, "instanc": 15, "falconregressor": 15, "regressor": 15, "coeffici": 15, "r": 15, "1": [15, 20, 21, 22, 23, 24, 28], "frac": 15, "u": 15, "v": 15, "where": [15, 24], "residu": 15, "sum": 15, "squar": 15, "y_true": 15, "y_pred": 15, "total": 15, "best": [15, 17], "0": [15, 20, 21, 22, 23, 24], "neg": 15, "becaus": 15, "arbitrarili": 15, "wors": 15, "constant": 15, "expect": [15, 28], "disregard": 15, "would": 15, "mai": [10, 15], "precomput": 15, "kernel": 15, "matrix": 15, "gener": 15, "n_samples_fit": 15, "note": 15, "multioutput": 15, "uniform_averag": 15, "23": 15, "keep": [15, 26, 27], "consist": 15, "r2_score": 15, "influenc": 15, "except": [15, 28], "multioutputregressor": 15, "model_class": [17, 18], "n_trial": 17, "int": [17, 20, 21, 22, 23, 24, 28], "optunalern": 17, "trial": 17, "minimum": [17, 20, 21], "20": [17, 20, 21], "float32": [17, 18, 19, 20, 21, 22, 23, 26, 27], "choos": [10, 17], "final": [17, 18], "balanc": [17, 18, 19, 22], "upsampl": [17, 18, 19], "minor": [17, 18, 19], "float32arrai": [17, 18, 19, 20, 21, 22, 23, 26, 27], "int64": [17, 18, 19], "int64arrai": [17, 18, 19, 25], "its": [10, 17, 18, 19, 26, 27], "base_estim": 19, "base_score_threshold": 19, "cv": [19, 22, 23, 28], "filter_estim": 19, "emploi": 19, "stackingmodel": 19, "construct": 19, "meta": [19, 22, 23], "cross": 19, "valid": 19, "threshold": 19, "filter": 19, "fold": [19, 22, 23, 28], "perfom": [19, 28], "were": 19, "equivalen": 19, "max_it": [20, 21], "100": [20, 21], "min_samples_leaf": [20, 21], "learning_r": [20, 21], "l2_regular": [20, 21], "random_se": [20, 21], "42": [20, 21], "decis": [20, 21], "tree": [20, 21], "per": [20, 21], "leaf": [20, 21], "rate": [20, 21], "l2": [20, 21], "regular": [20, 21], "baseestim": [22, 23], "final_estim": [22, 23], "5": [22, 23], "n_job": [22, 23], "passthrough": [22, 23], "verbos": [22, 23], "stack_method": 22, "auto": 22, "sklarn": [22, 23], "document": [22, 23], "logisticregress": 22, "oversampl": 22, "parallel": [22, 23], "job": [22, 23], "origin": [22, 23, 26, 27], "mask": [24, 26, 27, 28], "columntyp": [24, 26, 27, 28], "super_learn": 24, "learner_kwarg": 24, "preprocessor": 24, "multimodalencod": 24, "On": 24, "integ": [24, 25], "decod": [24, 25], "back": [24, 25], "string": [24, 25, 26, 27], "intern": 24, "numer": [24, 26, 27, 28], "std": 24, "categor": [24, 26, 27, 28], "hot": 24, "suitabl": [10, 24], "veri": [10, 24], "cardin": [24, 27], "low": [24, 27], "scalerandencod": 24, "consecut": 24, "labeldecod": 24, "appli": [24, 25, 26, 27], "befor": [24, 25], "actual": 24, "occur": 24, "point": 24, "processor": [25, 26, 27], "vice": 25, "versa": 25, "dummi": [25, 26, 27], "main": [10, 25], "phase": 25, "noth": 25, "invers": 25, "differ": 26, "text": 26, "date": 26, "datetim": 26, "_": [26, 27], "object_": [26, 27], "own": [26, 27], "node": [26, 27], "onehotencod": 27, "ordinalencod": 27, "standardscal": 27, "simpletabularpipelin": 28, "ft": 28, "columnslist": 28, "locat": 28, "By": 28, "4": 28, "whether": 28, "screen": 28, "includ": 28, "calcul": 28, "eval": 28, "predict_stored_subset": 28, "store": 28, "pre_ev": 28, "invok": 28, "procedur": [10, 28], "first": 28, "perfrom": 28, "via": 28, "10": 28, "reccomend": [10, 28], "whole": 28, "ha": 28, "lot": 28, "result": 28, "reproduc": 28, "dedic": 28, "alloc": 28, "dropdown": [], "width": [], "75": [], "header": [], "row": [], "content": [], "plainlearn": 10, "combin": 10, "individu": 10, "greater": 10, "than": 10, "alon": 10, "weigh": 10, "maxim": 10, "additionali": 10, "smaller": 10, "produc": 10, "tend": 10, "rel": 10, "focus": 10, "finetun": 10, "long": 10, "thu": 10, "good": 10, "baselin": 10, "autom": 10, "both": 10, "caution": 10, "certain": 10, "histgradientboostingregressor": 10, "histgradientboostingclassifi": 10}, "objects": {"falcon": [[11, 0, 1, "", "AutoML"], [11, 0, 1, "", "initialize"], [11, 0, 1, "", "run_model"]], "falcon.abstract": [[1, 1, 1, "", "Learner"], [2, 1, 1, "", "Model"], [3, 1, 1, "", "ONNXConvertible"], [4, 1, 1, "", "OptunaMixin"], [5, 1, 1, "", "Pipeline"], [6, 1, 1, "", "PipelineElement"], [7, 1, 1, "", "Processor"], [8, 1, 1, "", "TaskManager"]], "falcon.abstract.Learner": [[1, 2, 1, "", "__init__"], [1, 2, 1, "", "fit"], [1, 2, 1, "", "fit_pipe"], [1, 2, 1, "", "forward"], [1, 2, 1, "", "get_input_type"], [1, 2, 1, "", "get_output_type"], [1, 2, 1, "", "predict"]], "falcon.abstract.Model": [[2, 2, 1, "", "fit"], [2, 2, 1, "", "predict"]], "falcon.abstract.ONNXConvertible": [[3, 2, 1, "", "to_onnx"]], "falcon.abstract.OptunaMixin": [[4, 2, 1, "", "get_search_space"]], "falcon.abstract.Pipeline": [[5, 2, 1, "", "__init__"], [5, 2, 1, "", "add_element"], [5, 2, 1, "", "fit"], [5, 2, 1, "", "predict"], [5, 2, 1, "", "save"]], "falcon.abstract.PipelineElement": [[6, 2, 1, "", "fit"], [6, 2, 1, "", "fit_pipe"], [6, 2, 1, "", "forward"], [6, 2, 1, "", "get_input_type"], [6, 2, 1, "", "get_output_type"], [6, 2, 1, "", "predict"]], "falcon.abstract.Processor": [[7, 2, 1, "", "fit"], [7, 2, 1, "", "fit_pipe"], [7, 2, 1, "", "forward"], [7, 2, 1, "", "get_input_type"], [7, 2, 1, "", "get_output_type"], [7, 2, 1, "", "predict"], [7, 2, 1, "", "transform"]], "falcon.abstract.TaskManager": [[8, 2, 1, "", "__init__"], [8, 2, 1, "", "_create_pipeline"], [8, 2, 1, "", "_prepare_data"], [8, 3, 1, "", "default_pipeline"], [8, 3, 1, "", "default_pipeline_options"], [8, 2, 1, "", "evaluate"], [8, 2, 1, "", "performance_summary"], [8, 2, 1, "", "predict"], [8, 2, 1, "", "save_model"], [8, 2, 1, "", "train"]], "falcon.sklapi": [[15, 1, 1, "", "FalconTabularClassifier"], [15, 1, 1, "", "FalconTabularRegressor"]], "falcon.sklapi.FalconTabularClassifier": [[15, 2, 1, "", "__init__"], [15, 2, 1, "", "fit"], [15, 2, 1, "", "get_params"], [15, 2, 1, "", "save_model"], [15, 2, 1, "", "score"], [15, 2, 1, "", "set_params"]], "falcon.sklapi.FalconTabularRegressor": [[15, 2, 1, "", "__init__"], [15, 2, 1, "", "fit"], [15, 2, 1, "", "get_params"], [15, 2, 1, "", "save_model"], [15, 2, 1, "", "score"], [15, 2, 1, "", "set_params"]], "falcon.tabular": [[28, 1, 1, "", "TabularTaskManager"]], "falcon.tabular.TabularTaskManager": [[28, 2, 1, "", "__init__"], [28, 2, 1, "", "_create_pipeline"], [28, 2, 1, "", "_prepare_data"], [28, 3, 1, "", "default_pipeline"], [28, 3, 1, "", "default_pipeline_options"], [28, 2, 1, "", "evaluate"], [28, 2, 1, "", "performance_summary"], [28, 2, 1, "", "predict"], [28, 2, 1, "", "predict_stored_subset"], [28, 2, 1, "", "save_model"], [28, 2, 1, "", "train"]], "falcon.tabular.learners": [[17, 1, 1, "", "OptunaLearner"], [18, 1, 1, "", "PlainLearner"], [19, 1, 1, "", "SuperLearner"]], "falcon.tabular.learners.OptunaLearner": [[17, 2, 1, "", "__init__"], [17, 2, 1, "", "fit"], [17, 2, 1, "", "fit_pipe"], [17, 2, 1, "", "forward"], [17, 2, 1, "", "get_input_type"], [17, 2, 1, "", "get_output_type"], [17, 2, 1, "", "predict"], [17, 2, 1, "", "to_onnx"]], "falcon.tabular.learners.PlainLearner": [[18, 2, 1, "", "__init__"], [18, 2, 1, "", "fit"], [18, 2, 1, "", "fit_pipe"], [18, 2, 1, "", "forward"], [18, 2, 1, "", "get_input_type"], [18, 2, 1, "", "get_output_type"], [18, 2, 1, "", "predict"], [18, 2, 1, "", "to_onnx"]], "falcon.tabular.learners.SuperLearner": [[19, 2, 1, "", "__init__"], [19, 2, 1, "", "fit"], [19, 2, 1, "", "fit_pipe"], [19, 2, 1, "", "forward"], [19, 2, 1, "", "get_input_type"], [19, 2, 1, "", "get_output_type"], [19, 2, 1, "", "predict"], [19, 2, 1, "", "to_onnx"]], "falcon.tabular.models": [[20, 1, 1, "", "HistGradientBoostingClassifier"], [21, 1, 1, "", "HistGradientBoostingRegressor"], [22, 1, 1, "", "StackingClassifier"], [23, 1, 1, "", "StackingRegressor"]], "falcon.tabular.models.HistGradientBoostingClassifier": [[20, 2, 1, "", "__init__"], [20, 2, 1, "", "fit"], [20, 2, 1, "", "get_search_space"], [20, 2, 1, "", "predict"], [20, 2, 1, "", "to_onnx"]], "falcon.tabular.models.HistGradientBoostingRegressor": [[21, 2, 1, "", "__init__"], [21, 2, 1, "", "fit"], [21, 2, 1, "", "get_search_space"], [21, 2, 1, "", "predict"], [21, 2, 1, "", "to_onnx"]], "falcon.tabular.models.StackingClassifier": [[22, 2, 1, "", "__init__"], [22, 2, 1, "", "fit"], [22, 2, 1, "", "predict"], [22, 2, 1, "", "to_onnx"]], "falcon.tabular.models.StackingRegressor": [[23, 2, 1, "", "__init__"], [23, 2, 1, "", "fit"], [23, 2, 1, "", "predict"], [23, 2, 1, "", "to_onnx"]], "falcon.tabular.pipelines": [[24, 1, 1, "", "SimpleTabularPipeline"]], "falcon.tabular.pipelines.SimpleTabularPipeline": [[24, 2, 1, "", "__init__"], [24, 2, 1, "", "add_element"], [24, 2, 1, "", "fit"], [24, 2, 1, "", "predict"], [24, 2, 1, "", "save"]], "falcon.tabular.processors": [[25, 1, 1, "", "LabelDecoder"], [26, 1, 1, "", "MultiModalEncoder"], [27, 1, 1, "", "ScalerAndEncoder"]], "falcon.tabular.processors.LabelDecoder": [[25, 2, 1, "", "__init__"], [25, 2, 1, "", "fit"], [25, 2, 1, "", "fit_pipe"], [25, 2, 1, "", "forward"], [25, 2, 1, "", "get_input_type"], [25, 2, 1, "", "get_output_type"], [25, 2, 1, "", "predict"], [25, 2, 1, "", "to_onnx"], [25, 2, 1, "", "transform"]], "falcon.tabular.processors.MultiModalEncoder": [[26, 2, 1, "", "__init__"], [26, 2, 1, "", "fit"], [26, 2, 1, "", "fit_pipe"], [26, 2, 1, "", "forward"], [26, 2, 1, "", "get_input_type"], [26, 2, 1, "", "get_output_type"], [26, 2, 1, "", "predict"], [26, 2, 1, "", "to_onnx"], [26, 2, 1, "", "transform"]], "falcon.tabular.processors.ScalerAndEncoder": [[27, 2, 1, "", "__init__"], [27, 2, 1, "", "fit"], [27, 2, 1, "", "fit_pipe"], [27, 2, 1, "", "forward"], [27, 2, 1, "", "get_input_type"], [27, 2, 1, "", "get_output_type"], [27, 2, 1, "", "predict"], [27, 2, 1, "", "to_onnx"], [27, 2, 1, "", "transform"]], "falcon.task_configurations": [[14, 1, 1, "", "TaskConfigurationRegistry"]], "falcon.task_configurations.TaskConfigurationRegistry": [[14, 2, 1, "", "get_configuration"], [14, 2, 1, "", "get_registered_config_names"], [14, 2, 1, "", "get_registered_tasks"], [14, 2, 1, "", "get_task_manager"], [14, 2, 1, "", "is_known_task"], [14, 2, 1, "", "load_extension"], [14, 2, 1, "", "register_configurations"], [14, 2, 1, "", "register_task"]]}, "objtypes": {"0": "py:function", "1": "py:class", "2": "py:method", "3": "py:property"}, "objnames": {"0": ["py", "function", "Python function"], "1": ["py", "class", "Python class"], "2": ["py", "method", "Python method"], "3": ["py", "property", "Python property"]}, "titleterms": {"abstract": 0, "learner": 1, "model": [2, 13], "onnxconvert": 3, "optunamixin": 4, "pipelin": 5, "pipelineel": 6, "processor": 7, "taskmanag": 8, "api": [9, 11, 15], "refer": 9, "avail": 10, "configur": [10, 13], "tabular_regress": 10, "tabular_classif": 10, "task": [10, 14], "high": 11, "level": 11, "welcom": 12, "falcon": 12, "": 12, "document": 12, "get": 13, "start": 13, "instal": 13, "usag": 13, "demo": 13, "dataset": 13, "make": 13, "predict": 13, "train": 13, "manual": 13, "select": 13, "registri": 14, "scikit": 15, "learn": 15, "tabular": 16, "optunalearn": 17, "plainlearn": 18, "superlearn": 19, "histgradientboostingclassifi": 20, "histgradientboostingregressor": 21, "stackingclassifi": 22, "stackingregressor": 23, "simpletabularpipelin": 24, "labeldecod": 25, "multimodalencod": 26, "scalerandencod": 27, "tabulartaskmanag": 28, "addit": []}, "envversion": {"sphinx.domains.c": 2, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 8, "sphinx.domains.index": 1, "sphinx.domains.javascript": 2, "sphinx.domains.math": 2, "sphinx.domains.python": 3, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx": 57}, "alltitles": {"Abstract": [[0, "abstract"]], "Learner": [[1, "learner"]], "Model": [[2, "model"]], "ONNXConvertible": [[3, "onnxconvertible"]], "OptunaMixin": [[4, "optunamixin"]], "Pipeline": [[5, "pipeline"]], "PipelineElement": [[6, "pipelineelement"]], "Processor": [[7, "processor"]], "TaskManager": [[8, "taskmanager"]], "API reference": [[9, "api-reference"]], "High level API": [[11, "high-level-api"]], "Welcome to Falcon\u2019s documentation!": [[12, "welcome-to-falcon-s-documentation"]], "Getting started": [[13, "getting-started"]], "Installation": [[13, "installation"]], "Usage": [[13, "usage"]], "Demo datasets": [[13, "demo-datasets"]], "Making predictions with trained models": [[13, "making-predictions-with-trained-models"]], "Manually selecting a configuration": [[13, "manually-selecting-a-configuration"]], "Task Registry": [[14, "task-registry"]], "Scikit-learn API": [[15, "scikit-learn-api"]], "Tabular": [[16, "tabular"]], "OptunaLearner": [[17, "optunalearner"]], "PlainLearner": [[18, "plainlearner"]], "SuperLearner": [[19, "superlearner"]], "HistGradientBoostingClassifier": [[20, "histgradientboostingclassifier"]], "HistGradientBoostingRegressor": [[21, "histgradientboostingregressor"]], "StackingClassifier": [[22, "stackingclassifier"]], "StackingRegressor": [[23, "stackingregressor"]], "SimpleTabularPipeline": [[24, "simpletabularpipeline"]], "LabelDecoder": [[25, "labeldecoder"]], "MultiModalEncoder": [[26, "multimodalencoder"]], "ScalerAndEncoder": [[27, "scalerandencoder"]], "TabularTaskManager": [[28, "tabulartaskmanager"]], "Available Configurations": [[10, "available-configurations"]], "Configurations for tabular_regression/tabular_classification tasks": [[10, "configurations-for-tabular-regression-tabular-classification-tasks"]]}, "indexentries": {}}) \ No newline at end of file diff --git a/docs/docs_build/sklearn_api.html b/docs/docs_build/sklearn_api.html deleted file mode 100644 index aaf36ef..0000000 --- a/docs/docs_build/sklearn_api.html +++ /dev/null @@ -1,374 +0,0 @@ - - - - - - - Scikit-learn API — Falcon documentation - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

Scikit-learn API

-
-
-class falcon.sklapi.FalconTabularClassifier(config: Union[str, Dict] = 'SuperLearner', make_eval_set: bool = False)
-

Falcon sklearn wrapper to be used for tabular classification tasks. -Alternatively, FalconClassifier can be used as an alias.

-
-
-__init__(config: Union[str, Dict] = 'SuperLearner', make_eval_set: bool = False) None
-
-
Parameters
-
    -
  • config (Union[str, Dict], optional) – configuration to be used, by default “SuperLearner”

  • -
  • make_eval_set (bool, optional) – determines if an evaluation set should be created, by default False

  • -
-
-
-
- -
-
-fit(X: Union[DataFrame, ndarray[Any, dtype[ScalarType]]], y: Union[DataFrame, ndarray[Any, dtype[ScalarType]]]) _FalconBaseEstimator
-

Fits the classifier

-
-
Parameters
-
    -
  • X (Union[pd.DataFrame, npt.NDArray]) – data

  • -
  • y (Union[pd.DataFrame, npt.NDArray]) – labels

  • -
-
-
-
- -
-
-get_params(deep=True)
-

Get parameters for this estimator.

-
-
Parameters
-

deep (bool, default=True) – If True, will return the parameters for this estimator and -contained subobjects that are estimators.

-
-
Returns
-

params – Parameter names mapped to their values.

-
-
Return type
-

dict

-
-
-
- -
-
-save_model(filename: Optional[str]) None
-

Saves model in onnx format

-
-
Parameters
-

filename (str, optional) – filename of the saved model

-
-
-
- -
-
-score(X, y, sample_weight=None)
-

Return the mean accuracy on the given test data and labels.

-

In multi-label classification, this is the subset accuracy -which is a harsh metric since you require for each sample that -each label set be correctly predicted.

-
-
Parameters
-
    -
  • X (array-like of shape (n_samples, n_features)) – Test samples.

  • -
  • y (array-like of shape (n_samples,) or (n_samples, n_outputs)) – True labels for X.

  • -
  • sample_weight (array-like of shape (n_samples,), default=None) – Sample weights.

  • -
-
-
Returns
-

score – Mean accuracy of self.predict(X) wrt. y.

-
-
Return type
-

float

-
-
-
- -
-
-set_params(**params)
-

Set the parameters of this estimator.

-

The method works on simple estimators as well as on nested objects -(such as Pipeline). The latter have -parameters of the form <component>__<parameter> so that it’s -possible to update each component of a nested object.

-
-
Parameters
-

**params (dict) – Estimator parameters.

-
-
Returns
-

self – Estimator instance.

-
-
Return type
-

estimator instance

-
-
-
- -
- -
-
-class falcon.sklapi.FalconTabularRegressor(config: Union[str, Dict] = 'SuperLearner', make_eval_set: bool = False)
-

Falcon sklearn wrapper to be used for tabular regression tasks. -Alternatively, FalconRegressor can be used as an alias.

-
-
-__init__(config: Union[str, Dict] = 'SuperLearner', make_eval_set: bool = False) None
-
-
Parameters
-
    -
  • config (Union[str, Dict], optional) – configuration to be used, by default “SuperLearner”

  • -
  • make_eval_set (bool, optional) – determines if an evaluation set should be created, by default False

  • -
-
-
-
- -
-
-fit(X: Union[DataFrame, ndarray[Any, dtype[ScalarType]]], y: Union[DataFrame, ndarray[Any, dtype[ScalarType]]]) _FalconBaseEstimator
-

Fits the regressor

-
-
Parameters
-
    -
  • X (Union[pd.DataFrame, npt.NDArray]) – data

  • -
  • y (Union[pd.DataFrame, npt.NDArray]) – labels

  • -
-
-
-
- -
-
-get_params(deep=True)
-

Get parameters for this estimator.

-
-
Parameters
-

deep (bool, default=True) – If True, will return the parameters for this estimator and -contained subobjects that are estimators.

-
-
Returns
-

params – Parameter names mapped to their values.

-
-
Return type
-

dict

-
-
-
- -
-
-save_model(filename: Optional[str]) None
-

Saves model in onnx format

-
-
Parameters
-

filename (str, optional) – filename of the saved model

-
-
-
- -
-
-score(X, y, sample_weight=None)
-

Return the coefficient of determination of the prediction.

-

The coefficient of determination \(R^2\) is defined as -\((1 - \frac{u}{v})\), where \(u\) is the residual -sum of squares ((y_true - y_pred)** 2).sum() and \(v\) -is the total sum of squares ((y_true - y_true.mean()) ** 2).sum(). -The best possible score is 1.0 and it can be negative (because the -model can be arbitrarily worse). A constant model that always predicts -the expected value of y, disregarding the input features, would get -a \(R^2\) score of 0.0.

-
-
Parameters
-
    -
  • X (array-like of shape (n_samples, n_features)) – Test samples. For some estimators this may be a precomputed -kernel matrix or a list of generic objects instead with shape -(n_samples, n_samples_fitted), where n_samples_fitted -is the number of samples used in the fitting for the estimator.

  • -
  • y (array-like of shape (n_samples,) or (n_samples, n_outputs)) – True values for X.

  • -
  • sample_weight (array-like of shape (n_samples,), default=None) – Sample weights.

  • -
-
-
Returns
-

score\(R^2\) of self.predict(X) wrt. y.

-
-
Return type
-

float

-
-
-

Notes

-

The \(R^2\) score used when calling score on a regressor uses -multioutput='uniform_average' from version 0.23 to keep consistent -with default value of r2_score(). -This influences the score method of all the multioutput -regressors (except for -MultiOutputRegressor).

-
- -
-
-set_params(**params)
-

Set the parameters of this estimator.

-

The method works on simple estimators as well as on nested objects -(such as Pipeline). The latter have -parameters of the form <component>__<parameter> so that it’s -possible to update each component of a nested object.

-
-
Parameters
-

**params (dict) – Estimator parameters.

-
-
Returns
-

self – Estimator instance.

-
-
Return type
-

estimator instance

-
-
-
- -
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/docs_build/tabular/index.html b/docs/docs_build/tabular/index.html deleted file mode 100644 index 95d18cc..0000000 --- a/docs/docs_build/tabular/index.html +++ /dev/null @@ -1,182 +0,0 @@ - - - - - - - Tabular — Falcon documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

Tabular

- ---- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

TabularTaskManager(task, data[, pipeline, ...])

Default task manager for tabular data.

pipelines.SimpleTabularPipeline(task, mask, ...)

Default tabular pipeline.

processors.ScalerAndEncoder(mask)

Applies OneHotEncoder/OrdinalEncoder on low/high cardinality categorical features and StandardScaler on numerical features.

processors.MultiModalEncoder(mask)

Applies different types of encodings on numerical, categorical, text and date/datetime features.

processors.LabelDecoder()

Label encoder/decoder to be used for encoding labels as integers and vice versa.

learners.SuperLearner(task[, ...])

Tabular learner which employs StackingModel for construction of meta estimator.

learners.OptunaLearner(task[, model_class, ...])

OptunaLerner select the best hyperparameters for the given model using the Optuna Framework.

learners.PlainLearner(task[, model_class, ...])

PlainLearner trains a model using provided or default hyperparameters.

models.HistGradientBoostingClassifier([...])

Wrapper around sklearn.ensemble.HistGradientBoostingClassifier.

models.HistGradientBoostingRegressor([...])

Wrapper around sklearn.ensemble.HistGradientBoostingRegressor.

models.StackingClassifier(estimators, ...[, ...])

Small wrapper around sklearn.ensemble.StackingClassifier.

models.StackingRegressor(estimators, ...[, ...])

Small wrapper around sklearn.ensemble.StackingRegressor.

-
-
-
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/docs_build/tabular/learners/optuna_learner.html b/docs/docs_build/tabular/learners/optuna_learner.html deleted file mode 100644 index ab610d0..0000000 --- a/docs/docs_build/tabular/learners/optuna_learner.html +++ /dev/null @@ -1,261 +0,0 @@ - - - - - - - OptunaLearner — Falcon documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

OptunaLearner

-
-
-class falcon.tabular.learners.OptunaLearner(task: str, model_class: Optional[Type] = None, n_trials: Optional[int] = None, **kwargs: Any)
-

OptunaLerner select the best hyperparameters for the given model using the Optuna Framework.

-
-
-__init__(task: str, model_class: Optional[Type] = None, n_trials: Optional[int] = None, **kwargs: Any) None
-
-
Parameters
-
    -
  • task (str) – ‘tabular_classification’ or ‘tabular_regression’

  • -
  • model_class (Optional[Type], optional) – the class of the model to train, by default None; -if None, HistGradientBoosting

  • -
  • n_trials (Optional[int], optional) – number of optimization trials, minimum 20, by default None; -if None, the number of trials is chosen dynamically based on the dataset size

  • -
-
-
-
- -
-
-fit(X: ndarray[Any, dtype[float32]], y: ndarray[Any, dtype[float32]], *args: Any, **kwargs: Any) None
-

Fits the model by choosing the best hyperparameters and training the final model using them. -For classification tasks, the dataset will be balanced by upsampling the minority class(es).

-
-
Parameters
-
    -
  • X (Float32Array) – features

  • -
  • y (Float32Array) – targets

  • -
-
-
-
- -
-
-fit_pipe(X: ndarray[Any, dtype[float32]], y: ndarray[Any, dtype[float32]], *args: Any, **kwargs: Any) None
-

Equivalent to .fit(X, y)

-
-
Parameters
-
    -
  • X (Float32Array) – features

  • -
  • y (Float32Array) – targets

  • -
-
-
-
- -
-
-forward(X: ndarray[Any, dtype[float32]], *args: Any, **kwargs: Any) Union[ndarray[Any, dtype[float32]], ndarray[Any, dtype[int64]]]
-

Equivalent to .predict(X)

-
-
Parameters
-

X (Float32Array) – features

-
-
Returns
-

predictions

-
-
Return type
-

Union[Float32Array, Int64Array]

-
-
-
- -
-
-get_input_type() Type
-
-
Returns
-

Float32Array

-
-
Return type
-

Type

-
-
-
- -
-
-get_output_type() Type
-
-
Returns
-

Float32Array for regression, Int64Array for classification

-
-
Return type
-

Type

-
-
-
- -
-
-predict(X: ndarray[Any, dtype[float32]], *args: Any, **kwargs: Any) Union[ndarray[Any, dtype[float32]], ndarray[Any, dtype[int64]]]
-
-
Parameters
-

X (npt.NDArray) – features

-
-
Returns
-

predictions

-
-
Return type
-

npt.NDArray

-
-
-
- -
-
-to_onnx() SerializedModelRepr
-

Serializes the underlying model to onnx by calling its .to_onnx() method.

-
-
Return type
-

SerializedModelRepr

-
-
-
- -
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/docs_build/tabular/learners/plain_learner.html b/docs/docs_build/tabular/learners/plain_learner.html deleted file mode 100644 index 487e856..0000000 --- a/docs/docs_build/tabular/learners/plain_learner.html +++ /dev/null @@ -1,260 +0,0 @@ - - - - - - - PlainLearner — Falcon documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

PlainLearner

-
-
-class falcon.tabular.learners.PlainLearner(task: str, model_class: Optional[Type] = None, hyperparameters: Optional[Dict] = None, **kwargs: Any)
-

PlainLearner trains a model using provided or default hyperparameters.

-
-
-__init__(task: str, model_class: Optional[Type] = None, hyperparameters: Optional[Dict] = None, **kwargs: Any) None
-
-
Parameters
-
    -
  • task (str) – ‘tabular_classification’ or ‘tabular_regression’

  • -
  • model_class (Optional[Type], optional) – the class of the model to train, by default None; -if None, HistGradientBoosting is used

  • -
  • hyperparameters (Dict, optional) – the dictionary of hyperparameters for model training

  • -
-
-
-
- -
-
-fit(X: ndarray[Any, dtype[float32]], y: ndarray[Any, dtype[float32]], *args: Any, **kwargs: Any) None
-

Fits the model and trains the final model using them. -For classification tasks, the dataset will be balanced by upsampling the minority class(es).

-
-
Parameters
-
    -
  • X (Float32Array) – features

  • -
  • y (Float32Array) – targets

  • -
-
-
-
- -
-
-fit_pipe(X: ndarray[Any, dtype[float32]], y: ndarray[Any, dtype[float32]], *args: Any, **kwargs: Any) None
-

Equivalent to .fit(X, y)

-
-
Parameters
-
    -
  • X (Float32Array) – features

  • -
  • y (Float32Array) – targets

  • -
-
-
-
- -
-
-forward(X: Any, *args: Any, **kwargs: Any) Any
-

Equivalent of predict method that is used for elements chaining inside pipeline during inference.

-
-
Parameters
-

X (Any) – featrues

-
-
Returns
-

predictions

-
-
Return type
-

Any

-
-
-
- -
-
-get_input_type() Type
-
-
Returns
-

Float32Array

-
-
Return type
-

Type

-
-
-
- -
-
-get_output_type() Type
-
-
Returns
-

Float32Array for regression, Int64Array for classification

-
-
Return type
-

Type

-
-
-
- -
-
-predict(X: ndarray[Any, dtype[float32]], *args: Any, **kwargs: Any) Union[ndarray[Any, dtype[float32]], ndarray[Any, dtype[int64]]]
-
-
Parameters
-

X (npt.NDArray) – features

-
-
Returns
-

predictions

-
-
Return type
-

npt.NDArray

-
-
-
- -
-
-to_onnx() SerializedModelRepr
-

Serializes the underlying model to onnx by calling its .to_onnx() method.

-
-
Return type
-

SerializedModelRepr

-
-
-
- -
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/docs_build/tabular/learners/super_learner.html b/docs/docs_build/tabular/learners/super_learner.html deleted file mode 100644 index fbcf4a3..0000000 --- a/docs/docs_build/tabular/learners/super_learner.html +++ /dev/null @@ -1,263 +0,0 @@ - - - - - - - SuperLearner — Falcon documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

SuperLearner

-
-
-class falcon.tabular.learners.SuperLearner(task: str, base_estimators: Optional[List[Tuple[str, Callable, Dict]]] = None, base_score_threshold: Optional[float] = None, cv: Optional[Any] = None, filter_estimators: Optional[bool] = None)
-

Tabular learner which employs StackingModel for construction of meta estimator.

-
-
-__init__(task: str, base_estimators: Optional[List[Tuple[str, Callable, Dict]]] = None, base_score_threshold: Optional[float] = None, cv: Optional[Any] = None, filter_estimators: Optional[bool] = None) None
-

Constructs a meta model which is trained on cross-validated predictions of base estimators.

-
-
Parameters
-
    -
  • task (str) – tabular_classification or tabular_regression

  • -
  • base_estimators (Optional[List[Tuple[str, Callable, Dict]]], optional) – list of base estimators, by default None

  • -
  • base_score_threshold (Optional[float], optional) – threshold for filtering of the estimators, by default None

  • -
  • cv (Any, optional) – number of CV folds or CV custom object, by default None

  • -
  • filter_estimators (Optional[bool], optional) – when True, the perfomance of the estimators pre-estimated on the subset of training, estimators with the performance below the threshold will not be used for meta model construction, by default None

  • -
-
-
-
- -
-
-fit(X: ndarray[Any, dtype[float32]], y: ndarray[Any, dtype[float32]], *args: Any, **kwargs: Any) None
-

Fits the model. The hyperparameters that were not passed to the __init__ will be automatically determined based on the size of the training set. -For classification tasks, the dataset will be balanced by upsampling the minority class(es).

-
-
Parameters
-
    -
  • X (Float32Array) – features

  • -
  • y (Float32Array) – targets

  • -
-
-
-
- -
-
-fit_pipe(X: ndarray[Any, dtype[float32]], y: ndarray[Any, dtype[float32]], *args: Any, **kwargs: Any) None
-

Equivalent to .fit(X, y)

-
-
Parameters
-
    -
  • X (Float32Array) – features

  • -
  • y (Float32Array) – targets

  • -
-
-
-
- -
-
-forward(X: ndarray[Any, dtype[float32]], *args: Any, **kwargs: Any) Union[ndarray[Any, dtype[float32]], ndarray[Any, dtype[int64]]]
-

Equivalen to .predict(X)

-
-
Parameters
-

X (Float32Array) – features

-
-
Returns
-

predictions

-
-
Return type
-

Union[Float32Array, Int64Array]

-
-
-
- -
-
-get_input_type() Type
-
-
Returns
-

Float32Array

-
-
Return type
-

Type

-
-
-
- -
-
-get_output_type() Type
-
-
Returns
-

Float32Array for regression, Int64Array for classification

-
-
Return type
-

Type

-
-
-
- -
-
-predict(X: ndarray[Any, dtype[float32]], *args: Any, **kwargs: Any) Union[ndarray[Any, dtype[float32]], ndarray[Any, dtype[int64]]]
-

Makes a prediction for given X.

-
-
Parameters
-

X (Float32Array) – features

-
-
Returns
-

predictions

-
-
Return type
-

Union[Float32Array, Int64Array]

-
-
-
- -
-
-to_onnx() SerializedModelRepr
-

Serializes the underlying model to onnx by calling its .to_onnx() method.

-
-
Return type
-

SerializedModelRepr

-
-
-
- -
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/docs_build/tabular/models/hgbt_clf.html b/docs/docs_build/tabular/models/hgbt_clf.html deleted file mode 100644 index 838d0fc..0000000 --- a/docs/docs_build/tabular/models/hgbt_clf.html +++ /dev/null @@ -1,224 +0,0 @@ - - - - - - - HistGradientBoostingClassifier — Falcon documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

HistGradientBoostingClassifier

-
-
-class falcon.tabular.models.HistGradientBoostingClassifier(max_iter: int = 100, min_samples_leaf: int = 20, learning_rate: float = 0.1, l2_regularization: float = 0.0, random_seed: int = 42, **kwargs: Any)
-

Wrapper around sklearn.ensemble.HistGradientBoostingClassifier.

-
-
-__init__(max_iter: int = 100, min_samples_leaf: int = 20, learning_rate: float = 0.1, l2_regularization: float = 0.0, random_seed: int = 42, **kwargs: Any)
-
-
Parameters
-
    -
  • max_iter (int, optional) – number of decision trees, by default 100

  • -
  • min_samples_leaf (int, optional) – minimum number of samples per leaf, by default 20

  • -
  • learning_rate (float, optional) – learning rate, by default 0.1

  • -
  • l2_regularization (float, optional) – L2 regularization parameter, by default 0.0

  • -
  • random_seed (int, optional) – by default 42

  • -
-
-
-
- -
-
-fit(X: ndarray[Any, dtype[float32]], y: ndarray[Any, dtype[float32]], *args: Any, **kwargs: Any) None
-

Fits the model

-
-
Parameters
-
    -
  • X (Float32Array) – Features

  • -
  • y (Float32Array) – targets

  • -
-
-
-
- -
-
-classmethod get_search_space(X: ndarray[Any, dtype[ScalarType]], y: ndarray[Any, dtype[ScalarType]]) Union[Callable, Dict]
-

A class method that provides an optuna search space for the model. -Optionally, the search space can be adjusted based on the provided training data.

-
-
Parameters
-
    -
  • X (Any) – features

  • -
  • y (Any) – targets

  • -
-
-
Returns
-

dictionary that describes the search space, or custom objective function

-
-
Return type
-

Union[Callable, Dict]

-
-
-
- -
-
-predict(X: ndarray[Any, dtype[ScalarType]], *args: Any, **kwargs: Any) ndarray[Any, dtype[ScalarType]]
-
-
Parameters
-

X (npt.NDArray) – features

-
-
Returns
-

predictions

-
-
Return type
-

npt.NDArray

-
-
-
- -
-
-to_onnx() SerializedModelRepr
-

Serializes the model to onnx.

-
-
Return type
-

SerializedModelRepr

-
-
-
- -
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/docs_build/tabular/models/hgbt_regr.html b/docs/docs_build/tabular/models/hgbt_regr.html deleted file mode 100644 index 9afbb61..0000000 --- a/docs/docs_build/tabular/models/hgbt_regr.html +++ /dev/null @@ -1,224 +0,0 @@ - - - - - - - HistGradientBoostingRegressor — Falcon documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

HistGradientBoostingRegressor

-
-
-class falcon.tabular.models.HistGradientBoostingRegressor(max_iter: int = 100, min_samples_leaf: int = 20, learning_rate: float = 0.1, l2_regularization: float = 0.0, random_seed: int = 42, **kwargs: Any)
-

Wrapper around sklearn.ensemble.HistGradientBoostingRegressor.

-
-
-__init__(max_iter: int = 100, min_samples_leaf: int = 20, learning_rate: float = 0.1, l2_regularization: float = 0.0, random_seed: int = 42, **kwargs: Any)
-
-
Parameters
-
    -
  • max_iter (int, optional) – number of decision trees, by default 100

  • -
  • min_samples_leaf (int, optional) – minimum number of samples per leaf, by default 20

  • -
  • learning_rate (float, optional) – learning rate, by default 0.1

  • -
  • l2_regularization (float, optional) – L2 regularization parameter, by default 0.0

  • -
  • random_seed (int, optional) – by default 42

  • -
-
-
-
- -
-
-fit(X: ndarray[Any, dtype[float32]], y: ndarray[Any, dtype[float32]], *args: Any, **kwargs: Any) None
-

Fits the model

-
-
Parameters
-
    -
  • X (Float32Array) – Features

  • -
  • y (Float32Array) – targets

  • -
-
-
-
- -
-
-classmethod get_search_space(X: ndarray[Any, dtype[ScalarType]], y: ndarray[Any, dtype[ScalarType]]) Union[Callable, Dict]
-

A class method that provides an optuna search space for the model. -Optionally, the search space can be adjusted based on the provided training data.

-
-
Parameters
-
    -
  • X (Any) – features

  • -
  • y (Any) – targets

  • -
-
-
Returns
-

dictionary that describes the search space, or custom objective function

-
-
Return type
-

Union[Callable, Dict]

-
-
-
- -
-
-predict(X: ndarray[Any, dtype[ScalarType]], *args: Any, **kwargs: Any) ndarray[Any, dtype[ScalarType]]
-
-
Parameters
-

X (npt.NDArray) – features

-
-
Returns
-

predictions

-
-
Return type
-

npt.NDArray

-
-
-
- -
-
-to_onnx() SerializedModelRepr
-

Serializes the model to onnx.

-
-
Return type
-

SerializedModelRepr

-
-
-
- -
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/docs_build/tabular/models/stacking_clf.html b/docs/docs_build/tabular/models/stacking_clf.html deleted file mode 100644 index c07f8ea..0000000 --- a/docs/docs_build/tabular/models/stacking_clf.html +++ /dev/null @@ -1,209 +0,0 @@ - - - - - - - StackingClassifier — Falcon documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

StackingClassifier

-
-
-class falcon.tabular.models.StackingClassifier(estimators: List[Tuple[str, BaseEstimator]], final_estimator: BaseEstimator, balanced: bool = True, cv: Any = 5, n_jobs: int = 1, passthrough: bool = False, verbose: int = 0, stack_method: Any = 'auto', **kwargs: Any)
-

Small wrapper around sklearn.ensemble.StackingClassifier.

-
-
-__init__(estimators: List[Tuple[str, BaseEstimator]], final_estimator: BaseEstimator, balanced: bool = True, cv: Any = 5, n_jobs: int = 1, passthrough: bool = False, verbose: int = 0, stack_method: Any = 'auto', **kwargs: Any) None
-

Small wrapper around sklearn.ensemble.StackingClassifier. -For more detailed description please refer to sklarn documentation.

-
-
Parameters
-
    -
  • estimators (List[Tuple[str, BaseEstimator]]) – base estimators

  • -
  • final_estimator (BaseEstimator) – meta estimator, by default LogisticRegression

  • -
  • balanced (bool, optional) – if True, the classes are balanced by performing random oversampling, by default True

  • -
  • cv (Any, optional) – number of CV folds, or custom CV object, by default 5

  • -
  • n_jobs (int, optional) – number of parallel jobs, by default -1

  • -
  • passthrough (bool, optional) – when True the meta estimator is trained on original data in addition to the predictions of base estimators, by default False

  • -
  • verbose (int, optional) – verbosity level of underlying sklearn estimator, by default 0

  • -
  • stack_method (Any, optional) – methods called for each base estimator, by default “auto”

  • -
-
-
-
- -
-
-fit(X: ndarray[Any, dtype[float32]], y: ndarray[Any, dtype[float32]], *args: Any, **kwargs: Any) None
-

Fits the model

-
-
Parameters
-
    -
  • X (Float32Array) – Features

  • -
  • y (Float32Array) – targets

  • -
-
-
-
- -
-
-predict(X: ndarray[Any, dtype[float32]], *args: Any, **kwargs: Any) ndarray[Any, dtype[float32]]
-

Predicts the target for the given input

-
-
Parameters
-

X (Float32Array) – featrues

-
-
Returns
-

predictions

-
-
Return type
-

Float32Array

-
-
-
- -
-
-to_onnx() SerializedModelRepr
-

Serializes the model to onnx.

-
-
Return type
-

SerializedModelRepr

-
-
-
- -
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/docs_build/tabular/models/stacking_regr.html b/docs/docs_build/tabular/models/stacking_regr.html deleted file mode 100644 index d1b33d3..0000000 --- a/docs/docs_build/tabular/models/stacking_regr.html +++ /dev/null @@ -1,207 +0,0 @@ - - - - - - - StackingRegressor — Falcon documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

StackingRegressor

-
-
-class falcon.tabular.models.StackingRegressor(estimators: List[Tuple[str, BaseEstimator]], final_estimator: BaseEstimator, cv: Any = 5, n_jobs: int = 1, passthrough: bool = False, verbose: int = 0, **kwargs: Any)
-

Small wrapper around sklearn.ensemble.StackingRegressor.

-
-
-__init__(estimators: List[Tuple[str, BaseEstimator]], final_estimator: BaseEstimator, cv: Any = 5, n_jobs: int = 1, passthrough: bool = False, verbose: int = 0, **kwargs: Any) None
-

Small wrapper around sklearn.ensemble.StackingRegressor. -For more detailed description please refer to sklarn documentation.

-
-
Parameters
-
    -
  • estimators (List[Tuple[str, BaseEstimator]]) – base estimators

  • -
  • final_estimator (BaseEstimator) – meta estimator

  • -
  • cv (Any, optional) – number of CV folds, or custom CV object, by default 5

  • -
  • n_jobs (int, optional) – number of parallel jobs, by default -1

  • -
  • passthrough (bool, optional) – when True the meta estimator is trained on original data in addition to the predictions of base estimators, by default False

  • -
  • verbose (int, optional) – verbosity level of underlying sklearn estimator, by default 0

  • -
-
-
-
- -
-
-fit(X: ndarray[Any, dtype[float32]], y: ndarray[Any, dtype[float32]], *args: Any, **kwargs: Any) None
-

Fits the model

-
-
Parameters
-
    -
  • X (Float32Array) – Features

  • -
  • y (Float32Array) – targets

  • -
-
-
-
- -
-
-predict(X: ndarray[Any, dtype[float32]], *args: Any, **kwargs: Any) ndarray[Any, dtype[float32]]
-

Predicts the target for the given input

-
-
Parameters
-

X (Float32Array) – featrues

-
-
Returns
-

predictions

-
-
Return type
-

Float32Array

-
-
-
- -
-
-to_onnx() SerializedModelRepr
-

Serializes the model to onnx.

-
-
Return type
-

SerializedModelRepr

-
-
-
- -
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/docs_build/tabular/pipelines/simple_pipeline.html b/docs/docs_build/tabular/pipelines/simple_pipeline.html deleted file mode 100644 index cc61b8c..0000000 --- a/docs/docs_build/tabular/pipelines/simple_pipeline.html +++ /dev/null @@ -1,222 +0,0 @@ - - - - - - - SimpleTabularPipeline — Falcon documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

SimpleTabularPipeline

-
-
-class falcon.tabular.pipelines.SimpleTabularPipeline(task: str, mask: ~typing.List[~falcon.types.ColumnTypes], learner: ~typing.Type[~falcon.abstract.learner.Learner] = <class 'falcon.tabular.learners.super_learner.SuperLearner'>, learner_kwargs: ~typing.Optional[~typing.Dict] = None, preprocessor: str = 'MultiModalEncoder')
-

Default tabular pipeline.

-
-
-__init__(task: str, mask: ~typing.List[~falcon.types.ColumnTypes], learner: ~typing.Type[~falcon.abstract.learner.Learner] = <class 'falcon.tabular.learners.super_learner.SuperLearner'>, learner_kwargs: ~typing.Optional[~typing.Dict] = None, preprocessor: str = 'MultiModalEncoder')
-

Default tabular pipeline. On a high level it simply chains a preprocessor and model learner (by default SuperLearner). -For classification tasks, the labels are also encoded as integers (while predictions are decoded back to strings). -Internally, all numerical features are scaled to 0 mean and 1 std. All categorical features are one-hot encoded (this approach might not be suitable for features with very high cardinality).

-
-
Parameters
-
    -
  • task (str) – tabular_classification or tabular_regression

  • -
  • mask (List[int]) – list of ints where 1/2 indicates a low/high cardinality categorical feature and 0 indicates a numerical feature

  • -
  • learner (Learner, optional) – learner class to be used, by default SuperLearner

  • -
  • learner_kwargs (Optional[Dict], optional) – arguments to be passed to the learner, by default None

  • -
  • preprocessor (str) – defines which preprocessor to use, can be one of {‘MultiModalEncoder’,’ScalerAndEncoder’}, by default ‘MultiModalEncoder’

  • -
-
-
-
- -
-
-add_element(element: PipelineElement) None
-

Adds element to pipeline. The input type of added element should match the output type of the last element in the pipeline.

-
-
Parameters
-

element (PipelineElement) – element to be added to the end of the pipeline

-
-
-
- -
-
-fit(X: ndarray[Any, dtype[ScalarType]], y: ndarray[Any, dtype[ScalarType]], *args: Any, **kwargs: Any) None
-

Fits the pipeline by consecutively calling .fit_pipe() method of each element in pipeline. -For tabular classification, LabelDecoder is applied to targets before actual training occurs.

-
-
Parameters
-
    -
  • X (npt.NDArray) – train featrues

  • -
  • y (npt.NDArray) – train targets

  • -
-
-
-
- -
-
-predict(X: ndarray[Any, dtype[ScalarType]], *args: Any, **kwargs: Any) ndarray[Any, dtype[ScalarType]]
-

Predicts the label of passed data points.

-
-
Parameters
-

X (npt.NDArray) – features

-
-
Returns
-

predicted label

-
-
Return type
-

npt.NDArray

-
-
-
- -
-
-save() ModelProto
-

Exports the pipeline to ONNX ModelProto

-
-
Returns
-

Pipeline as ONNX ModelProto

-
-
Return type
-

ModelProto

-
-
-
- -
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/docs_build/tabular/processors/label_decoder.html b/docs/docs_build/tabular/processors/label_decoder.html deleted file mode 100644 index d5d93b4..0000000 --- a/docs/docs_build/tabular/processors/label_decoder.html +++ /dev/null @@ -1,274 +0,0 @@ - - - - - - - LabelDecoder — Falcon documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

LabelDecoder

-
-
-class falcon.tabular.processors.LabelDecoder
-

Label encoder/decoder to be used for encoding labels as integers and vice versa.

-
-
-__init__() None
-

does not take any arguments

-
- -
-
-fit(X: ndarray[Any, dtype[ScalarType]], y: Optional[Any] = None, *args: Any, **kwargs: Any) None
-

Fits the decoder.

-
-
Parameters
-
    -
  • X (npt.NDArray) – labels to be encoded as integers

  • -
  • y (Any, optional) – dummy argument, by default None

  • -
-
-
-
- -
-
-fit_pipe(X: Any, y: Any, *args: Any, **kwargs: Any) None
-

Since label decoder should initially be fitted and applied before the main training phase of pipeline, this method does nothing.

-
-
Parameters
-
    -
  • X (Any) – dummy argument

  • -
  • y (Any) – dummy argument

  • -
-
-
-
- -
-
-forward(X: ndarray[Any, dtype[ScalarType]], *args: Any, **kwargs: Any) ndarray[Any, dtype[ScalarType]]
-

Equivalent to .transform(X, inverse=True).

-
-
Parameters
-

X (npt.NDArray) – labels to decode

-
-
Returns
-

labels decoded to strings

-
-
Return type
-

npt.NDArray

-
-
-
- -
-
-get_input_type() Type
-
-
Returns
-

Int64Array

-
-
Return type
-

Type

-
-
-
- -
-
-get_output_type() Type
-
-
Returns
-

NDArray[str]

-
-
Return type
-

Type

-
-
-
- -
-
-predict(X: ndarray[Any, dtype[ScalarType]], inverse: bool = True, *args: Any, **kwargs: Any) ndarray[Any, dtype[ScalarType]]
-

Equivalent of .transform().

-
-
Parameters
-
    -
  • X (npt.NDArray) – labels

  • -
  • inverse (bool, optional) – if True, encode strings as integers, else convert integers back to strings, by default True

  • -
-
-
Returns
-

encoded/decoded labels

-
-
Return type
-

npt.NDArray

-
-
-
- -
-
-to_onnx() SerializedModelRepr
-

Serializes the encoder to onnx.

-
-
Return type
-

SerializedModelRepr

-
-
-
- -
-
-transform(X: ndarray[Any, dtype[ScalarType]], inverse: bool = True, *args: Any, **kwargs: Any) ndarray[Any, dtype[ScalarType]]
-

Encodes/decodes the labels.

-
-
Parameters
-
    -
  • X (npt.NDArray) – labels

  • -
  • inverse (bool, optional) – if True, encode strings as integers, else convert integers back to strings, by default True

  • -
-
-
Returns
-

encoded/decoded labels

-
-
Return type
-

npt.NDArray

-
-
-
- -
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/docs_build/tabular/processors/mm_encoder.html b/docs/docs_build/tabular/processors/mm_encoder.html deleted file mode 100644 index 8863588..0000000 --- a/docs/docs_build/tabular/processors/mm_encoder.html +++ /dev/null @@ -1,279 +0,0 @@ - - - - - - - MultiModalEncoder — Falcon documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

MultiModalEncoder

-
-
-class falcon.tabular.processors.MultiModalEncoder(mask: List[ColumnTypes])
-

Applies different types of encodings on numerical, categorical, text and date/datetime features.

-
-
-__init__(mask: List[ColumnTypes]) None
-
-
Parameters
-

mask (List[ColumnTypes]) – provides a type for each column at a given index

-
-
-
- -
-
-fit(X: ndarray[Any, dtype[ScalarType]], y: Optional[Any] = None, *args: Any, **kwargs: Any) None
-

Fits the encoder.

-
-
Parameters
-
    -
  • X (npt.NDArray) – data to encode

  • -
  • _ (Any, optional) – dummy argument to keep compatibility with pipeline training

  • -
-
-
-
- -
-
-fit_pipe(X: Any, y: Any, *args: Any, **kwargs: Any) Any
-

Equivalent of fit method that is used for elements chaining inisde pipeline during training.

-
-
Parameters
-
    -
  • X (Any) – features

  • -
  • y (Any) – targets

  • -
-
-
Returns
-

usually None

-
-
Return type
-

Any

-
-
-
- -
-
-forward(X: ndarray[Any, dtype[object_]], *args: Any, **kwargs: Any) ndarray[Any, dtype[ScalarType]]
-

Equivalent of .predict() or .transform().

-
-
Parameters
-

X (npt.NDArray[object]) – data to process

-
-
Returns
-

processed data

-
-
Return type
-

npt.NDArray

-
-
-
- -
-
-get_input_type() Type
-
-
Returns
-

object

-
-
Return type
-

Type

-
-
-
- -
-
-get_output_type() Type
-
-
Returns
-

Float32Array

-
-
Return type
-

Type

-
-
-
- -
-
-predict(X: ndarray[Any, dtype[ScalarType]], *args: Any, **kwargs: Any) ndarray[Any, dtype[ScalarType]]
-

Applies the encoder.

-
-
Parameters
-

X (npt.NDArray) – input data

-
-
Returns
-

encoded data

-
-
Return type
-

npt.NDArray

-
-
-
- -
-
-to_onnx() SerializedModelRepr
-

Serializes the encoder to onnx. -Each feature in the original dataset is mapped to its own input node (float32 for numerical or string for categorical).

-
-
Return type
-

SerializedModelRepr

-
-
-
- -
-
-transform(X: ndarray[Any, dtype[ScalarType]], *args: Any, **kwargs: Any) ndarray[Any, dtype[ScalarType]]
-

Equivalent of self.predict(X)

-
-
Parameters
-

X (npt.NDArray) – features

-
-
Returns
-

transformed features

-
-
Return type
-

npt.NDArray

-
-
-
- -
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/docs_build/tabular/processors/scaler_and_encoder.html b/docs/docs_build/tabular/processors/scaler_and_encoder.html deleted file mode 100644 index c2c61d8..0000000 --- a/docs/docs_build/tabular/processors/scaler_and_encoder.html +++ /dev/null @@ -1,279 +0,0 @@ - - - - - - - ScalerAndEncoder — Falcon documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

ScalerAndEncoder

-
-
-class falcon.tabular.processors.ScalerAndEncoder(mask: List[ColumnTypes])
-

Applies OneHotEncoder/OrdinalEncoder on low/high cardinality categorical features and StandardScaler on numerical features.

-
-
-__init__(mask: List[ColumnTypes]) None
-
-
Parameters
-

mask (List[ColumnTypes]) – provides a type for each column at a given index

-
-
-
- -
-
-fit(X: ndarray[Any, dtype[ScalarType]], y: Optional[Any] = None, *args: Any, **kwargs: Any) None
-

Fits the encoder.

-
-
Parameters
-
    -
  • X (npt.NDArray) – data to encode

  • -
  • _ (Any, optional) – dummy argument to keep compatibility with pipeline training

  • -
-
-
-
- -
-
-fit_pipe(X: Any, y: Any, *args: Any, **kwargs: Any) Any
-

Equivalent of fit method that is used for elements chaining inisde pipeline during training.

-
-
Parameters
-
    -
  • X (Any) – features

  • -
  • y (Any) – targets

  • -
-
-
Returns
-

usually None

-
-
Return type
-

Any

-
-
-
- -
-
-forward(X: ndarray[Any, dtype[object_]], *args: Any, **kwargs: Any) ndarray[Any, dtype[ScalarType]]
-

Equivalent of .predict() or .transform().

-
-
Parameters
-

X (npt.NDArray[object]) – data to process

-
-
Returns
-

processed data

-
-
Return type
-

npt.NDArray

-
-
-
- -
-
-get_input_type() Type
-
-
Returns
-

object

-
-
Return type
-

Type

-
-
-
- -
-
-get_output_type() Type
-
-
Returns
-

Float32Array

-
-
Return type
-

Type

-
-
-
- -
-
-predict(X: ndarray[Any, dtype[ScalarType]], *args: Any, **kwargs: Any) ndarray[Any, dtype[ScalarType]]
-

Applies the encoder.

-
-
Parameters
-

X (npt.NDArray) – input data

-
-
Returns
-

encoded data

-
-
Return type
-

npt.NDArray

-
-
-
- -
-
-to_onnx() SerializedModelRepr
-

Serializes the encoder to onnx. -Each feature in the original dataset is mapped to its own input node (float32 for numerical or string for categorical).

-
-
Return type
-

SerializedModelRepr

-
-
-
- -
-
-transform(X: ndarray[Any, dtype[ScalarType]], *args: Any, **kwargs: Any) ndarray[Any, dtype[ScalarType]]
-

Equivalent of self.predict(X)

-
-
Parameters
-

X (npt.NDArray) – features

-
-
Returns
-

transformed features

-
-
Return type
-

npt.NDArray

-
-
-
- -
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/docs_build/tabular/tab_manager.html b/docs/docs_build/tabular/tab_manager.html deleted file mode 100644 index 1056c3d..0000000 --- a/docs/docs_build/tabular/tab_manager.html +++ /dev/null @@ -1,326 +0,0 @@ - - - - - - - TabularTaskManager — Falcon documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

TabularTaskManager

-
-
-class falcon.tabular.TabularTaskManager(task: str, data: Union[str, ndarray[Any, dtype[ScalarType]], DataFrame, Tuple], pipeline: Optional[Type[Pipeline]] = None, pipeline_options: Optional[Dict] = None, extra_pipeline_options: Optional[Dict] = None, features: Optional[Union[List[str], List[int]]] = None, target: Optional[Union[str, int]] = None, **options: Any)
-

Default task manager for tabular data.

-
-
-__init__(task: str, data: Union[str, ndarray[Any, dtype[ScalarType]], DataFrame, Tuple], pipeline: Optional[Type[Pipeline]] = None, pipeline_options: Optional[Dict] = None, extra_pipeline_options: Optional[Dict] = None, features: Optional[Union[List[str], List[int]]] = None, target: Optional[Union[str, int]] = None, **options: Any) None
-
-
Parameters
-
    -
  • task (str) – tabular_classification or tabular_regression

  • -
  • data (Union[str, npt.NDArray, pd.DataFrame, Tuple]) – path to data file or pandas dataframe or numpy array or tuple (X,y)

  • -
  • pipeline (Optional[Type[Pipeline]]) – class to be used as pipeline, by default None. -If None, SimpleTabularPipeline will be used

  • -
  • pipeline_options (Optional[Dict], optional) – arguments to be passed to the pipeline, by default None. -These options will overwrite the ones from default_pipeline_options attribute.

  • -
  • extra_pipeline_options (Optional[Dict], optional) – arguments to be passed to the pipeline, by default None. -These options will be passed in addition to the ones from default_pipeline_options attribute. -This argument is ignored if pipeline_options is not None

  • -
  • features (Optional[ft.ColumnsList], optional) – names or indices of columns to be used as features, by default None. -If None, all columns except the last one will be used. -If target argument is not None, features should be passed explicitly as well

  • -
  • target (Optional[Union[str, int]], optional) – name or index of column to be used as target, by default None. -If None, the last column will be used as target. -If features argument is not None, target should be specified explicitly as well

  • -
-
-
-
- -
-
-_create_pipeline(pipeline: Optional[Type[Pipeline]], options: Optional[Dict]) None
-

Initializes the pipeline.

-
-
Parameters
-
    -
  • pipeline (Optional[Type[Pipeline]]) – pipeline class

  • -
  • options (Optional[Dict]) – pipeline options

  • -
-
-
-
- -
-
-_prepare_data(data: Union[str, ndarray[Any, dtype[ScalarType]], DataFrame, Tuple], training: bool = True) Tuple[ndarray[Any, dtype[ScalarType]], ndarray[Any, dtype[ScalarType]], List[ColumnTypes]]
-

Initial data preparation: -1) optional: read data from the specified location; -2) split into features and targets. By default it is assumed that the last column is the target; -3) clean data; -4) determine numerical and categorical features (create categorical mask).

-
-
Parameters
-

data (Union[str, npt.NDArray, pd.DataFrame, Tuple]) – path to data file or pandas dataframe or numpy array or Tuple(X,y)

-
-
Returns
-

tuple of features, target and type mask for features

-
-
Return type
-

Tuple[npt.NDArray, npt.NDArray, List[ColumnTypes]]

-
-
-
- -
-
-property default_pipeline: Type[Pipeline]
-

Default pipeline class.

-
- -
-
-property default_pipeline_options: Dict
-

Default options for pipeline.

-
- -
-
-evaluate(test_data: Union[str, ndarray[Any, dtype[ScalarType]], DataFrame, Tuple], silent: bool = False) Dict
-

Perfoms and prints the evaluation report on the given dataset.

-
-
Parameters
-
    -
  • test_data (Union[str, npt.NDArray, pd.DataFrame, Tuple]) – dataset to be used for evaluation

  • -
  • silent (bool) – controls whether the metrics are printed on screen, by default False

  • -
-
-
-
- -
-
-performance_summary(test_data: Optional[Union[str, ndarray[Any, dtype[ScalarType]], DataFrame, Tuple]]) dict
-

Prints a performance summary of the model. -The summary always includes metrics calculated for the train set. -If the train/eval split was done during training, the summary includes metrics calculated on eval set. -If test set is provided as an argument, the performance includes metrics calculated on test set.

-
-
Parameters
-

test_data (Optional[Union[str, npt.NDArray, pd.DataFrame, Tuple]]) – data to be used as test set, by default None

-
-
Returns
-

metrics for each subset

-
-
Return type
-

dict

-
-
-
- -
-
-predict(data: Union[str, ndarray[Any, dtype[ScalarType]], DataFrame]) ndarray[Any, dtype[ScalarType]]
-

Performs prediction on new data.

-
-
Parameters
-

data (Union[str, npt.NDArray, pd.DataFrame]) – path to data file or pandas dataframe or numpy array

-
-
Returns
-

predictions

-
-
Return type
-

npt.NDArray

-
-
-
- -
-
-predict_stored_subset(subset: str = 'train') ndarray[Any, dtype[ScalarType]]
-

Makes a prediction on a stored subset (train or eval).

-
-
Parameters
-

subset (str, optional) – subset to predict on (train or eval), by default ‘train’

-
-
Returns
-

predicted values

-
-
Return type
-

npt.NDArray

-
-
-
- -
-
-save_model(filename: Optional[str] = None, **kwargs: Any) ModelProto
-

Serializes and saves the model.

-
-
Parameters
-

filename (Optional[str], optional) – filename for the model file, by default None. If filename is not specified, the model is not saved on disk and only returned as bytes object

-
-
Returns
-

ONNX ModelProto of the model

-
-
Return type
-

ModelProto

-
-
-
- -
-
-train(make_eval_subset: bool = True, pre_eval: bool = False, **kwargs: Any) TabularTaskManager
-

Invokes the training procedure of an underlying pipeline. Print an expected model performance if available.

-
-
Parameters
-
    -
  • pre_eval (bool) – if True, first estimate model perfromance via 10 folds CV for small datasets or 25% test split for large datasets, by default False. -Setting pre_eval = True is not reccomended as it pre-evaluates the pipeline as a whole which has lots of random elements therefore the results might be non reproducable

  • -
  • make_eval_subset (bool) – controls whether a dedicated eval set should be allocated for performance report, by default True. -If True, overwrites the value of pre_eval to False

  • -
-
-
Returns
-

self

-
-
Return type
-

TabularTaskManager

-
-
-
- -
- -
- - -
-
- -
-
-
-
- - - - \ No newline at end of file diff --git a/docs/guide.md b/docs/guide.md new file mode 100644 index 0000000..1e34c8c --- /dev/null +++ b/docs/guide.md @@ -0,0 +1,341 @@ +# Falcon + +Falcon trains tabular models and exports them to a self-contained [FNNX](https://github.com/BeastByteAI/FNNX) bundle. The bundle carries the full pipeline, so inference does not depend on the training environment. + +## Installation + +```bash +pip install falcon-ml +``` + +The base install covers training and export with scikit-learn estimators. Three optional extras add the rest: + +```bash +pip install "falcon-ml[runtime]" # load and run exported .fnnx models +pip install "falcon-ml[gbdt]" # LightGBM, XGBoost and CatBoost candidates +pip install "falcon-ml[hpo]" # Optuna-based hyperparameter search +``` + +[Compiling a model to C](#compiling-to-c) additionally needs the FNNX ahead-of-time compiler, which is not a Falcon extra: + +```bash +pip install "fnnx[compiler]" +``` + +Falcon requires Python 3.10 or newer. + +## Training a model + +`AutoML` reads a dataset, infers the column types, trains a model and writes an `.fnnx` file: + +```python +from falcon import AutoML + +predictor = AutoML(task="tabular_classification", train_data="titanic.csv") +``` + +The task is either `tabular_classification` or `tabular_regression`. The training data can be a path to a `.csv` or `.parquet` file, a pandas `DataFrame`, a numpy array, or an `(X, y)` tuple. Without explicit `features` and `target`, the last column becomes the target and the rest become features. + +```python +predictor = AutoML( + task="tabular_classification", + train_data=df, + test_data=(X_test, y_test), + features=["sex", "class", "age"], + target="survived", +) +``` + +Passing `test_data` changes how the training data is used. With a test set, every training row is used for fitting and the test set is scored. Without one, the score comes from the training data itself, by cross-validation or by a holdout depending on the size of the dataset. [Evaluation and grouped data](#evaluation-and-grouped-data) covers that choice and how to override it. + +The saved file is named `falcon__.fnnx`, and `save_model=False` skips writing it. + +## What a run does + +Every entry point runs the same steps. Falcon reads the data and infers a type for each feature column, then builds a preprocessing pipeline from those types. It trains a list of candidates one after another. A candidate is one estimator with one fixed set of hyperparameters, such as a random forest with 300 trees. + +Each candidate is scored on rows it did not see while fitting, using out-of-fold predictions. Falcon then builds a weighted ensemble from the candidates, adding one at a time and keeping an addition only while the score improves. + +Turning ensembling off changes only what happens after scoring. Every candidate is still trained and scored. Instead of a blend, the best-scoring one is refit on all the training rows and becomes the model. Either way, the model, the preprocessing and the label decoding are exported together as one file. + +*Note: an out-of-fold prediction for a row comes from a copy of the model trained without that row. Falcon splits the data into `oof_folds` parts and rotates which part is held out, which yields one such prediction per row. Scores and ensemble weights computed this way are comparable across candidates without setting a validation set aside.* + +## How the data is read + +Feature columns are typed by inspection, and the type decides both the encoding and how a missing value is filled: + +| Type | Detected when | Encoded as | Missing values filled with | +|---|---|---|---| +| numeric | numeric, more than ten distinct values | scaled | training median, plus an indicator column | +| categorical | numeric with ten or fewer distinct values, or any unmatched non-numeric column | one-hot up to 100 distinct values, target encoding above | sentinel category | +| text | non-numeric, detected as free text | TF-IDF, reduced with SVD | sentinel category | +| date / datetime | matches an ISO 8601 date or datetime pattern | split into components | reference value, plus a marker column | + +Non-numeric columns are matched against the date and datetime patterns first, then checked for free text, and otherwise fall through to categorical. + +*Note: inference runs on the data you pass, and it cannot tell an identifier from a measurement. A numeric ID with few distinct values is read as a category, and one with many distinct values is target-encoded. Drop such columns from `features` if that is not what you want.* + +*Note: one-hot encoding gives every category a column of its own, which stops being practical at hundreds of categories. Target encoding instead replaces the category with a number derived from the target values seen for it in training, so one column stays one column. Falcon cross-fits that number, so a row never contributes to its own encoded value.* + +Rows with a missing target are dropped before training and the count is logged. Missing feature values are kept and filled inside the pipeline. The filling is part of the exported graph, so a filled value is the same whether it comes from `Predictor.predict` or from the runtime. + +## Controlling the run + +`AutoML` is a wrapper around `Predictor`, which exposes the same run as separate steps: + +```python +from falcon import Predictor + +predictor = Predictor("tabular_classification", preset="best", time_limit=600) +predictor.fit(df, features=["sex", "class", "age"], target="survived") + +predictions = predictor.predict(unlabeled_df) +probabilities = predictor.predict_proba(unlabeled_df) +metrics = predictor.evaluate(test_df) +predictor.save("model.fnnx") +``` + +`predict_proba` is available for classification only. `save()` returns the serialized bundle as `bytes`, and writes it to disk when given a path. + +Two methods report on the run. `leaderboard()` returns a `DataFrame` with one row per trained candidate, holding its family, score, fit time and weight in the ensemble. `feature_importance(n_repeats=10)` shuffles one column at a time and measures how much the score drops, which tells you how much the model relies on that column. It scores with balanced accuracy for classification and R² for regression, and returns the features ordered by importance. + +## Presets and RunConfig + +Three presets ship with the library: + +| Preset | Candidates | Ensembling | `oof_folds` | Plateau stop | +|---|---|---|---|---| +| `fast` | 1 | off | 2 | off | +| `balanced` (default) | up to 4 | on, up to 50 additions | 5 | on, patience 2 | +| `best` | full portfolio | on, up to 100 additions | 10 | off | + +```python +predictor = Predictor("tabular_regression", preset="fast") +``` + +Every setting a preset controls is a field on `RunConfig`. Passing a `RunConfig` overrides only the fields you set explicitly, and the rest keep their preset values. + +```python +from falcon import Predictor, RunConfig + +config = RunConfig(oof_folds=10, calibrate=True, time_limit=1800) +predictor = Predictor("tabular_classification", preset="best", config=config) +``` + +| Field | Default | Meaning | +|---|---|---| +| `candidate_sources` | `(PortfolioSource(),)` | where candidates come from | +| `ensemble_enabled` | `True` | blend candidates; when off, the best-scoring one is refit and kept | +| `ensemble_max_iterations` | `100` | most candidates the ensemble may add | +| `plateau_enabled` | `True` | stop adding once the score stops improving | +| `plateau_patience` | `3` | additions without improvement before stopping | +| `plateau_tolerance` | `1e-4` | improvement below this counts as none | +| `oof_folds` | `5` | parts the data is split into for out-of-fold predictions | +| `eval_strategy` | `"auto"` | how the reported score is computed | +| `time_limit` | `None` | budget in seconds for the whole run | +| `random_state` | `42` | seed for splits, candidates and search | +| `dataset_aware_ordering` | `False` | try candidates in an order picked from dataset statistics | +| `calibrate` | `False` | correct overconfident probabilities (classification) | +| `conformal_alpha` | `None` | target miss rate for prediction intervals (regression) | +| `impute_missing` | `True` | fill missing values inside the pipeline | +| `class_weight` | `"none"` | reweight training rows by inverse class frequency (classification) | +| `decision_metric` | `"balanced_accuracy"` | metric the tuned decision rule maximises, or `None` for plain argmax | + +`time_limit`, `random_state` and `eval_strategy` can also be passed directly to `AutoML` and `Predictor`, where they take precedence over both the preset and the config. + +## Choosing candidate models + +Falcon asks each source in `candidate_sources` for a list of candidates, then trains and ensembles the pooled result. The default source, `PortfolioSource`, hands back a built-in list covering linear models, random forests, extra trees and histogram gradient boosting, interleaved with LightGBM, XGBoost and CatBoost when those libraries are installed. + +The cost of a run scales with the number of candidates whether ensembling is on or not. `max_candidates` shortens the built-in list for a cheaper run, and `specs` replaces the list outright: + +```python +from falcon import Predictor, RunConfig +from falcon.config import PortfolioSource +from falcon.tabular.candidates import EstimatorSpec + +source = PortfolioSource( + specs=( + EstimatorSpec("rf", "random_forest", {"n_estimators": 300}), + EstimatorSpec("gbm", "lightgbm", {"num_leaves": 63}, min_rows=1000), + ) +) +predictor = Predictor("tabular_classification", config=RunConfig(candidate_sources=(source,))) +``` + +An `EstimatorSpec` names a family and the parameters to build it with. `min_rows`, `max_rows` and `max_features` decide whether it applies to the dataset at hand, so a spec meant for large data is skipped on a small one instead of trained. + +`HPOSource` runs an Optuna study over one family and returns its best trials as candidates: + +```python +from falcon.config import HPOSource, PortfolioSource, RunConfig + +config = RunConfig( + candidate_sources=( + PortfolioSource(max_candidates=3), + HPOSource(family="lightgbm", n_trials=40, top_n=3), + ) +) +``` + +Sources run in order and their candidates go into one pool, so a fixed portfolio and a search can feed the same ensemble. `time_budget_fraction` caps the share of the remaining time limit the study may spend, and defaults to a quarter. + +*Note: `HPOSource` requires the `hpo` extra, and GBDT families require the `gbdt` extra. A family whose library is missing is dropped from the default portfolio with a log message, but naming it explicitly in an `EstimatorSpec` raises an `ImportError`.* + +## Evaluation and grouped data + +`eval_strategy` decides how the reported score is produced. Under `"auto"`, datasets below 2,500 rows are scored with cross-validation and larger ones with a 25% holdout. `"holdout"` and `"cv"` force either choice. `None` skips evaluation and fits on every row, which is what happens when you pass `test_data` to `AutoML`. A scikit-learn cross-validator or a callable returning train and test indices can be passed instead of a name. + +When rows are not independent, `group_by` keeps a group on one side of every split: + +```python +predictor.fit( + df, + features=["customer_id", "sex", "age"], + target="churn", + group_by="customer_id", +) +``` + +`group_by` accepts a column name, several column names, or an array of group labels aligned with the rows of the input data. A named column has to be one of the selected `features`. To group on a column the model should not see, pass its values as an array instead. Grouping applies to the evaluation split and to the out-of-fold folds, which the calibration and interval settings below also rely on. + +*Note: without `group_by`, rows are grouped by their full feature vector. Duplicate rows therefore stay on the same side of a split, which keeps an exact copy of a training row out of the evaluation set.* + +## Calibrated probabilities + +A classifier can rank cases correctly and still report probabilities that are too confident. Of all the cases a model calls 90% likely, about 90% should actually turn out positive. Calibration closes that gap. + +`calibrate=True` fits a temperature on the out-of-fold predictions. The temperature is a single number that divides the model's scores before they become probabilities, which makes every probability softer or sharper by the same factor. Falcon bakes it into the exported graph, so `predict_proba` returns calibrated values both natively and through the runtime. Temperature alone preserves the ordering of the scores, so on its own it moves the probabilities and not the labels. With the tuned decision rule below it can move labels too, because the rule reads the calibrated scores and a weighted comparison between three or more classes is not preserved by a temperature. Regression models reject the setting. + +## Imbalanced classes + +When one class is much rarer than the others, a model trained to predict the most likely class will rarely predict the rare one. Falcon addresses this at the decision, not at the probabilities. + +`decision_metric` fits one weight per class on the out-of-fold predictions and picks the label at `argmax(p * w)` instead of `argmax(p)`. In a two-class problem that is exactly a tuned threshold; with more classes it is a per-class tilt. The weights go into the exported graph as two nodes, so native and runtime predictions agree. The default, `"balanced_accuracy"`, matches the score Falcon reports. `"f1"` and `"mcc"` are also accepted, and `None` turns the rule off and restores plain argmax. `"f1"` is macro-averaged on two classes as well as on more, because Falcon encodes labels alphabetically and neither class of a two-class target is inherently the positive one. Falcon leaves the weights at one when the rarest class has fewer than 50 out-of-fold rows, or when no weighting beats plain argmax on the metric. + +The rule changes labels only. `predict_proba` returns the same numbers with the rule on or off, which means code that thresholds `predict_proba` at 0.5 itself bypasses the tuned rule entirely and keeps the untuned decision. + +`class_weight="balanced"` is the other lever, and it works the other way around: it reweights the training rows by inverse class frequency, which shifts the probabilities themselves rather than the decision taken from them. It costs log loss and calibration quality and largely duplicates what the decision rule already does, so it stays off by default. + +*Note: optimising balanced accuracy trades against plain accuracy, since predicting the rare class more often costs errors on the common one. `evaluate()` reports both.* + +## Prediction intervals + +A regression model returns one number per row. `conformal_alpha` adds a lower and an upper bound around it. + +Falcon measures how far the out-of-fold predictions land from the true values, then takes the quantile of those distances that `alpha` implies. That distance becomes a fixed margin added on both sides of every prediction. With `alpha=0.1`, roughly 90% of future rows should fall inside their interval. The exported model then has three outputs instead of one: `y_pred`, `y_lower` and `y_upper`. + +```python +from falcon import Predictor, RunConfig +from falcon.runtime import Runtime + +predictor = Predictor("tabular_regression", config=RunConfig(conformal_alpha=0.1)) +predictor.fit(df, target="charges") +predictor.save("model.fnnx") + +lower, upper = Runtime("model.fnnx").predict_interval(X) +``` + +The 90% holds across rows on average, not for any single row, and only while new data resembles the data the margin was measured on. Every interval has the same width, so a row the model finds hard is not given a wider one. + +## Export and inference + +`save()` writes a single `.fnnx` file containing the preprocessing, the model or ensemble, and the label decoding. The `runtime` extra provides a thin wrapper for loading it: + +```python +from falcon.runtime import Runtime + +runtime = Runtime("model.fnnx") +predictions = runtime.predict(unlabeled_df) +probabilities = runtime.predict_proba(unlabeled_df) +``` + +The runtime accepts a `DataFrame`, a numpy array, or a dict of column arrays. Columns have to arrive in the same order and with the same types as during training. This is assumed rather than checked, so a reordered frame produces wrong numbers instead of an error. Classification models return decoded labels, matching what `Predictor.predict` returns. + +## Models without inference-time imputation + +Not every deployment target accepts a graph whose data path depends on the values flowing through it. Filling a missing value at inference time creates such a dependency, because it means choosing between two values per row. In ONNX that is a `Where` node, fed by an `IsNaN` test for numeric columns and by `Equal`/`Or` comparisons against four missing tokens for string columns. The data path through the graph then depends on the values flowing through it. + +`impute_missing=False` builds the pipeline without that handling: + +```python +from falcon import AutoML, RunConfig + +AutoML(task="tabular_classification", train_data=df, config=RunConfig(impute_missing=False)) +``` + +Numeric columns are then cast to `float32` and nothing else, which drops both the choice and the indicator column. There is no way to fill a missing number without that choice, so Falcon refuses at fit time and names the column. Categorical and text columns keep their plain string form, which turns a missing value into an ordinary category (`"nan"`, `"None"`, and so on) instead of a shared sentinel. + +Date and datetime features are rejected in this mode. Their tokenizer fills missing values internally and needs the same choice to do it, so Falcon fails with an error listing the offending columns rather than quietly emitting one. + +The resulting graph holds no `Where`, `IsNaN`, `Equal`, `Or`, `If`, `Loop` or `Scan` nodes for numeric, categorical and text features. In exchange, missing values at inference no longer have defined behavior. A missing number travels through the model as `NaN`, and a missing category is treated as one the model never saw. + +## Compiling to C + +An exported model can also be compiled to C source instead of being loaded through the runtime. The generated code carries the whole pipeline, from the scaling and encoding through to every tree of the ensemble, and needs no runtime, no allocation and no ONNX at inference time. `compile_to_c` reads a `.fnnx` file and writes the C into a directory you name: + +```python +from falcon.codegen import compile_to_c + +compile_to_c("model.fnnx", "out/", prefix="charges", batch_size=32) +``` + +Three files land in the output directory. `charges.h` holds the model as straight-line C, `charges_falcon.h` holds the string tables the next section covers, and `charges_report.json` describes the artifact for tooling. Both headers follow the single-header convention: include them anywhere, and define the implementation macro in exactly one translation unit. + +```c +#define CHARGES_IMPLEMENTATION +#include "charges.h" + +#define CHARGES_FALCON_IMPLEMENTATION +#include "charges_falcon.h" + +cat_color[0] = charges_encode_cat_color("red"); +charges_run(rows, num_a, num_b, cat_color, cat_size, y_pred); +``` + +`batch_size` is the largest number of rows one call may pass, and it fixes the size of the buffers the artifact reserves. A call can always pass fewer rows. + +Generating C requires FNNX's ahead-of-time compiler, the `fnnx.extras.compilers.c` module that `pip install "fnnx[compiler]"` brings in. Without it the call raises `CodegenError` saying as much. + +## Categories and labels in compiled C + +C has no string tensor, so the two places Falcon puts one have to be resolved before the graph is compiled. Categorical features arrive as int64 category codes rather than strings, and a classifier returns the predicted class as an int64 index rather than a label. This happens during code generation, not during export: the `.fnnx` file is not modified and keeps its string interface for every other consumer. + +The mapping is not lost. It moves into `_falcon.h`. Each categorical feature gets its vocabulary as a table and a lookup that returns the code for a string, and a classifier gets its class labels and a lookup that returns the label for an index. The header also documents the argument order of `_run()` and what each argument holds. + +```c +int64_t charges_encode_cat_color(const char* value); +const char* charges_class_label(int64_t index); +``` + +A value in no category encodes to `-1`, which the model treats the way it treats any category it did not see during training: as an all-zero encoding, not as an error. + +Imputation needs no special handling. When the model was trained with `impute_missing=True`, the generated lookup maps `NULL` and the missing tokens Falcon recognizes to the sentinel category the pipeline fills with, so a missing value reaches the model as the same category it would have in Python. Numeric imputation compiles as it stands, `IsNaN` and `Where` included. + +*Note: text and date features cannot be compiled. Both are encoded by splitting and parsing the string itself rather than by looking the whole value up, so no integer code stands in for one. `compile_to_c` raises `CodegenError` naming the offending columns. Drop them from `features`, or deploy that model through the FNNX runtime instead.* + +## The scikit-learn API + +`FalconTabularClassifier` and `FalconTabularRegressor` wrap `Predictor` behind the estimator interface, which lets Falcon sit inside scikit-learn tooling that expects `fit`/`predict`: + +```python +from falcon.sklapi import FalconTabularClassifier + +model = FalconTabularClassifier(preset="balanced") +model.fit(X_train, y_train, group_by="customer_id") +model.predict(X_test) +model.save_model("model.fnnx") +``` + +`preset` takes either a preset name or a `RunConfig`. `FalconClassifier` and `FalconRegressor` are aliases for the same classes. + +## Demo datasets + +Two datasets are bundled for trying the library out. `load_churn_dataset` suits classification and `load_insurance_dataset` suits regression. Both take `mode="training"` for a labelled `DataFrame` and `mode="inference"` for an unlabelled array. + +```python +from falcon import AutoML +from falcon.datasets import load_churn_dataset + +AutoML(task="tabular_classification", train_data=load_churn_dataset()) +``` diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index dc1312a..0000000 --- a/docs/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=source -set BUILDDIR=build - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.https://www.sphinx-doc.org/ - exit /b 1 -) - -if "%1" == "" goto help - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd diff --git a/docs/source/_static/customStyles.css b/docs/source/_static/customStyles.css deleted file mode 100644 index 45063a6..0000000 --- a/docs/source/_static/customStyles.css +++ /dev/null @@ -1,5 +0,0 @@ -@import url("css/theme.css"); - -.wy-nav-content { - max-width: 90%; -} diff --git a/docs/source/abstract/index.rst b/docs/source/abstract/index.rst deleted file mode 100644 index ca5fe09..0000000 --- a/docs/source/abstract/index.rst +++ /dev/null @@ -1,28 +0,0 @@ -Abstract -=================== - -.. currentmodule:: falcon.abstract - -.. autosummary:: - - TaskManager - Model - Pipeline - PipelineElement - Learner - Processor - ONNXConvertible - OptunaMixin - - -.. toctree:: - :hidden: - - task_manager - model - pipeline - pipeline_element - learner - processor - onnx - optuna \ No newline at end of file diff --git a/docs/source/abstract/learner.rst b/docs/source/abstract/learner.rst deleted file mode 100644 index f3c528c..0000000 --- a/docs/source/abstract/learner.rst +++ /dev/null @@ -1,8 +0,0 @@ -Learner -========================= - -.. autoclass:: falcon.abstract.Learner - :members: - :private-members: - :inherited-members: - :special-members: __init__ \ No newline at end of file diff --git a/docs/source/abstract/model.rst b/docs/source/abstract/model.rst deleted file mode 100644 index 8bf6a24..0000000 --- a/docs/source/abstract/model.rst +++ /dev/null @@ -1,8 +0,0 @@ -Model -========================= - -.. autoclass:: falcon.abstract.Model - :members: - :private-members: - :inherited-members: - :special-members: __init__ \ No newline at end of file diff --git a/docs/source/abstract/onnx.rst b/docs/source/abstract/onnx.rst deleted file mode 100644 index 5a39450..0000000 --- a/docs/source/abstract/onnx.rst +++ /dev/null @@ -1,8 +0,0 @@ -ONNXConvertible -========================= - -.. autoclass:: falcon.abstract.ONNXConvertible - :members: - :private-members: - :inherited-members: - :special-members: __init__ \ No newline at end of file diff --git a/docs/source/abstract/optuna.rst b/docs/source/abstract/optuna.rst deleted file mode 100644 index 7120332..0000000 --- a/docs/source/abstract/optuna.rst +++ /dev/null @@ -1,8 +0,0 @@ -OptunaMixin -========================= - -.. autoclass:: falcon.abstract.OptunaMixin - :members: - :private-members: - :inherited-members: - :special-members: __init__ \ No newline at end of file diff --git a/docs/source/abstract/pipeline.rst b/docs/source/abstract/pipeline.rst deleted file mode 100644 index daa4baf..0000000 --- a/docs/source/abstract/pipeline.rst +++ /dev/null @@ -1,8 +0,0 @@ -Pipeline -========================= - -.. autoclass:: falcon.abstract.Pipeline - :members: - :private-members: - :inherited-members: - :special-members: __init__ \ No newline at end of file diff --git a/docs/source/abstract/pipeline_element.rst b/docs/source/abstract/pipeline_element.rst deleted file mode 100644 index 0d79dcf..0000000 --- a/docs/source/abstract/pipeline_element.rst +++ /dev/null @@ -1,8 +0,0 @@ -PipelineElement -========================= - -.. autoclass:: falcon.abstract.PipelineElement - :members: - :private-members: - :inherited-members: - :special-members: __init__ \ No newline at end of file diff --git a/docs/source/abstract/processor.rst b/docs/source/abstract/processor.rst deleted file mode 100644 index 6eabcdf..0000000 --- a/docs/source/abstract/processor.rst +++ /dev/null @@ -1,8 +0,0 @@ -Processor -========================= - -.. autoclass:: falcon.abstract.Processor - :members: - :private-members: - :inherited-members: - :special-members: __init__ \ No newline at end of file diff --git a/docs/source/abstract/task_manager.rst b/docs/source/abstract/task_manager.rst deleted file mode 100644 index 04dcd22..0000000 --- a/docs/source/abstract/task_manager.rst +++ /dev/null @@ -1,8 +0,0 @@ -TaskManager -========================= - -.. autoclass:: falcon.abstract.TaskManager - :members: - :private-members: - :inherited-members: - :special-members: __init__ \ No newline at end of file diff --git a/docs/source/api.rst b/docs/source/api.rst deleted file mode 100644 index 529db7b..0000000 --- a/docs/source/api.rst +++ /dev/null @@ -1,10 +0,0 @@ -API reference -================================== - -.. toctree:: - high_level_api - sklearn_api - abstract/index - tabular/index - registry - available_configurations \ No newline at end of file diff --git a/docs/source/available_configurations.rst b/docs/source/available_configurations.rst deleted file mode 100644 index 12c6841..0000000 --- a/docs/source/available_configurations.rst +++ /dev/null @@ -1,70 +0,0 @@ -Available Configurations -============================== - -The tables below list both main and additional configurations that can be used. -Additional configurations should be used with caution as they may not be suitable for certain datasets. It is reccomended to always choose one of the main configurations. - -*********************************************************************************** -Configurations for tabular_regression/tabular_classification tasks -*********************************************************************************** - -.. list-table:: - :width: 100% - :widths: 18 12 70 - :header-rows: 1 - - * - Name - - Extension - - Description - * - SuperLearner - - -- - - | Uses :doc:`tabular/learners/super_learner` to build a stacking ensemble of base estimators. - | SuperLearner combines multiple individual estimators to make predictions with greater accuracy than any of the individual estimators alone. - | Additionaly, it learns to weigh the predictions of each individual model, optimizing the combination to maximize performance on the given task. - | SuperLearner is more suitable for smaller datasets, but the produced models tend to be relatively large. - * - OptunaLearner - - -- - - | Uses :doc:`tabular/learners/optuna_learner`. - | It builds a model and optimizes its hyperparameters using Optuna framework; :doc:`tabular/models/hgbt_clf`/:doc:`tabular/models/hgbt_regr` is used as a default model. - | Since OptunaLearner focuses on finetuning a single model, the produced model is not very large in size, but the optimization procedure can be very long. - * - PlainLearner - - -- - - | Uses :doc:`tabular/learners/plain_learner`. - | It builds a model using default hyperparameters; :doc:`tabular/models/hgbt_clf`/:doc:`tabular/models/hgbt_regr` is used as a default model. - | PlainLearner is very fast, thus it is a good choice for building initial baselines or automizing preprocessing steps. - -.. dropdown:: Additional configurations - - .. list-table:: - :width: 100% - :widths: 18 12 70 - :header-rows: 1 - - * - Name - - Extension - - Description - * - SuperLearner.mini - - -- - - | Uses :doc:`tabular/learners/super_learner` with a config for small datasets. - | The dataset is considered small when the number of cells after preprocessing [n_rows*n_columns] is < 80k. - * - SuperLearner.mid - - -- - - | Uses :doc:`tabular/learners/super_learner` with a config for mid datasets. - | The dataset is considered small when the number of cells after preprocessing [n_rows*n_columns] is < 4kk. - * - SuperLearner.large - - -- - - | Uses :doc:`tabular/learners/super_learner` with a config for large datasets. - | The dataset is considered small when the number of cells after preprocessing [n_rows*n_columns] is < 16kk. - * - SuperLearner.xlarge - - -- - - | Uses :doc:`tabular/learners/super_learner` with a config for x-large datasets. - | The dataset is considered small when the number of cells after preprocessing [n_rows*n_columns] is >= 16kk. - * - OptunaLearner.hgbt - - -- - - | Uses :doc:`tabular/learners/optuna_learner`. - | It builds a :doc:`tabular/models/hgbt_clf`/:doc:`tabular/models/hgbt_regr` model with hyperparameters optimized by Optuna framework. - * - PlainLearner.hgbt - - -- - - | Uses :doc:`tabular/learners/plain_learner`. - | It builds a :doc:`tabular/models/hgbt_clf`/:doc:`tabular/models/hgbt_regr` model with default hyperparameters. - diff --git a/docs/source/conf.py b/docs/source/conf.py deleted file mode 100644 index 5a3c391..0000000 --- a/docs/source/conf.py +++ /dev/null @@ -1,42 +0,0 @@ -# Configuration file for the Sphinx documentation builder. -# -# For the full list of built-in configuration values, see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - -# -- Project information ----------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information - -import sys -from os.path import dirname, abspath - -d = dirname(dirname(dirname(abspath(__file__)))) -print(d) -sys.path.append(d) - -project = "Falcon" -copyright = "2022, Oleg Kostromin, Marco Pasini, Iryna Kondrashchenko" -author = "Oleg Kostromin, Marco Pasini, Iryna Kondrashchenko" - -# -- General configuration --------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration - -extensions = [ - "sphinx.ext.napoleon", - "sphinx.ext.duration", - "sphinx.ext.doctest", - "sphinx.ext.autodoc", - "sphinx.ext.autosummary", - "sphinx_design" -] -napoleon_numpy_docstring = True -templates_path = ["_templates"] -exclude_patterns = [] - - -# -- Options for HTML output ------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output - -html_theme = "sphinx_rtd_theme" -html_style = "customStyles.css" -html_static_path = ["_static"] -html_theme_options = {"navigation_depth": 4} diff --git a/docs/source/high_level_api.rst b/docs/source/high_level_api.rst deleted file mode 100644 index 15b97fe..0000000 --- a/docs/source/high_level_api.rst +++ /dev/null @@ -1,8 +0,0 @@ -High level API -======================== - -.. autofunction:: falcon.AutoML - -.. autofunction:: falcon.initialize - -.. autofunction:: falcon.run_model \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst deleted file mode 100644 index e83a8b3..0000000 --- a/docs/source/index.rst +++ /dev/null @@ -1,18 +0,0 @@ -.. Falcon documentation master file, created by - sphinx-quickstart on Thu Sep 1 12:30:41 2022. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to Falcon's documentation! -================================== - -.. toctree:: - intro - api - -.. Indices and tables -.. ================== - -.. * :ref:`genindex` -.. * :ref:`modindex` -.. * :ref:`search` diff --git a/docs/source/intro.rst b/docs/source/intro.rst deleted file mode 100644 index cd07cfb..0000000 --- a/docs/source/intro.rst +++ /dev/null @@ -1,193 +0,0 @@ -Getting started -================================== - -**Train a powerful Machine Learning model in a single line of code with Falcon!** - -Falcon is a simple and lightweight AutoML library designed for people who want to train a model on a custom dataset in an instant even without specific data-science knowledge. Simply give Falcon your dataset and specify which feature you want the ML model to predict. Falcon will do the rest! - -Falcon allows the trained models to be immediately used in production by saving them in the widely used ONNX format. No need to write custom code to save complicated models to ONNX anymore! - -Installation -=================== - -Stable release from `PyPi `_ - -.. code-block:: bash - - pip install falcon-ml - -Latest version from `GitHub `_ - -.. code-block:: bash - - pip install git+https://github.com/OKUA1/falcon - -Installing some of the dependencies on **Apple Silicon Macs** might not work, the workaround is to create an X86 environment using `Conda `_ - -.. code-block:: bash - - conda create -n falcon_env - conda activate falcon_env - conda config --env --set subdir osx-64 - conda install python=3.9 - pip3 install falcon-ml - -Usage -================== - -Currently, Falcon supports only tabular datasets and two tasks: 'tabular_classification' and 'tabular_regression'. - -The easiest way to use the library is by using the highest level API as shown below: - -.. code-block:: python - - from falcon import AutoML - - AutoML(task = 'tabular_classification', train_data = 'titanic.csv') - - -This single line of code will read and prepare the dataset, scale/encode the features, encode the labels, train the model and save it as ONNX file for future inference. - -Additionally, it is also possible to explicitly specify the feature/target columns (otherwise the last column will be used as target and all other as features) and test data (otherwise 25% of training set will be kept) for evaluation report. - -.. code-block:: python - - from falcon import AutoML - - manager = AutoML( - task="tabular_classification", - train_data=df, - test_data=(X_test, y_test), - features=["sex", "gender", "class", "age"], - target="survived", - ) - - -It is also possible to provide train/test data as a pandas dataframe, numpy array, or tuple containing X and y. In order to do that, simply pass the required object as an argument. This might be relevant in cases when custom data preparation is needed or data itself comes from non-conventional source. - -.. code-block:: python - - from falcon import AutoML - import pandas as pd - - df = pd.read_csv('titanic.csv') - X_test = pd.read_csv('X_test.csv') - y_test = pd.read_csv('y_test.csv') - - manager = AutoML( - task="tabular_classification", - train_data=df, - test_data=(X_test, y_test), - features=["sex", "gender", "class", "age"], - target="survived", - ) - - -While AutoML function enables extremely fast experimentation, it does not provide enough control over the training steps and might be not flexible enough for more advanced users. As an alternative, it is possible to use the relevant TaskManager class either directly or by using :code:`initialize` helper function. - -.. code-block:: python - - from falcon import initialize - import pandas as pd - - test_df = pd.read_csv('titanic_test.csv') - - manager = initialize(task='tabular_classification', data='titanic.csv') - manager.train() - manager.performance_summary(test_df) - - -When using :code:`initialize` function it is also possible to provide a custom configuration or even a custom pipeline. For more details please check the API reference section. - -Demo datasets -================== - -You can try out falcon using one of the built-in demo datasets. - -.. code-block:: python - - from falcon import AutoML - # churn -> classification; insurance -> regression - from falcon.datasets import load_churn_dataset, load_insurance_dataset - - df = load_churn_dataset() - - AutoML(task = 'tabular_classification', train_data = df) - -Making predictions with trained models -============================================ - -There are 2 ways to make a prediction using a trained model. If the input/unlabeled data is available right away, the same manager object that was used for training the model can be used. -An important thing to notice is that the input data should have the same structure as the training set (the same number, order and type of the features). This is assumed by the model, but not explicitly checked during runtime. -The recommended approach is to provide the data as a numpy array. - -.. code-block:: python - - from falcon import AutoML - import pandas as pd - - df = pd.read_csv('training_data.csv') - manager = AutoML(task = 'tabular_classification', train_data = df) - - unlabeled_data = pd.read_csv('unlabeled_data.csv').to_numpy() - predictions = manager.predict(unlabeled_data) - print(predictions) - -While this solution is straight-forward, in real-world applications the new/unlabeled data is not always available right away. Therefore, it is desirable to train a model and reuse it in the future. - -One of the key features of falcon is native `ONNX `_ support. ONNX (Open Neural Network Exchange) is an open standard for representing machine learning algorithms. This means that once the model is exported to ONNX, it can be run on any platform with available ONNX implementation. -For example, `Microsoft ONNX Rutime (ORT) `_ is available for Python, C, C++, Java, JavaScript and multiple other languages which allows to run the model virtually everywhere. There are also alternative implementations, but there is a high chance they do not support all the required operators. - -In order to simplify the interaction with ONNX Runtime, falcon provides a `run_model` function that takes the path to the ONNX model, the input data as a numpy array and returns the predictions. - -.. code-block:: python - - from falcon import run_model - import pandas as pd - - unlabeled_data = pd.read_csv('unlabeled_data.csv').to_numpy() # ONLY NUMPY ARRAYS ARE ACCEPTED AS INPUT !!! - - predictions = run_model(model_path = "/path/to/model.onnx", X = unlabeled_data) - - print(predictions) - -Below is the complete example of model training and inference using the built-in datasets. - -.. code-block:: python - - ############################################ training.py ########################################################### - from falcon import AutoML - from falcon.datasets import load_churn_dataset - - df = load_churn_dataset(mode = "training") - AutoML(task = "tabular_classification", train_data = df) - # onnx model name will be printed after the training is done, use it instead of during infernce - - ############################################ inference.py ########################################################## - from falcon import run_model - from falcon.datasets import load_churn_dataset - - X = load_churn_dataset(mode = "inference") # for this example we are reusing training dataset but without labels - predictions = run_model(model_path = ".onnx", X = X) - print(predictions) - -Manually selecting a configuration -====================================== - -All of the examples in the previous sections demonstrated how to train falcon models using the default configuration. -However, there are several configurations available and it is easily possible to switch between them by providing a single additional argument. - -For tabular classification task, by default, falcon will use a :doc:`tabular/learners/super_learner` and the sub-configuration (e.g. list of base estimators) will be chosen automatically based on the dataset size. -But if we want to specify that a 'mini' sub-configuration of the learner is to be used, we can do it by adding `config = 'SuperLearner.mini'`. - -.. code-block:: python - - AutoML(task = "tabular_classification", train_data = df, config = 'SuperLearner.mini') # SuperLearner.mini config is used - -Similarly, instead of :doc:`tabular/learners/super_learner` which builds a stacking ensemble of base estimators, it is possible to use :doc:`tabular/learners/optuna_learner` which uses a single model and performs hyperparameter optimization using the Optuna framework. - -.. code-block:: python - - AutoML(task = "tabular_classification", train_data = df, config = 'OptunaLearner') # OptunaLearner config is used - -All the available configurations can be found :doc:`here`. \ No newline at end of file diff --git a/docs/source/logo.png b/docs/source/logo.png deleted file mode 100644 index c37d334..0000000 Binary files a/docs/source/logo.png and /dev/null differ diff --git a/docs/source/logo_cropped.png b/docs/source/logo_cropped.png deleted file mode 100644 index 8c18049..0000000 Binary files a/docs/source/logo_cropped.png and /dev/null differ diff --git a/docs/source/registry.rst b/docs/source/registry.rst deleted file mode 100644 index 18ac0c3..0000000 --- a/docs/source/registry.rst +++ /dev/null @@ -1,6 +0,0 @@ -Task Registry -======================== - -.. autoclass:: falcon.task_configurations.TaskConfigurationRegistry - :members: - diff --git a/docs/source/sklearn_api.rst b/docs/source/sklearn_api.rst deleted file mode 100644 index c3e25a6..0000000 --- a/docs/source/sklearn_api.rst +++ /dev/null @@ -1,12 +0,0 @@ -Scikit-learn API -======================== - -.. autoclass:: falcon.sklapi.FalconTabularClassifier - :members: - :inherited-members: - :special-members: __init__ - -.. autoclass:: falcon.sklapi.FalconTabularRegressor - :members: - :inherited-members: - :special-members: __init__ \ No newline at end of file diff --git a/docs/source/tabular/index.rst b/docs/source/tabular/index.rst deleted file mode 100644 index 92370fe..0000000 --- a/docs/source/tabular/index.rst +++ /dev/null @@ -1,37 +0,0 @@ -Tabular -=================== - -.. currentmodule:: falcon.tabular - -.. autosummary:: - - TabularTaskManager - pipelines.SimpleTabularPipeline - processors.ScalerAndEncoder - processors.MultiModalEncoder - processors.LabelDecoder - learners.SuperLearner - learners.OptunaLearner - learners.PlainLearner - models.HistGradientBoostingClassifier - models.HistGradientBoostingRegressor - models.StackingClassifier - models.StackingRegressor - - - -.. toctree:: - :hidden: - - tab_manager - pipelines/simple_pipeline - processors/scaler_and_encoder - processors/mm_encoder - processors/label_decoder - learners/super_learner - learners/optuna_learner - learners/plain_learner - models/hgbt_clf - models/hgbt_regr - models/stacking_clf - models/stacking_regr diff --git a/docs/source/tabular/learners/optuna_learner.rst b/docs/source/tabular/learners/optuna_learner.rst deleted file mode 100644 index f3a5395..0000000 --- a/docs/source/tabular/learners/optuna_learner.rst +++ /dev/null @@ -1,8 +0,0 @@ -OptunaLearner -========================= - -.. autoclass:: falcon.tabular.learners.OptunaLearner - :members: - :inherited-members: - :special-members: __init__ - diff --git a/docs/source/tabular/learners/plain_learner.rst b/docs/source/tabular/learners/plain_learner.rst deleted file mode 100644 index e456d43..0000000 --- a/docs/source/tabular/learners/plain_learner.rst +++ /dev/null @@ -1,8 +0,0 @@ -PlainLearner -========================= - -.. autoclass:: falcon.tabular.learners.PlainLearner - :members: - :inherited-members: - :special-members: __init__ - diff --git a/docs/source/tabular/learners/super_learner.rst b/docs/source/tabular/learners/super_learner.rst deleted file mode 100644 index 4fcce8f..0000000 --- a/docs/source/tabular/learners/super_learner.rst +++ /dev/null @@ -1,8 +0,0 @@ -SuperLearner -========================= - -.. autoclass:: falcon.tabular.learners.SuperLearner - :members: - :inherited-members: - :special-members: __init__ - \ No newline at end of file diff --git a/docs/source/tabular/models/hgbt_clf.rst b/docs/source/tabular/models/hgbt_clf.rst deleted file mode 100644 index 21c44e0..0000000 --- a/docs/source/tabular/models/hgbt_clf.rst +++ /dev/null @@ -1,7 +0,0 @@ -HistGradientBoostingClassifier -================================================== - -.. autoclass:: falcon.tabular.models.HistGradientBoostingClassifier - :members: - :inherited-members: - :special-members: __init__ diff --git a/docs/source/tabular/models/hgbt_regr.rst b/docs/source/tabular/models/hgbt_regr.rst deleted file mode 100644 index 6bc487f..0000000 --- a/docs/source/tabular/models/hgbt_regr.rst +++ /dev/null @@ -1,8 +0,0 @@ -HistGradientBoostingRegressor -================================================== - -.. autoclass:: falcon.tabular.models.HistGradientBoostingRegressor - :members: - :inherited-members: - :special-members: __init__ - diff --git a/docs/source/tabular/models/stacking_clf.rst b/docs/source/tabular/models/stacking_clf.rst deleted file mode 100644 index 8275aac..0000000 --- a/docs/source/tabular/models/stacking_clf.rst +++ /dev/null @@ -1,7 +0,0 @@ -StackingClassifier -========================= - -.. autoclass:: falcon.tabular.models.StackingClassifier - :members: - :inherited-members: - :special-members: __init__ diff --git a/docs/source/tabular/models/stacking_regr.rst b/docs/source/tabular/models/stacking_regr.rst deleted file mode 100644 index 1ca541e..0000000 --- a/docs/source/tabular/models/stacking_regr.rst +++ /dev/null @@ -1,7 +0,0 @@ -StackingRegressor -========================= - -.. autoclass:: falcon.tabular.models.StackingRegressor - :members: - :inherited-members: - :special-members: __init__ diff --git a/docs/source/tabular/pipelines/simple_pipeline.rst b/docs/source/tabular/pipelines/simple_pipeline.rst deleted file mode 100644 index 44d953e..0000000 --- a/docs/source/tabular/pipelines/simple_pipeline.rst +++ /dev/null @@ -1,7 +0,0 @@ -SimpleTabularPipeline -========================= - -.. autoclass:: falcon.tabular.pipelines.SimpleTabularPipeline - :members: - :inherited-members: - :special-members: __init__ \ No newline at end of file diff --git a/docs/source/tabular/processors/label_decoder.rst b/docs/source/tabular/processors/label_decoder.rst deleted file mode 100644 index 1d9634f..0000000 --- a/docs/source/tabular/processors/label_decoder.rst +++ /dev/null @@ -1,8 +0,0 @@ -LabelDecoder -=================== - -.. autoclass:: falcon.tabular.processors.LabelDecoder - :members: - :private-members: - :inherited-members: - :special-members: __init__ \ No newline at end of file diff --git a/docs/source/tabular/processors/mm_encoder.rst b/docs/source/tabular/processors/mm_encoder.rst deleted file mode 100644 index 5599f06..0000000 --- a/docs/source/tabular/processors/mm_encoder.rst +++ /dev/null @@ -1,8 +0,0 @@ -MultiModalEncoder -=================== - -.. autoclass:: falcon.tabular.processors.MultiModalEncoder - :members: - :private-members: - :inherited-members: - :special-members: __init__ \ No newline at end of file diff --git a/docs/source/tabular/processors/scaler_and_encoder.rst b/docs/source/tabular/processors/scaler_and_encoder.rst deleted file mode 100644 index 870a261..0000000 --- a/docs/source/tabular/processors/scaler_and_encoder.rst +++ /dev/null @@ -1,8 +0,0 @@ -ScalerAndEncoder -=================== - -.. autoclass:: falcon.tabular.processors.ScalerAndEncoder - :members: - :private-members: - :inherited-members: - :special-members: __init__ \ No newline at end of file diff --git a/docs/source/tabular/tab_manager.rst b/docs/source/tabular/tab_manager.rst deleted file mode 100644 index 9616c2a..0000000 --- a/docs/source/tabular/tab_manager.rst +++ /dev/null @@ -1,8 +0,0 @@ -TabularTaskManager -=================== - -.. autoclass:: falcon.tabular.TabularTaskManager - :members: - :private-members: - :inherited-members: - :special-members: __init__ \ No newline at end of file diff --git a/docs/sphinx_req.txt b/docs/sphinx_req.txt deleted file mode 100644 index fc00bbd..0000000 --- a/docs/sphinx_req.txt +++ /dev/null @@ -1,9 +0,0 @@ -Sphinx==5.3.0 -sphinx-rtd-theme==1.1.1 -sphinxcontrib-applehelp==1.0.2 -sphinxcontrib-devhelp==1.0.2 -sphinxcontrib-htmlhelp==2.0.0 -sphinxcontrib-jsmath==1.0.1 -sphinxcontrib-qthelp==1.0.3 -sphinxcontrib-serializinghtml==1.1.5 -sphinx-design==0.3.0 \ No newline at end of file diff --git a/falcon/__init__.py b/falcon/__init__.py index 3527d2f..b6407bb 100644 --- a/falcon/__init__.py +++ b/falcon/__init__.py @@ -1,7 +1,8 @@ -__version__ = '0.6.0' -__author__ = 'Oleg Kostromin, Marco Pasini, Iryna Kondrashchenko' +__version__ = "1.0.0" +__author__ = "Oleh Kostromin, Iryna Kondrashchenko" -from falcon.main import initialize, AutoML -from falcon.utils import disable_warnings, run_model +from falcon.config import RunConfig as RunConfig +from falcon.main import AutoML as AutoML +from falcon.predictor import Predictor as Predictor -disable_warnings() \ No newline at end of file +__all__ = ["AutoML", "Predictor", "RunConfig"] diff --git a/falcon/abstract/__init__.py b/falcon/abstract/__init__.py index 37726c0..ab54fe8 100644 --- a/falcon/abstract/__init__.py +++ b/falcon/abstract/__init__.py @@ -1,7 +1,3 @@ -from falcon.abstract.learner import Learner -from falcon.abstract.model import Model -from falcon.abstract.processor import Processor -from falcon.abstract.task_manager import TaskManager -from falcon.abstract.task_pipeline import Pipeline, PipelineElement -from falcon.abstract.onnx_convertible import ONNXConvertible -from falcon.abstract.optuna import OptunaMixin +from falcon.abstract.task_pipeline import Pipeline, PipelineStep + +__all__ = ["Pipeline", "PipelineStep"] diff --git a/falcon/abstract/learner.py b/falcon/abstract/learner.py deleted file mode 100644 index a1ee7af..0000000 --- a/falcon/abstract/learner.py +++ /dev/null @@ -1,21 +0,0 @@ -from abc import ABC, abstractmethod -from numpy import typing as npt -from falcon.abstract.task_pipeline import PipelineElement -from typing import Any, Optional - - -class Learner(PipelineElement): - """ - Subclass of `PipelineElement`. - Learners are task aware pipeline elements that act as wrappers around models and responsible for tuning of the hyperparameters. - """ - def __init__(self, task: str, **kwargs: Any) -> None: - """ - Parameters - ---------- - task : str - current ML task - """ - self.task = task - - \ No newline at end of file diff --git a/falcon/abstract/model.py b/falcon/abstract/model.py deleted file mode 100644 index 65d7f92..0000000 --- a/falcon/abstract/model.py +++ /dev/null @@ -1,61 +0,0 @@ -from abc import ABC, abstractmethod -from numpy import typing as npt -from typing import Any -from typing_extensions import Protocol - - -class Model(ABC): - """ - Base class for all models. - """ - - @abstractmethod - def fit(self, X: npt.NDArray, y: npt.NDArray, *args: Any, **kwargs: Any) -> Any: - """ - - Parameters - ---------- - X : npt.NDArray - features - y : npt.NDArray - targets - - Returns - ------- - Any - usually `None` - """ - pass - - @abstractmethod - def predict(self, X: npt.NDArray, *args: Any, **kwargs: Any) -> npt.NDArray: - """ - Parameters - ---------- - X : npt.NDArray - features - - Returns - ------- - npt.NDArray - predictions - """ - pass - - -class TransformerMixin: - def transform(self, X: npt.NDArray, *args: Any, **kwargs: Any) -> npt.NDArray: - """ - Equivalent of `self.predict(X)` - - Parameters - ---------- - X : npt.NDArray - features - - Returns - ------- - npt.NDArray - transformed features - """ - return self.predict(X) # type: ignore diff --git a/falcon/abstract/onnx_convertible.py b/falcon/abstract/onnx_convertible.py deleted file mode 100644 index 332dab3..0000000 --- a/falcon/abstract/onnx_convertible.py +++ /dev/null @@ -1,18 +0,0 @@ -from abc import ABC, abstractmethod -from falcon.serialization import SerializedModelRepr - - -class ONNXConvertible(ABC): - """ - Base class for all models/pipeline_elements/pipelines that can be converted to onnx. - """ - @abstractmethod - def to_onnx(self) -> SerializedModelRepr: - """ - Converted model - - Returns - ------- - SerializedModelRepr - """ - pass diff --git a/falcon/abstract/optuna.py b/falcon/abstract/optuna.py deleted file mode 100644 index 025c2ba..0000000 --- a/falcon/abstract/optuna.py +++ /dev/null @@ -1,30 +0,0 @@ -from abc import abstractmethod, ABC -from typing import Any, Union, Callable, Dict -from typing_extensions import Protocol -from numpy import typing as npt - -class OptunaMixin(ABC): - - """ - Abstract mixin that should be used in order to indicate the compatibility of the model with OptunaLearner. - """ - @classmethod - @abstractmethod - def get_search_space(cls, X: Any, y: Any) -> Union[Callable, Dict]: - """ - A class method that provides an optuna search space for the model. - Optionally, the search space can be adjusted based on the provided training data. - - Parameters - ---------- - X : Any - features - y : Any - targets - - Returns - ------- - Union[Callable, Dict] - dictionary that describes the search space, or custom objective function - """ - pass \ No newline at end of file diff --git a/falcon/abstract/processor.py b/falcon/abstract/processor.py deleted file mode 100644 index 1e0fa6e..0000000 --- a/falcon/abstract/processor.py +++ /dev/null @@ -1,10 +0,0 @@ -from falcon.abstract.model import TransformerMixin - -from falcon.abstract.task_pipeline import PipelineElement - - -class Processor(PipelineElement, TransformerMixin): - """ - Subclass of `PipelineElement`. Used for data pre and post processing (e.g. data scaling). - """ - pass diff --git a/falcon/abstract/task_manager.py b/falcon/abstract/task_manager.py deleted file mode 100644 index 80d6cf9..0000000 --- a/falcon/abstract/task_manager.py +++ /dev/null @@ -1,198 +0,0 @@ -from __future__ import annotations -from abc import ABC, abstractmethod -from numpy import typing as npt -from .task_pipeline import Pipeline -from typing import Dict, Optional, Any, Callable, Type, List -from falcon.serialization import SerializedModelRepr -from onnx import ModelProto -from onnx import save_model as onnx_save_model - - -class TaskManager(ABC): - """ - Base class for all Task Managers. - """ - def __init__( - self, - task: str, - data: Any, - pipeline: Optional[Type[Pipeline]] = None, - pipeline_options: Optional[Dict] = None, - extra_pipeline_options: Optional[Dict] = None, - features: Any = None, - target: Any = None, - ): - """ - - Parameters - ---------- - task : str - current task - data : Any - data to be used for training - pipeline : Optional[Type[Pipeline]], optional - pipeline class to be used, by default None - pipeline_options : Optional[Dict], optional - arguments to be passed to pipeline instead of default ones, by default None - extra_pipeline_options : Optional[Dict], optional - arguments to be passed to pipeline in addition to default ones, by default None - features : Any, optional - featrues to be used for training, by default None - target : Any, optional - targets to be used for training, by default None - """ - self.task: str = task - self.features = features - self.target = target - self.dataset_size = () - self.feature_names_to_save: List[Any] = [] - self._data = self._prepare_data(data) - if self.dataset_size is None: - raise RuntimeError('It seems like prepare_data() method did not set dataset_size attribute.') - self._extra_pipeline_options: Optional[Dict] = extra_pipeline_options - self._create_pipeline(pipeline=pipeline, options=pipeline_options) - - @abstractmethod - def train(self, **kwargs: Any) -> TaskManager: - """ - Trains the underlying pipeline. - - Returns - ------- - TaskManager - self - """ - pass - - @abstractmethod - def _prepare_data(self, data: Any) -> Any: - """ - Initial data preparation (e.g. reading from file). - Warning: initial data preparation (e.g. reading, cleaning) and data preprocessing (e.g. scaling, encoding) are two distinct steps. The later one is performed inside the pipeline. - - Parameters - ---------- - data : Any - training data - - Returns - ------- - Any - prepared data - """ - pass - - @property - @abstractmethod - def default_pipeline(self) -> Type[Pipeline]: - """ - Default pipeline class. Can be chosen dynamically. - """ - pass - - @property - @abstractmethod - def default_pipeline_options(self) -> Dict: - """ - Default pipeline options. Can be chosen dynamically. - """ - pass - - def predict(self, X: Any) -> Any: - """ - Calls predict methods of the pipeline. - - Parameters - ---------- - X : Any - features - - Returns - ------- - Any - predictions - """ - return self._pipeline.predict(X) - - def _create_pipeline( - self, pipeline: Optional[Type[Pipeline]], options: Optional[Dict] - ) -> None: - """ - Initializes the pipeline. - - Parameters - ---------- - pipeline : Optional[Type[Pipeline]] - pipeline class - options : Optional[Dict] - pipeline options - """ - - # if pipeline is not None and options is None: - # self._pipeline = pipeline(task=self.task) - - if pipeline is None: - pipeline = self.default_pipeline - if options is None: - options = self.default_pipeline_options - if self._extra_pipeline_options is not None: - for k, v in self._extra_pipeline_options.items(): - options[k] = v - self._pipeline: Pipeline = pipeline(task=self.task, dataset_size = self.dataset_size, **options) - - def save_model(self, filename: Optional[str] = None, **kwargs: Any) -> ModelProto: - """ - Serializes and saves the model. - - Parameters - ---------- - filename : Optional[str], optional - filename for the model file, by default None. If filename is not specified, the model is not saved on disk and only returned as bytes object - Returns - ------- - ModelProto - ONNX ModelProto of the model - """ - - serialized_model = self._pipeline.save(feature_names=self.feature_names_to_save) - if filename is not None: - if not filename.endswith(f".onnx"): - filename += f".onnx" - onnx_save_model(serialized_model, filename, save_as_external_data=True, all_tensors_to_one_file=True, location=f"{filename}.tensors", size_threshold=0, convert_attribute=True) - return serialized_model - - @abstractmethod - def evaluate(self, test_data: Any) -> Any: - """ - Evaluates the performance of a trained pipeline. - - Parameters - ---------- - test_data : Any - data to be used for evaluation - - - Returns - ------- - Any - evaluation metric or None - """ - pass - - @abstractmethod - def performance_summary(self, test_data: Any) -> Any: - """ - Prints the performance summary of the trained pipeline. - - - Parameters - ---------- - test_data : Any - test set, optional - - Returns - ------- - Any - relevant metrics or None - """ - pass \ No newline at end of file diff --git a/falcon/abstract/task_pipeline.py b/falcon/abstract/task_pipeline.py index 2cbe431..42bd0d2 100644 --- a/falcon/abstract/task_pipeline.py +++ b/falcon/abstract/task_pipeline.py @@ -1,128 +1,109 @@ -from abc import abstractmethod -from typing import Any, Type, Union, Optional, List, Tuple -from onnx import ModelProto -from falcon.abstract.model import Model -from falcon.abstract.onnx_convertible import ONNXConvertible -from falcon.serialization import SerializedModelRepr, serialize_to_onnx - - -class PipelineElement(Model): - """ - Base class for all pipeline elements. - """ - - @abstractmethod - def get_input_type(self) -> Type: - """ - Returns - ------- - Type - Input types - """ - pass - - @abstractmethod - def get_output_type(self) -> Type: - """ - Returns - ------- - Type - Output types - """ - pass - - def forward(self, X: Any, *args: Any, **kwargs: Any) -> Any: - """ - Equivalent of `predict` method that is used for elements chaining inside pipeline during inference. - - Parameters - ---------- - X : Any - featrues - - Returns - ------- - Any - predictions - """ - return self.predict(X) - - def fit_pipe(self, X: Any, y: Any, *args: Any, **kwargs: Any) -> Any: - """ - Equivalent of `fit` method that is used for elements chaining inisde pipeline during training. - - Parameters - ---------- - X : Any - features - y : Any - targets - - Returns - ------- - Any - usually None - """ - self.fit(X, y) - - -class Pipeline(Model): - """ - Base class for all pipelines. - """ +from __future__ import annotations +from typing import Any, Protocol, runtime_checkable + +from numpy import typing as npt + +from falcon.serialization import FNNXSerializer, SerializedModelRepr +from falcon.types import DatasetSchema + + +@runtime_checkable +class PipelineStep(Protocol): + def fit( + self, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + schema: DatasetSchema, + *, + groups: npt.ArrayLike | None = None, + ) -> None: ... + + def transform(self, X: npt.NDArray[Any]) -> npt.NDArray[Any]: ... + + def serialize(self) -> SerializedModelRepr: ... + + def get_input_type(self) -> object: ... + + def get_output_type(self) -> object: ... + + +class Pipeline: def __init__( - self, task: str, dataset_size: Tuple[int, ...], mask: List[Any], **kwargs: Any + self, + task: str, + dataset_size: tuple[int, ...], + schema: DatasetSchema | None = None, + **kwargs: Any, ) -> None: self.task = task - self._pipeline: List[PipelineElement] = [] self.dataset_size = dataset_size - self.mask = mask - - def add_element(self, element: PipelineElement) -> None: - """ - Adds element to pipeline. The input type of added element should match the output type of the last element in the pipeline. - - Parameters - ---------- - element : PipelineElement - element to be added to the end of the pipeline - """ - if ( - len(self._pipeline) > 1 - and element.get_input_type() != self._pipeline[-1].get_output_type() - ): + self.schema = schema + self._steps: list[PipelineStep] = [] + + @property + def steps(self) -> tuple[PipelineStep, ...]: + return tuple(self._steps) + + def clear_steps(self) -> None: + self._steps.clear() + + def add_step(self, step: PipelineStep) -> None: + if step is self: + raise ValueError("Cannot add the pipeline to itself") + if not isinstance(step, PipelineStep): + raise TypeError( + "Pipeline steps must implement fit, transform, serialize, and type metadata" + ) + if self._steps and step.get_input_type() != self._steps[-1].get_output_type(): raise RuntimeError( - "The element cannot be added to pipeline due to input type missmatch." + "The step cannot be added to the pipeline because its input type " + "does not match the previous output type." ) - if element is self: - raise ValueError("Cannot add self to the pipeline") - self._pipeline.append(element) - - def save(self, feature_names: Optional[List] = None) -> ModelProto: - """ - Exports the pipeline to ONNX ModelProto - - Parameters - ---------- - feature_names : Optional[List], optional - feature names, by default None - Returns - ------- - ModelProto - Pipeline as ONNX ModelProto - """ - serialized_pipeline_elements: List[SerializedModelRepr] = [] - for p in self._pipeline: - if isinstance(p, ONNXConvertible): - serialized_pipeline_elements.append(p.to_onnx()) + self._steps.append(step) + + def fit( + self, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + schema: DatasetSchema, + *, + groups: npt.ArrayLike | None = None, + ) -> None: + if X.ndim != 2 or X.shape[1] != schema.n_features: + raise ValueError("Feature data does not match the dataset schema") + self.schema = schema + transformed = X + for step in self._steps: + fit_transform = getattr(step, "fit_transform", None) + if fit_transform is None: + step.fit(transformed, y, schema, groups=groups) + transformed = step.transform(transformed) else: - raise RuntimeError("Encountered non convertible pipeline element") - - serialized_model = serialize_to_onnx( - serialized_pipeline_elements, + transformed = fit_transform(transformed, y, schema, groups=groups) + + def predict(self, X: npt.NDArray[Any]) -> npt.NDArray[Any]: + transformed = X + for step in self._steps: + transformed = step.transform(transformed) + return transformed + + def save( + self, + feature_names: list[Any] | None = None, + producer_extra_tags: list[str] | None = None, + schema: DatasetSchema | None = None, + ) -> FNNXSerializer: + serialized_steps = [step.serialize() for step in self._steps] + resolved_schema = schema if schema is not None else self.schema + if resolved_schema is None: + raise RuntimeError("A dataset schema is required to save a pipeline") + + return FNNXSerializer( + models=serialized_steps, task=self.task, - init_types=self.mask, + init_types=list(resolved_schema.column_types), init_feature_names=feature_names, + producer_extra_tags=producer_extra_tags, + schema=resolved_schema, ) - return serialized_model diff --git a/falcon/addons/sklearn/__init__.py b/falcon/addons/sklearn/__init__.py index f41db69..83eeae3 100644 --- a/falcon/addons/sklearn/__init__.py +++ b/falcon/addons/sklearn/__init__.py @@ -1,2 +1,3 @@ -from falcon.addons.sklearn.ensemble.balanced_stacking import BalancedStackingClassifier # type: ignore -from falcon.addons.sklearn.preprocessing.date_tokenizer import DateTimeTokenizer \ No newline at end of file +from falcon.addons.sklearn.preprocessing.date_tokenizer import DateTimeTokenizer + +__all__ = ["DateTimeTokenizer"] diff --git a/falcon/addons/sklearn/decomposition/svd.py b/falcon/addons/sklearn/decomposition/svd.py index 3f3b408..107f5c6 100644 --- a/falcon/addons/sklearn/decomposition/svd.py +++ b/falcon/addons/sklearn/decomposition/svd.py @@ -1,21 +1,28 @@ -from sklearn.decomposition import TruncatedSVD as _TruncatedSVD -from skl2onnx.operator_converters.decomposition import convert_truncated_svd as _convert_truncated_svd -from skl2onnx.shape_calculators.svd import calculate_sklearn_truncated_svd_output_shapes as _calculate_sklearn_truncated_svd_output_shapes +from __future__ import annotations + +from typing import Any + from skl2onnx import update_registered_converter as _update_registered_converter +from skl2onnx.operator_converters.decomposition import ( + convert_truncated_svd as _convert_truncated_svd, +) +from skl2onnx.shape_calculators.svd import ( + calculate_sklearn_truncated_svd_output_shapes as _calculate_sklearn_truncated_svd_output_shapes, +) +from sklearn.decomposition import TruncatedSVD as _TruncatedSVD class ConditionalSVD(_TruncatedSVD): - - def _svd(self, X) -> _TruncatedSVD: + def _svd(self, X: Any) -> Any: self._mode = "svd" return super().fit_transform(X) - - def _identity(self, X): + + def _identity(self, X: Any) -> Any: self._mode = "identity" self.out_dim = X.shape[-1] return X - - def fit(self, X) -> _TruncatedSVD: + + def fit(self, X: Any, y: Any = None) -> ConditionalSVD: if X.shape[1] > self.n_components: self._svd(X) else: @@ -23,22 +30,23 @@ def fit(self, X) -> _TruncatedSVD: self.fit_ = True return self - def transform(self, X): + def transform(self, X: Any) -> Any: if self._mode == "svd": return super().transform(X) else: return X - def fit_transform(self, X, y=None): + def fit_transform(self, X: Any, y: Any = None) -> Any: self.fit_ = True if X.shape[1] > self.n_components: return self._svd(X) else: return self._identity(X) -def _svd_shape_calc(operator): + +def _svd_shape_calc(operator: Any) -> None: if operator.raw_operator._mode == "svd": - operator.type = 'SklearnTruncatedSVD' + operator.type = "SklearnTruncatedSVD" _calculate_sklearn_truncated_svd_output_shapes(operator=operator) else: cls_type = operator.inputs[0].type.__class__ @@ -47,9 +55,9 @@ def _svd_shape_calc(operator): operator.outputs[0].type = cls_type([N, K]) -def _svd_converter(scope, operator, container): +def _svd_converter(scope: Any, operator: Any, container: Any) -> None: if operator.raw_operator._mode == "svd": - operator.type = 'SklearnTruncatedSVD' + operator.type = "SklearnTruncatedSVD" _convert_truncated_svd(scope, operator, container) else: in_name = operator.inputs[0].full_name diff --git a/falcon/addons/sklearn/ensemble/balanced_stacking.py b/falcon/addons/sklearn/ensemble/balanced_stacking.py deleted file mode 100644 index a12924a..0000000 --- a/falcon/addons/sklearn/ensemble/balanced_stacking.py +++ /dev/null @@ -1,167 +0,0 @@ -# type: ignore -from imblearn.over_sampling import RandomOverSampler -from abc import ABCMeta, abstractmethod -from copy import deepcopy - -import numpy as np -from joblib import Parallel -import scipy.sparse as sparse - -from sklearn.base import clone -from sklearn.base import is_classifier -from sklearn.ensemble._base import _fit_single_estimator -from sklearn.model_selection import cross_val_predict -from sklearn.model_selection import check_cv -from sklearn.preprocessing import LabelEncoder -from sklearn.utils import Bunch -from sklearn.utils.multiclass import check_classification_targets -from sklearn.utils.validation import check_is_fitted -from sklearn.utils.validation import check_scalar -from sklearn.utils.fixes import delayed -from sklearn.ensemble import StackingClassifier -from types import MethodType -from falcon.addons.sklearn.model_selection.balanced_strat_kfold import ( - BalancedStratifiedKFold, -) -from sklearn.preprocessing import LabelEncoder -from sklearn import __version__ as sklearn_version -from packaging import version - -# Slightly modified version of StackingClassifier from sklearn that upsamples the minority class during training -# The .fit() method was adopted from https://github.com/scikit-learn/scikit-learn/blob/36958fb24/sklearn/ensemble/_stacking.py - - -def _fit(self, X, y, sample_weight=None): - check_classification_targets(y) - self._le = LabelEncoder().fit(y) - self.classes_ = self._le.classes_ - check_scalar( - self.passthrough, - name="passthrough", - target_type=(np.bool_, bool), - include_boundaries="neither", - ) - # all_estimators contains all estimators, the one to be fitted and the - # 'drop' string. - names, all_estimators = self._validate_estimators() - self._validate_final_estimator() - - stack_method = [self.stack_method] * len(all_estimators) - - if self.cv == "prefit": - self.estimators_ = [] - for estimator in all_estimators: - if estimator != "drop": - check_is_fitted(estimator) - self.estimators_.append(estimator) - else: - # Fit the base estimators on the whole training data. Those - # base estimators will be used in transform, predict, and - # predict_proba. They are exposed publicly. - X_resampled, y_resampled = RandomOverSampler().fit_resample(X, y) - self.estimators_ = Parallel(n_jobs=self.n_jobs)( - delayed(_fit_single_estimator)( - clone(est), X_resampled, y_resampled, sample_weight - ) - for est in all_estimators - if est != "drop" - ) - - self.named_estimators_ = Bunch() - est_fitted_idx = 0 - for name_est, org_est in zip(names, all_estimators): - if org_est != "drop": - current_estimator = self.estimators_[est_fitted_idx] - self.named_estimators_[name_est] = current_estimator - est_fitted_idx += 1 - if hasattr(current_estimator, "feature_names_in_"): - self.feature_names_in_ = current_estimator.feature_names_in_ - else: - self.named_estimators_[name_est] = "drop" - - self.stack_method_ = [ - self._method_name(name, est, meth) - for name, est, meth in zip(names, all_estimators, stack_method) - ] - - if self.cv == "prefit": - # Generate predictions from prefit models - predictions = [ - getattr(estimator, predict_method)(X) - for estimator, predict_method in zip(all_estimators, self.stack_method_) - if estimator != "drop" - ] - else: - # To train the meta-classifier using the most data as possible, we use - # a cross-validation to obtain the output of the stacked estimators. - # To ensure that the data provided to each estimator are the same, - # we need to set the random state of the cv if there is one and we - # need to take a copy. - - if isinstance(self.cv, int): - self.cv = BalancedStratifiedKFold(self.cv) - - cv = check_cv(self.cv, y=y, classifier=is_classifier(self)) - if hasattr(cv, "random_state") and cv.random_state is None: - cv.random_state = np.random.RandomState() - - fit_params = ( - {"sample_weight": sample_weight} if sample_weight is not None else None - ) - - predictions = Parallel(n_jobs=self.n_jobs)( - delayed(cross_val_predict)( - clone(est), - X, - y, - cv=deepcopy(cv), - method=meth, - n_jobs=self.n_jobs, - fit_params=fit_params, - verbose=self.verbose, - ) - for est, meth in zip(all_estimators, self.stack_method_) - if est != "drop" - ) - - # Only not None or not 'drop' estimators will be used in transform. - # Remove the None from the method as wl. - self.stack_method_ = [ - meth for (meth, est) in zip(self.stack_method_, all_estimators) if est != "drop" - ] - - X_meta = self._concatenate_predictions(X, predictions) - X_meta_resampled, y_meta_resampled = RandomOverSampler().fit_resample(X_meta, y) - _fit_single_estimator( - self.final_estimator_, - X_meta_resampled, - y_meta_resampled, - sample_weight=sample_weight, - ) - - return self - -class _EncoderPlaceholder(LabelEncoder): - - def fit(self, y, **args): - return self - - def transform(self, y, **args): - return y - - def fit_transform(self, y, **args): - return y - - def inverse_transform(self, y, **args): - return y - - - -# the object is being patched with a new method instead of subclassing -# so the estimator can be converted to ONNX using the default converter -def BalancedStackingClassifier(estimators, final_estimator, **kwargs): - clf = StackingClassifier(estimators, final_estimator, **kwargs) - clf.fit = MethodType(_fit, clf) - if version.parse(sklearn_version) >= version.parse("1.2.0"): - clf._label_encoder = _EncoderPlaceholder() - return clf diff --git a/falcon/addons/sklearn/model_selection/balanced_strat_kfold.py b/falcon/addons/sklearn/model_selection/balanced_strat_kfold.py deleted file mode 100644 index c356d63..0000000 --- a/falcon/addons/sklearn/model_selection/balanced_strat_kfold.py +++ /dev/null @@ -1,15 +0,0 @@ -from sklearn.model_selection import StratifiedKFold -from imblearn.over_sampling import RandomOverSampler - - -class BalancedStratifiedKFold(StratifiedKFold): - def split(self, X, y, groups=None): # type: ignore - for train, test in super().split(X, y, groups): - y_train_split = y[train] - y_test_split = y[test] - train, _ = RandomOverSampler().fit_resample( - train.reshape(-1, 1), y_train_split - ) - - - yield train.squeeze(), test.squeeze() diff --git a/falcon/addons/sklearn/preprocessing/date_tokenizer.py b/falcon/addons/sklearn/preprocessing/date_tokenizer.py index 4f77ed4..c9feee0 100644 --- a/falcon/addons/sklearn/preprocessing/date_tokenizer.py +++ b/falcon/addons/sklearn/preprocessing/date_tokenizer.py @@ -1,74 +1,504 @@ -import pandas as pd +from __future__ import annotations + +import re +from typing import Any + import numpy as np -from sklearn.base import BaseEstimator, TransformerMixin -from onnxconverter_common.data_types import StringTensorType -from skl2onnx.proto import onnx_proto +from numpy import typing as npt from skl2onnx import update_registered_converter -import re +from skl2onnx.common.data_types import DoubleTensorType +from skl2onnx.proto import onnx_proto +from sklearn.base import BaseEstimator, TransformerMixin +from sklearn.utils.validation import check_is_fitted + +from falcon.addons.sklearn.preprocessing.missing_values import ( + add_string_missing_mask, + string_values_and_missing_mask, +) + +DATE_FORMAT = r"%Y-%m-%d" +DATETIME_SPACE_FORMAT = r"%Y-%m-%d %H:%M:%S" +DATETIME_T_FORMAT = r"%Y-%m-%dT%H:%M:%SZ" + +_DATE_VARIANT = "date" +_DATE_PATTERN = re.compile(r"\d{4}-\d{2}-\d{2}") +_DATETIME_VARIANTS: dict[str, tuple[re.Pattern[str], str, bool]] = { + "datetime_t_z": ( + re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z"), + "T", + True, + ), + "datetime_t": ( + re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}"), + "T", + False, + ), + "datetime_space_z": ( + re.compile(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}Z"), + " ", + True, + ), + "datetime_space": ( + re.compile(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}"), + " ", + False, + ), +} +_DATE_CYCLIC_INDICES = np.asarray([1, 2], dtype=np.int64) +_DATE_CYCLIC_PERIODS = np.asarray([12, 31], dtype=np.float64) +_DATETIME_CYCLIC_INDICES = np.asarray([1, 2, 3, 4, 5], dtype=np.int64) +_DATETIME_CYCLIC_PERIODS = np.asarray([12, 31, 24, 60, 60], dtype=np.float64) +# ONNX Runtime evaluates Sin/Cos only for floats; rounding prevents tiny kernel +# differences from sending native and exported tree models down different branches. +_CYCLIC_QUANTIZATION_SCALE = np.float32(100_000) class DateTimeTokenizer(BaseEstimator, TransformerMixin): - def __init__(self, format: str): - if format not in (r"%Y-%m-%d", r"%Y-%m-%d %H:%M:%S", r"%Y-%m-%dT%H:%M:%SZ"): + format: str + variant_: str + reference_value_: str + raw_dim_: int + out_dim: int + + def __init__(self, format: str) -> None: + if format not in (DATE_FORMAT, DATETIME_SPACE_FORMAT, DATETIME_T_FORMAT): raise ValueError("Selected date format is not supported") self.format = format - def fit(self, X): - self.fit_ = True + def fit(self, X: npt.NDArray[Any], y: Any = None) -> DateTimeTokenizer: + values, missing = self._single_column_values(X) + present_values = values[~missing] + if present_values.size == 0: + raise ValueError( + "DateTimeTokenizer requires at least one non-missing value" + ) + + self.variant_ = self._detect_variant(present_values) + self.reference_value_ = str(present_values[0]) + self.raw_dim_ = 3 if self.variant_ == _DATE_VARIANT else 6 + cyclic_count = 2 if self.variant_ == _DATE_VARIANT else 5 + self.out_dim = self.raw_dim_ + 2 * cyclic_count + 1 return self - def transform(self, X: np.ndarray) -> np.ndarray: - assert ( - len(X.shape) < 2 or X.shape[1] == 1 - ), "DateTimeTokenizer only accepts single column arrays" - if self.format == r"%Y-%m-%d": - self.out_dim = 3 - elif self.format in (r"%Y-%m-%d %H:%M:%S", r"%Y-%m-%dT%H:%M:%SZ"): - self.out_dim = 6 - r = re.compile("[0-9]+") - l = np.apply_along_axis(lambda x: r.findall(str(x)), -1, X).reshape(-1, self.out_dim) - return l - -def _dt_shape_calculator(operator): # type: ignore - c = operator.raw_operator.out_dim - operator.outputs[0].type = StringTensorType([None, c]) - - -def _dt_converter(scope, operator, container): # type: ignore - in_name = operator.inputs[0].full_name - out_name = operator.outputs[0].full_name - tokenizer_name = scope.get_unique_operator_name("dt_token") - sq_name = scope.get_unique_operator_name("dt_squeeze") - tokenized = scope.get_unique_operator_name("tokenized") - - axis_name = scope.get_unique_variable_name("axis") - axis = np.asarray([1], dtype=np.int64) - - proto_dtype = onnx_proto.TensorProto.INT64 + def transform(self, X: npt.NDArray[Any]) -> npt.NDArray[np.float64]: + check_is_fitted(self, ("variant_", "reference_value_", "raw_dim_", "out_dim")) + values, missing = self._single_column_values(X) + if values.size == 0: + return np.empty((0, self.out_dim), dtype=np.float64) + + present_values = values[~missing] + if present_values.size: + variant = self._detect_variant(present_values) + if variant != self.variant_: + raise ValueError( + "Input does not match the fitted datetime format variant " + f"{self.variant_!r}" + ) + values = np.where(missing, self.reference_value_, values) + variant = self.variant_ + + rows: list[list[str]] = [] + if variant == _DATE_VARIANT: + rows = [value.split("-") for value in values] + cyclic_indices = _DATE_CYCLIC_INDICES + cyclic_periods = _DATE_CYCLIC_PERIODS + else: + _, delimiter, has_trailing_z = _DATETIME_VARIANTS[variant] + for value in values: + date_value, time_value = value.split(delimiter, maxsplit=1) + if has_trailing_z: + time_value = time_value.removesuffix("Z") + rows.append(date_value.split("-") + time_value.split(":")) + cyclic_indices = _DATETIME_CYCLIC_INDICES + cyclic_periods = _DATETIME_CYCLIC_PERIODS + + components = np.asarray(rows, dtype=np.float64) + angle_scales = (2 * np.pi) / cyclic_periods + angles = (components[:, cyclic_indices] * angle_scales).astype(np.float32) + sin_values = ( + np.round(np.sin(angles) * _CYCLIC_QUANTIZATION_SCALE) + / _CYCLIC_QUANTIZATION_SCALE + ) + cos_values = ( + np.round(np.cos(angles) * _CYCLIC_QUANTIZATION_SCALE) + / _CYCLIC_QUANTIZATION_SCALE + ) + return np.concatenate( + ( + components, + sin_values.astype(np.float64), + cos_values.astype(np.float64), + missing.astype(np.float64).reshape(-1, 1), + ), + axis=1, + ) + + def _detect_variant(self, values: npt.NDArray[np.str_]) -> str: + if self.format == DATE_FORMAT: + if all(_DATE_PATTERN.fullmatch(value) is not None for value in values): + return _DATE_VARIANT + raise ValueError("Values must all use the YYYY-MM-DD date format") + + detected: set[str] = set() + for value in values: + variant = next( + ( + name + for name, (pattern, _, _) in _DATETIME_VARIANTS.items() + if pattern.fullmatch(value) is not None + ), + None, + ) + if variant is None: + raise ValueError( + "Values must use a supported YYYY-MM-DD datetime format" + ) + detected.add(variant) + + if len(detected) != 1: + raise ValueError( + "DateTimeTokenizer requires a single datetime format variant; " + "mixed delimiters or trailing-Z usage were found" + ) + return detected.pop() + + @staticmethod + def _single_column_values( + X: npt.NDArray[Any], + ) -> tuple[npt.NDArray[np.str_], npt.NDArray[np.bool_]]: + values, missing = string_values_and_missing_mask(X) + if values.ndim == 1: + return values, missing + if values.ndim == 2 and values.shape[1] == 1: + return values[:, 0], missing[:, 0] + raise ValueError("DateTimeTokenizer only accepts single-column arrays") + + +def _dt_shape_calculator(operator: Any) -> None: + check_is_fitted(operator.raw_operator, ("variant_", "reference_value_", "out_dim")) + batch_size = operator.inputs[0].get_first_dimension() + operator.outputs[0].type = DoubleTensorType( + [batch_size, operator.raw_operator.out_dim] + ) + + +def _add_string_split( + scope: Any, + container: Any, + input_name: str, + delimiter: str, + maxsplit: int, + name: str, +) -> str: + split_values = scope.get_unique_variable_name(f"{name}_values") + split_counts = scope.get_unique_variable_name(f"{name}_counts") + container.add_node( + "StringSplit", + [input_name], + [split_values, split_counts], + name=scope.get_unique_operator_name(name), + op_domain="", + op_version=20, + delimiter=delimiter, + maxsplit=maxsplit, + ) + return split_values + + +def _add_squeeze( + scope: Any, + container: Any, + input_name: str, + axes_name: str, + name: str, +) -> str: + output_name = scope.get_unique_variable_name(f"{name}_output") + container.add_node( + "Squeeze", + [input_name, axes_name], + [output_name], + name=scope.get_unique_operator_name(name), + op_domain="", + ) + return output_name + +def _add_int64_initializer( + scope: Any, container: Any, name: str, values: npt.NDArray[np.int64] +) -> str: + initializer_name = scope.get_unique_variable_name(name) container.add_initializer( - name=axis_name, onnx_type=proto_dtype, shape=[1], content=axis + name=initializer_name, + onnx_type=onnx_proto.TensorProto.INT64, + shape=list(values.shape), + # Flattened to a list because onnx below 1.22 takes the length of the content, + # which a 0-d array holding a scalar initializer cannot supply. + content=values.reshape(-1).tolist(), ) + return initializer_name - attrs = { - "pad_value": "0", - "mark": False, - "mincharnum": 1, - "tokenexp": r"[0-9]+", - } +def _dt_converter(scope: Any, operator: Any, container: Any) -> None: + transformer: DateTimeTokenizer = operator.raw_operator + check_is_fitted(transformer, ("variant_", "reference_value_", "out_dim")) + input_name = operator.inputs[0].full_name + output_name = operator.outputs[0].full_name + missing = add_string_missing_mask(scope, container, input_name, "date_missing") + reference_value = scope.get_unique_variable_name("date_reference_value") + container.add_initializer( + reference_value, + onnx_proto.TensorProto.STRING, + [], + [transformer.reference_value_], + ) + filled_input = scope.get_unique_variable_name("filled_date_values") container.add_node( - "Tokenizer", - [in_name], - [tokenized], - name=tokenizer_name, - op_domain="com.microsoft", - op_version=1, - **attrs + "Where", + [missing, reference_value, input_name], + [filled_input], + name=scope.get_unique_operator_name("fill_missing_date_values"), + op_domain="", + ) + squeeze_axis = _add_int64_initializer( + scope, container, "date_squeeze_axis", np.asarray([1], dtype=np.int64) ) + if transformer.variant_ == _DATE_VARIANT: + split_date = _add_string_split( + scope, container, filled_input, "-", 2, "split_date" + ) + raw_strings = _add_squeeze( + scope, container, split_date, squeeze_axis, "squeeze_date" + ) + cyclic_indices = _DATE_CYCLIC_INDICES + cyclic_periods = _DATE_CYCLIC_PERIODS + else: + _, delimiter, has_trailing_z = _DATETIME_VARIANTS[transformer.variant_] + split_datetime = _add_string_split( + scope, container, filled_input, delimiter, 1, "split_datetime" + ) + date_and_time = _add_squeeze( + scope, container, split_datetime, squeeze_axis, "squeeze_datetime" + ) + + date_index = _add_int64_initializer( + scope, container, "date_index", np.asarray([0], dtype=np.int64) + ) + time_index = _add_int64_initializer( + scope, container, "time_index", np.asarray([1], dtype=np.int64) + ) + date_value = scope.get_unique_variable_name("date_value") + time_value = scope.get_unique_variable_name("time_value") + container.add_node( + "Gather", + [date_and_time, date_index], + [date_value], + name=scope.get_unique_operator_name("gather_date"), + op_domain="", + axis=1, + ) + container.add_node( + "Gather", + [date_and_time, time_index], + [time_value], + name=scope.get_unique_operator_name("gather_time"), + op_domain="", + axis=1, + ) + + if has_trailing_z: + split_z = _add_string_split( + scope, container, time_value, "Z", 1, "strip_trailing_z" + ) + first_token_index = _add_int64_initializer( + scope, + container, + "first_token_index", + np.asarray(0, dtype=np.int64), + ) + time_without_z = scope.get_unique_variable_name("time_without_z") + container.add_node( + "Gather", + [split_z, first_token_index], + [time_without_z], + name=scope.get_unique_operator_name("gather_time_without_z"), + op_domain="", + axis=2, + ) + time_value = time_without_z + + split_date = _add_string_split( + scope, container, date_value, "-", 2, "split_date" + ) + split_time = _add_string_split( + scope, container, time_value, ":", 2, "split_time" + ) + date_components = _add_squeeze( + scope, container, split_date, squeeze_axis, "squeeze_date" + ) + time_components = _add_squeeze( + scope, container, split_time, squeeze_axis, "squeeze_time" + ) + raw_strings = scope.get_unique_variable_name("raw_datetime_components") + container.add_node( + "Concat", + [date_components, time_components], + [raw_strings], + name=scope.get_unique_operator_name("concat_datetime_components"), + op_domain="", + axis=1, + ) + cyclic_indices = _DATETIME_CYCLIC_INDICES + cyclic_periods = _DATETIME_CYCLIC_PERIODS + + integer_components = scope.get_unique_variable_name("integer_date_components") + container.add_node( + "Cast", + [raw_strings], + [integer_components], + name=scope.get_unique_operator_name("parse_date_components"), + op_domain="", + to=onnx_proto.TensorProto.INT64, + ) + raw_components = scope.get_unique_variable_name("raw_date_components") + container.add_node( + "Cast", + [integer_components], + [raw_components], + name=scope.get_unique_operator_name("cast_date_components"), + op_domain="", + to=onnx_proto.TensorProto.DOUBLE, + ) + + cyclic_index_name = _add_int64_initializer( + scope, container, "cyclic_indices", cyclic_indices + ) + cyclic_components = scope.get_unique_variable_name("cyclic_components") + container.add_node( + "Gather", + [raw_components, cyclic_index_name], + [cyclic_components], + name=scope.get_unique_operator_name("gather_cyclic_components"), + op_domain="", + axis=1, + ) + + angle_scales = (2 * np.pi) / cyclic_periods + angle_scale_name = scope.get_unique_variable_name("cyclic_angle_scales") + container.add_initializer( + name=angle_scale_name, + onnx_type=onnx_proto.TensorProto.DOUBLE, + shape=list(angle_scales.shape), + content=angle_scales, + ) + double_angles = scope.get_unique_variable_name("double_cyclic_angles") + angles = scope.get_unique_variable_name("cyclic_angles") + container.add_node( + "Mul", + [cyclic_components, angle_scale_name], + [double_angles], + name=scope.get_unique_operator_name("scale_cyclic_components"), + op_domain="", + ) + container.add_node( + "Cast", + [double_angles], + [angles], + name=scope.get_unique_operator_name("cast_cyclic_angles"), + op_domain="", + to=onnx_proto.TensorProto.FLOAT, + ) + unrounded_sin = scope.get_unique_variable_name("unrounded_cyclic_sin") + unrounded_cos = scope.get_unique_variable_name("unrounded_cyclic_cos") + container.add_node( + "Sin", + [angles], + [unrounded_sin], + name=scope.get_unique_operator_name("sin_cyclic_components"), + op_domain="", + ) + container.add_node( + "Cos", + [angles], + [unrounded_cos], + name=scope.get_unique_operator_name("cos_cyclic_components"), + op_domain="", + ) + + quantization_scale_name = scope.get_unique_variable_name( + "cyclic_quantization_scale" + ) + container.add_initializer( + name=quantization_scale_name, + onnx_type=onnx_proto.TensorProto.FLOAT, + shape=[], + # A one-element sequence rather than a 0-d array: onnx below 1.22 takes the + # length of the content even for a scalar initializer, which an unsized array + # cannot supply. + content=[float(_CYCLIC_QUANTIZATION_SCALE)], + ) + quantized_values: list[str] = [] + for function_name, unrounded_values in ( + ("sin", unrounded_sin), + ("cos", unrounded_cos), + ): + scaled_values = scope.get_unique_variable_name(f"scaled_cyclic_{function_name}") + rounded_values = scope.get_unique_variable_name( + f"rounded_cyclic_{function_name}" + ) + float_values = scope.get_unique_variable_name(f"float_cyclic_{function_name}") + double_values = scope.get_unique_variable_name(f"double_cyclic_{function_name}") + container.add_node( + "Mul", + [unrounded_values, quantization_scale_name], + [scaled_values], + name=scope.get_unique_operator_name( + f"scale_cyclic_{function_name}_for_rounding" + ), + op_domain="", + ) + container.add_node( + "Round", + [scaled_values], + [rounded_values], + name=scope.get_unique_operator_name(f"round_cyclic_{function_name}"), + op_domain="", + ) + container.add_node( + "Div", + [rounded_values, quantization_scale_name], + [float_values], + name=scope.get_unique_operator_name( + f"unscale_cyclic_{function_name}_after_rounding" + ), + op_domain="", + ) + container.add_node( + "Cast", + [float_values], + [double_values], + name=scope.get_unique_operator_name(f"cast_cyclic_{function_name}"), + op_domain="", + to=onnx_proto.TensorProto.DOUBLE, + ) + quantized_values.append(double_values) + + missing_indicator = scope.get_unique_variable_name("date_missing_indicator") + container.add_node( + "Cast", + [missing], + [missing_indicator], + name=scope.get_unique_operator_name("cast_date_missing_indicator"), + op_domain="", + to=onnx_proto.TensorProto.DOUBLE, + ) container.add_node( - "Squeeze", [tokenized, axis_name], [out_name], name=sq_name, op_domain="" + "Concat", + [raw_components, *quantized_values, missing_indicator], + [output_name], + name=scope.get_unique_operator_name("concat_date_features"), + op_domain="", + axis=1, ) diff --git a/falcon/addons/sklearn/preprocessing/missing_values.py b/falcon/addons/sklearn/preprocessing/missing_values.py new file mode 100644 index 0000000..66121fd --- /dev/null +++ b/falcon/addons/sklearn/preprocessing/missing_values.py @@ -0,0 +1,315 @@ +from __future__ import annotations + +from typing import Any + +import numpy as np +import pandas as pd +from numpy import typing as npt +from skl2onnx import update_registered_converter +from skl2onnx.common.data_types import FloatTensorType, StringTensorType +from skl2onnx.proto import onnx_proto +from sklearn.base import BaseEstimator, TransformerMixin +from sklearn.utils.validation import check_is_fitted + +MISSING_STRING_TOKENS: tuple[str, ...] = ("nan", "None", "", "NaT") + + +def string_values_and_missing_mask( + X: npt.NDArray[Any], +) -> tuple[npt.NDArray[np.str_], npt.NDArray[np.bool_]]: + values = np.asarray(X, dtype=np.object_) + if values.ndim not in (1, 2): + raise ValueError("String features must be one- or two-dimensional") + missing = np.asarray(pd.isna(values), dtype=np.bool_) + strings = values.astype(np.str_) + missing |= np.isin(strings, MISSING_STRING_TOKENS) + return strings, missing + + +def numeric_values(X: npt.NDArray[Any]) -> npt.NDArray[np.float64]: + raw_values = np.asarray(X, dtype=np.object_) + if raw_values.ndim != 2: + raise ValueError("Numeric features must be two-dimensional") + missing = np.asarray(pd.isna(raw_values), dtype=np.bool_) + values = np.empty(raw_values.shape, dtype=np.float64) + values[missing] = np.nan + try: + values[~missing] = raw_values[~missing].astype(np.float64) + except (TypeError, ValueError) as error: + raise ValueError("Numeric features contain a non-numeric value") from error + return values + + +class NumericMedianImputer(BaseEstimator, TransformerMixin): + statistics_: npt.NDArray[np.float64] + n_features_in_: int + + def fit(self, X: npt.NDArray[Any], y: Any = None) -> NumericMedianImputer: + values = numeric_values(X) + all_missing = np.isnan(values).all(axis=0) + if all_missing.any(): + column = int(np.flatnonzero(all_missing)[0]) + raise ValueError(f"Cannot impute all-missing numeric column {column}") + self.statistics_ = np.nanmedian(values, axis=0) + self.n_features_in_ = values.shape[1] + return self + + def transform(self, X: npt.NDArray[Any]) -> npt.NDArray[np.float32]: + check_is_fitted(self, ("statistics_", "n_features_in_")) + values = numeric_values(X) + if values.shape[1] != self.n_features_in_: + raise ValueError("Numeric feature count differs from fitted data") + missing = np.isnan(values) + filled = np.where(missing, self.statistics_, values) + return np.concatenate((filled, missing.astype(np.float64)), axis=1).astype( + np.float32 + ) + + +class NumericCast(BaseEstimator, TransformerMixin): + """ + Numeric counterpart of `NumericMedianImputer` for pipelines built without + imputation. Exports to a single `Cast` node so that no `IsNaN`/`Where` pair + ends up in the graph, and emits no missing indicator column. + """ + + n_features_in_: int + + def fit(self, X: npt.NDArray[Any], y: Any = None) -> NumericCast: + values = numeric_values(X) + if np.isnan(values).any(): + column = int(np.flatnonzero(np.isnan(values).any(axis=0))[0]) + raise ValueError( + f"Numeric column {column} contains missing values, which cannot be " + "handled while imputation is disabled" + ) + self.n_features_in_ = values.shape[1] + return self + + def transform(self, X: npt.NDArray[Any]) -> npt.NDArray[np.float32]: + check_is_fitted(self, "n_features_in_") + values = numeric_values(X) + if values.shape[1] != self.n_features_in_: + raise ValueError("Numeric feature count differs from fitted data") + return values.astype(np.float32) + + +class MissingStringImputer(BaseEstimator, TransformerMixin): + fill_value: str + n_features_in_: int + + def __init__(self, fill_value: str) -> None: + self.fill_value = fill_value + + def fit(self, X: npt.NDArray[Any], y: Any = None) -> MissingStringImputer: + values, _ = string_values_and_missing_mask(X) + self.n_features_in_ = values.shape[1] if values.ndim == 2 else 1 + return self + + def transform(self, X: npt.NDArray[Any]) -> npt.NDArray[np.str_]: + check_is_fitted(self, "n_features_in_") + values, missing = string_values_and_missing_mask(X) + n_features = values.shape[1] if values.ndim == 2 else 1 + if n_features != self.n_features_in_: + raise ValueError("String feature count differs from fitted data") + return np.where(missing, self.fill_value, values).astype(np.str_) + + +class StringCast(BaseEstimator, TransformerMixin): + """ + String counterpart of `MissingStringImputer` for pipelines built without + imputation. Missing values keep their plain string representation + (`"nan"`, `"None"`, ...) and are encoded as ordinary categories, which keeps + the exported graph free of the `Equal`/`Or`/`Where` mask. + """ + + n_features_in_: int + + def fit(self, X: npt.NDArray[Any], y: Any = None) -> StringCast: + values, _ = string_values_and_missing_mask(X) + self.n_features_in_ = values.shape[1] if values.ndim == 2 else 1 + return self + + def transform(self, X: npt.NDArray[Any]) -> npt.NDArray[np.str_]: + check_is_fitted(self, "n_features_in_") + values, _ = string_values_and_missing_mask(X) + n_features = values.shape[1] if values.ndim == 2 else 1 + if n_features != self.n_features_in_: + raise ValueError("String feature count differs from fitted data") + return values + + +def add_string_missing_mask( + scope: Any, + container: Any, + input_name: str, + name: str, +) -> str: + comparisons: list[str] = [] + for index, token in enumerate(MISSING_STRING_TOKENS): + token_name = scope.get_unique_variable_name(f"{name}_token_{index}") + container.add_initializer( + token_name, + onnx_proto.TensorProto.STRING, + [], + [token], + ) + comparison = scope.get_unique_variable_name(f"{name}_equal_{index}") + container.add_node( + "Equal", + [input_name, token_name], + [comparison], + name=scope.get_unique_operator_name(f"{name}_equal_{index}"), + op_domain="", + ) + comparisons.append(comparison) + + missing = comparisons[0] + for index, comparison in enumerate(comparisons[1:], start=1): + combined = scope.get_unique_variable_name(f"{name}_or_{index}") + container.add_node( + "Or", + [missing, comparison], + [combined], + name=scope.get_unique_operator_name(f"{name}_or_{index}"), + op_domain="", + ) + missing = combined + return missing + + +def _numeric_shape_calculator(operator: Any) -> None: + batch_size = operator.inputs[0].get_first_dimension() + feature_count = operator.inputs[0].type.shape[1] + output_features = None if feature_count is None else feature_count * 2 + operator.outputs[0].type = FloatTensorType([batch_size, output_features]) + + +def _numeric_converter(scope: Any, operator: Any, container: Any) -> None: + transformer: NumericMedianImputer = operator.raw_operator + check_is_fitted(transformer, ("statistics_", "n_features_in_")) + input_name = operator.inputs[0].full_name + output_name = operator.outputs[0].full_name + + missing = scope.get_unique_variable_name("numeric_missing") + container.add_node( + "IsNaN", + [input_name], + [missing], + name=scope.get_unique_operator_name("numeric_is_nan"), + op_domain="", + ) + statistics = scope.get_unique_variable_name("numeric_medians") + container.add_initializer( + statistics, + onnx_proto.TensorProto.FLOAT, + [1, transformer.n_features_in_], + transformer.statistics_.astype(np.float32).reshape(1, -1), + ) + filled = scope.get_unique_variable_name("imputed_numeric_values") + container.add_node( + "Where", + [missing, statistics, input_name], + [filled], + name=scope.get_unique_operator_name("impute_numeric_values"), + op_domain="", + ) + indicator = scope.get_unique_variable_name("numeric_missing_indicator") + container.add_node( + "Cast", + [missing], + [indicator], + name=scope.get_unique_operator_name("cast_numeric_missing_indicator"), + op_domain="", + to=onnx_proto.TensorProto.FLOAT, + ) + container.add_node( + "Concat", + [filled, indicator], + [output_name], + name=scope.get_unique_operator_name("append_numeric_missing_indicator"), + op_domain="", + axis=1, + ) + + +def _numeric_cast_shape_calculator(operator: Any) -> None: + batch_size = operator.inputs[0].get_first_dimension() + operator.outputs[0].type = FloatTensorType( + [batch_size, operator.inputs[0].type.shape[1]] + ) + + +def _numeric_cast_converter(scope: Any, operator: Any, container: Any) -> None: + check_is_fitted(operator.raw_operator, "n_features_in_") + container.add_node( + "Cast", + [operator.inputs[0].full_name], + [operator.outputs[0].full_name], + name=scope.get_unique_operator_name("cast_numeric_values"), + op_domain="", + to=onnx_proto.TensorProto.FLOAT, + ) + + +def _string_shape_calculator(operator: Any) -> None: + operator.outputs[0].type = StringTensorType(operator.inputs[0].type.shape) + + +def _string_converter(scope: Any, operator: Any, container: Any) -> None: + transformer: MissingStringImputer = operator.raw_operator + check_is_fitted(transformer, "n_features_in_") + input_name = operator.inputs[0].full_name + output_name = operator.outputs[0].full_name + missing = add_string_missing_mask(scope, container, input_name, "string_missing") + fill_value = scope.get_unique_variable_name("string_fill_value") + container.add_initializer( + fill_value, + onnx_proto.TensorProto.STRING, + [], + [transformer.fill_value], + ) + container.add_node( + "Where", + [missing, fill_value, input_name], + [output_name], + name=scope.get_unique_operator_name("impute_string_values"), + op_domain="", + ) + + +def _string_cast_converter(scope: Any, operator: Any, container: Any) -> None: + check_is_fitted(operator.raw_operator, "n_features_in_") + container.add_node( + "Identity", + [operator.inputs[0].full_name], + [operator.outputs[0].full_name], + name=scope.get_unique_operator_name("pass_string_values"), + op_domain="", + ) + + +update_registered_converter( + NumericMedianImputer, + "FalconNumericMedianImputer", + _numeric_shape_calculator, + _numeric_converter, +) +update_registered_converter( + NumericCast, + "FalconNumericCast", + _numeric_cast_shape_calculator, + _numeric_cast_converter, +) +update_registered_converter( + MissingStringImputer, + "FalconMissingStringImputer", + _string_shape_calculator, + _string_converter, +) +update_registered_converter( + StringCast, + "FalconStringCast", + _string_shape_calculator, + _string_cast_converter, +) diff --git a/falcon/addons/sklearn/preprocessing/target_encoder.py b/falcon/addons/sklearn/preprocessing/target_encoder.py new file mode 100644 index 0000000..df42352 --- /dev/null +++ b/falcon/addons/sklearn/preprocessing/target_encoder.py @@ -0,0 +1,164 @@ +from __future__ import annotations + +from typing import Any, cast + +import numpy as np +from numpy import typing as npt +from skl2onnx import update_registered_converter +from skl2onnx.common._apply_operation import apply_concat +from skl2onnx.common.data_types import FloatTensorType +from skl2onnx.proto import onnx_proto +from sklearn.base import clone +from sklearn.preprocessing import TargetEncoder +from sklearn.utils.validation import check_is_fitted + +from falcon.tabular.splitting import cross_validation_indices +from falcon.types import TargetKind + + +class FalconTargetEncoder(TargetEncoder): + def fit_transform( + self, X: npt.NDArray[Any], y: npt.NDArray[Any] + ) -> npt.NDArray[np.float64]: + return self.fit(X, y).transform(X) + + def cross_fit_transform( + self, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + split_features: npt.NDArray[Any], + target_kind: TargetKind, + *, + groups: npt.ArrayLike | None = None, + ) -> npt.NDArray[np.float64]: + check_is_fitted( + self, ("categories_", "encodings_", "target_mean_", "target_type_") + ) + values = np.asarray(X) + targets = np.asarray(y).reshape(-1) + if values.ndim != 2 or values.shape[0] != targets.shape[0]: + raise ValueError("Target-encoder inputs must contain one target per row") + + transformed = np.empty( + (values.shape[0], len(self.encodings_)), dtype=np.float64 + ) + task = f"tabular_{target_kind}" + splits = cross_validation_indices( + split_features, + targets, + task=task, + groups=groups, + n_splits=int(self.cv), + ) + for train_indices, validation_indices in splits: + fold_encoder = cast(FalconTargetEncoder, clone(self)) + fold_encoder.fit(values[train_indices], targets[train_indices]) + if hasattr(self, "classes_") and not np.array_equal( + fold_encoder.classes_, self.classes_ + ): + raise ValueError( + "Every target-encoding fold must contain all target classes" + ) + fold_values = fold_encoder.transform(values[validation_indices]) + if fold_values.shape[1] != transformed.shape[1]: + raise ValueError( + "Target-encoding folds produced inconsistent feature counts" + ) + transformed[validation_indices] = fold_values + return transformed + + +def _target_encoder_shape_calculator(operator: Any) -> None: + encoder: FalconTargetEncoder = operator.raw_operator + check_is_fitted(encoder, ("categories_", "encodings_")) + batch_size = operator.inputs[0].get_first_dimension() + operator.outputs[0].type = FloatTensorType([batch_size, len(encoder.encodings_)]) + + +def _feature_input_name( + scope: Any, + container: Any, + input_name: str, + feature_index: int, + feature_count: int, +) -> str: + if feature_count == 1: + return input_name + index_name = scope.get_unique_variable_name("target_encoder_feature_index") + container.add_initializer( + index_name, + onnx_proto.TensorProto.INT64, + [], + [feature_index], + ) + feature_name = scope.get_unique_variable_name("target_encoder_feature") + container.add_node( + "ArrayFeatureExtractor", + [input_name, index_name], + [feature_name], + name=scope.get_unique_operator_name("target_encoder_feature"), + op_domain="ai.onnx.ml", + op_version=1, + ) + return feature_name + + +def _target_encoder_converter(scope: Any, operator: Any, container: Any) -> None: + encoder: FalconTargetEncoder = operator.raw_operator + check_is_fitted( + encoder, ("categories_", "encodings_", "target_mean_", "target_type_") + ) + class_count = len(encoder.classes_) if encoder.target_type_ == "multiclass" else 1 + if len(encoder.encodings_) != len(encoder.categories_) * class_count: + raise RuntimeError("Target encoder has inconsistent fitted mappings") + + target_means = np.asarray(encoder.target_mean_, dtype=np.float32).reshape(-1) + encoded_outputs: list[str] = [] + input_name = operator.inputs[0].full_name + for feature_index, categories in enumerate(encoder.categories_): + feature_name = _feature_input_name( + scope, + container, + input_name, + feature_index, + len(encoder.categories_), + ) + for class_index in range(class_count): + encoding_index = feature_index * class_count + class_index + output_name = scope.get_unique_variable_name("target_encoded_feature") + encoded_outputs.append(output_name) + container.add_node( + "LabelEncoder", + [feature_name], + [output_name], + name=scope.get_unique_operator_name("target_encoder_mapping"), + op_domain="ai.onnx.ml", + op_version=2, + keys_strings=np.asarray( + [str(category).encode("utf-8") for category in categories] + ), + values_floats=np.asarray( + encoder.encodings_[encoding_index], dtype=np.float32 + ), + default_float=float(target_means[class_index]), + ) + + output_name = operator.outputs[0].full_name + if len(encoded_outputs) == 1: + container.add_node( + "Identity", + encoded_outputs, + [output_name], + name=scope.get_unique_operator_name("target_encoder_output"), + op_domain="", + ) + else: + apply_concat(scope, encoded_outputs, output_name, container, axis=1) + + +update_registered_converter( + FalconTargetEncoder, + "FalconTargetEncoder", + _target_encoder_shape_calculator, + _target_encoder_converter, +) diff --git a/falcon/addons/sklearn/preprocessing/text_vectorizer.py b/falcon/addons/sklearn/preprocessing/text_vectorizer.py new file mode 100644 index 0000000..a5d2267 --- /dev/null +++ b/falcon/addons/sklearn/preprocessing/text_vectorizer.py @@ -0,0 +1,140 @@ +from __future__ import annotations + +from typing import Any + +import numpy as np +from skl2onnx import update_registered_converter +from skl2onnx.common._apply_operation import ( + apply_identity, + apply_normalizer, + apply_reshape, +) +from skl2onnx.common.data_types import FloatTensorType +from sklearn.feature_extraction.text import TfidfVectorizer +from sklearn.utils.validation import check_is_fitted + +_ASCII_LOWERCASE = str.maketrans( + "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz" +) + + +def _lower_ascii(text: str) -> str: + return text.translate(_ASCII_LOWERCASE) + + +class FalconTfidfVectorizer(TfidfVectorizer): + def __init__( + self, + *, + max_features: int | None = 1024, + stop_words: str | list[str] | None = "english", + ) -> None: + super().__init__( + analyzer="word", + input="content", + lowercase=False, + max_features=max_features, + preprocessor=_lower_ascii, + stop_words=stop_words, + token_pattern=r"(?u)[^ ]+", + ) + + +def _text_shape_calculator(operator: Any) -> None: + vectorizer: FalconTfidfVectorizer = operator.raw_operator + check_is_fitted(vectorizer, "vocabulary_") + batch_size = operator.inputs[0].get_first_dimension() + operator.outputs[0].type = FloatTensorType( + [batch_size, len(vectorizer.vocabulary_)] + ) + + +def _ordered_vocabulary(vectorizer: FalconTfidfVectorizer) -> list[str]: + vocabulary = vectorizer.vocabulary_ + expected_indices = list(range(len(vocabulary))) + if sorted(vocabulary.values()) != expected_indices: + raise RuntimeError("Text vocabulary indices must be contiguous") + + terms = ["" for _ in vocabulary] + for term, index in vocabulary.items(): + terms[index] = term + return terms + + +def _text_converter(scope: Any, operator: Any, container: Any) -> None: + vectorizer: FalconTfidfVectorizer = operator.raw_operator + check_is_fitted(vectorizer, ("vocabulary_", "idf_")) + vocabulary = _ordered_vocabulary(vectorizer) + + flattened = scope.get_unique_variable_name("flattened_documents") + apply_reshape( + scope, + operator.inputs[0].full_name, + flattened, + container, + desired_shape=(-1,), + ) + + normalized = scope.get_unique_variable_name("normalized_documents") + container.add_node( + "StringNormalizer", + [flattened], + [normalized], + name=scope.get_unique_operator_name("StringNormalizer"), + op_domain="", + op_version=10, + case_change_action="LOWER", + is_case_sensitive=0, + locale="C", + ) + + tokens = scope.get_unique_variable_name("document_tokens") + token_counts = scope.get_unique_variable_name("document_token_counts") + container.add_node( + "StringSplit", + [normalized], + [tokens, token_counts], + name=scope.get_unique_operator_name("StringSplit"), + op_domain="", + op_version=20, + delimiter=" ", + ) + + tfidf = scope.get_unique_variable_name("tfidf_features") + container.add_node( + "TfIdfVectorizer", + [tokens], + [tfidf], + name=scope.get_unique_operator_name("TfIdfVectorizer"), + op_domain="", + op_version=9, + max_gram_length=1, + max_skip_count=0, + min_gram_length=1, + mode="TFIDF", + ngram_counts=[0], + ngram_indexes=list(range(len(vocabulary))), + pool_strings=vocabulary, + weights=list(np.asarray(vectorizer.idf_, dtype=np.float32)), + ) + + output_name = operator.outputs[0].full_name + if vectorizer.norm is None: + apply_identity(scope, tfidf, output_name, container) + else: + apply_normalizer( + scope, + tfidf, + output_name, + container, + norm=vectorizer.norm.upper(), + use_float=True, + ) + + +update_registered_converter( + FalconTfidfVectorizer, + "FalconTfidfVectorizer", + _text_shape_calculator, + _text_converter, +) diff --git a/falcon/codegen/__init__.py b/falcon/codegen/__init__.py new file mode 100644 index 0000000..6165d5a --- /dev/null +++ b/falcon/codegen/__init__.py @@ -0,0 +1,13 @@ +"""Ahead-of-time code generation from an exported falcon model.""" + +from falcon.codegen.bundle import CodegenError +from falcon.codegen.c import CArtifact, compile_to_c +from falcon.codegen.graph import CategoricalMapping, StringMapping + +__all__ = [ + "CArtifact", + "CategoricalMapping", + "CodegenError", + "StringMapping", + "compile_to_c", +] diff --git a/falcon/codegen/bundle.py b/falcon/codegen/bundle.py new file mode 100644 index 0000000..ee54529 --- /dev/null +++ b/falcon/codegen/bundle.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +import json +import tarfile +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +import onnx + +from falcon.constants import DEFAULT_PRODUCER_NAME + +MANIFEST_FILE = "manifest.json" +VARIANT_FILE = "variant_config.json" +ONNX_MODEL_FILE = "ops_artifacts/onnx_main/model.onnx" + + +class CodegenError(Exception): + """Raised when a model cannot be turned into C.""" + + +@dataclass(frozen=True) +class Bundle: + """The parts of an exported `.fnnx` file the C code generator reads.""" + + model: onnx.ModelProto + manifest: dict[str, Any] + variant: dict[str, Any] + + @property + def name(self) -> str: + tags = self.manifest.get("producer_tags", []) + for tag in tags: + if tag.startswith(f"{DEFAULT_PRODUCER_NAME}::tabular_"): + return tag.split("::", 1)[1].split(":", 1)[0] + return "falcon_model" + + @property + def output_names(self) -> list[str]: + return [output["name"] for output in self.manifest.get("outputs", [])] + + def column_types(self) -> dict[str, str]: + """Falcon's inferred type per feature column, keyed by column name.""" + schema = self.manifest.get("schema") + if not isinstance(schema, dict): + return {} + return { + column["name"]: column["type"] + for column in schema.get("columns", []) + if isinstance(column, dict) + } + + +def read_bundle(path: str | Path) -> Bundle: + """Read a `.fnnx` file, or an already unpacked bundle directory.""" + source = Path(path) + if not source.exists(): + raise CodegenError(f"Model not found: `{source}`.") + reader = _read_directory if source.is_dir() else _read_archive + try: + model_bytes, manifest_bytes, variant_bytes = reader(source) + except (KeyError, OSError, tarfile.TarError) as error: + raise CodegenError( + f"Could not read the FNNX bundle `{source}`: {error}." + ) from error + + manifest = json.loads(manifest_bytes) + if manifest.get("variant") != "pipeline": + raise CodegenError( + f"`{source}` is an FNNX `{manifest.get('variant')}` bundle; the C code " + "generator only handles the `pipeline` variant that falcon exports." + ) + return Bundle( + model=onnx.load_from_string(model_bytes), + manifest=manifest, + variant=json.loads(variant_bytes), + ) + + +def _read_directory(source: Path) -> tuple[bytes, bytes, bytes]: + return ( + (source / ONNX_MODEL_FILE).read_bytes(), + (source / MANIFEST_FILE).read_bytes(), + (source / VARIANT_FILE).read_bytes(), + ) + + +def _read_archive(source: Path) -> tuple[bytes, bytes, bytes]: + with tarfile.open(source) as archive: + return ( + _member(archive, ONNX_MODEL_FILE), + _member(archive, MANIFEST_FILE), + _member(archive, VARIANT_FILE), + ) + + +def _member(archive: tarfile.TarFile, name: str) -> bytes: + handle = archive.extractfile(name) + if handle is None: + raise KeyError(f"`{name}` is missing or is not a file") + return handle.read() diff --git a/falcon/codegen/c.py b/falcon/codegen/c.py new file mode 100644 index 0000000..b16154f --- /dev/null +++ b/falcon/codegen/c.py @@ -0,0 +1,145 @@ +"""Generating a self-contained C99 artifact from an exported falcon model.""" + +from __future__ import annotations + +import re +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +from falcon import __version__ +from falcon.codegen.bundle import Bundle, CodegenError, read_bundle +from falcon.codegen.graph import StringMapping, resolve_strings +from falcon.codegen.helper import render_helper +from falcon.utils import logger + +DEFAULT_BATCH_SIZE = 128 + +_COLUMN_NOTES = { + "NUMERIC_REGULAR": "numeric feature, as-is", + "CAT_LOW_CARD": "category code from {prefix}_encode_{name}()", + "CAT_HIGH_CARD": "category code from {prefix}_encode_{name}()", +} + + +@dataclass(frozen=True) +class CArtifact: + """The files written for one model, and the string tables they were built with.""" + + header_path: Path + helper_path: Path + report_path: Path + mapping: StringMapping + report: dict[str, Any] + + @property + def prefix(self) -> str: + return str(self.report["prefix"]) + + @property + def entrypoint(self) -> str: + return f"{self.prefix}_run" + + @property + def batch_size(self) -> int: + dims = self.report.get("runtime_dims") or [] + return int(dims[0]["max"]) if dims else 1 + + +def compile_to_c( + model_path: str | Path, + output_dir: str | Path, + *, + prefix: str | None = None, + batch_size: int = DEFAULT_BATCH_SIZE, +) -> CArtifact: + """Compile an exported `.fnnx` model into a self-contained C99 artifact. + + Writes three files into `output_dir`: `.h` with the model as straight-line C, + `_falcon.h` with the category and class-label tables, and the compiler's + `_report.json`. Neither header needs a runtime, an allocator, or ONNX. + + `batch_size` is the largest number of rows one call may pass; a call can pass fewer. + The model at `model_path` is left unmodified. + + Raises `CodegenError` for a model C cannot represent, in practice one with a text or + date feature. + """ + if batch_size < 1: + raise CodegenError(f"batch_size must be at least 1, got {batch_size}.") + compile_onnx = _load_compiler() + + bundle = read_bundle(model_path) + identifier = _sanitize(prefix or bundle.name) + logger.info(f"Generating C for `{identifier}` from {model_path}...") + + mapping = resolve_strings(bundle.model, bundle.column_types(), bundle.output_names) + destination = Path(output_dir) + result = compile_onnx( + bundle.model, + destination, + prefix=identifier, + runtime_dims={"batch": batch_size}, + ) + + helper_path = destination / f"{identifier}_falcon.h" + helper_path.write_text( + render_helper( + identifier, + mapping, + version=__version__, + inputs=_input_notes(identifier, result.report, bundle), + outputs=[ + (tensor["c_name"], tensor["c_type"]) + for tensor in result.report["entrypoint"]["outputs"] + ], + ), + encoding="utf-8", + ) + logger.info( + f"Wrote {result.header_path.name} and {helper_path.name} to {destination} " + f"({result.report['memory']['static_bytes'] / 1024:.1f} KiB static)." + ) + return CArtifact( + header_path=result.header_path, + helper_path=helper_path, + report_path=result.report_path, + mapping=mapping, + report=result.report, + ) + + +def _input_notes( + prefix: str, report: dict[str, Any], bundle: Bundle +) -> list[tuple[str, str, str]]: + column_types = bundle.column_types() + notes = [] + for tensor in report["entrypoint"]["inputs"]: + column_type = column_types.get(tensor["name"], "unknown") + template = _COLUMN_NOTES.get(column_type, f"{column_type} feature") + notes.append( + ( + tensor["c_name"], + tensor["c_type"], + template.format(prefix=prefix, name=tensor["name"]), + ) + ) + return notes + + +def _load_compiler() -> Any: + try: + from fnnx.extras.compilers.c import compile_onnx + except ImportError as error: + raise CodegenError( + "Generating C requires the FNNX ahead-of-time compiler. Install it with " + '`pip install "fnnx[compiler]"`.' + ) from error + return compile_onnx + + +def _sanitize(name: str) -> str: + identifier = re.sub(r"[^0-9a-zA-Z_]", "_", name).strip("_").lower() + if not identifier or identifier[0].isdigit(): + identifier = f"model_{identifier}" if identifier else "model" + return identifier diff --git a/falcon/codegen/graph.py b/falcon/codegen/graph.py new file mode 100644 index 0000000..5128842 --- /dev/null +++ b/falcon/codegen/graph.py @@ -0,0 +1,358 @@ +"""Resolving falcon's string tensors into integer codes, at code generation time. + +C has no string tensor, so before the graph reaches the FNNX compiler every string is +replaced by the integer indexing it: a category by its position in the fitted vocabulary, +a predicted label by its class index. The vocabularies leave as `StringMapping`, which the +generated helper header turns into lookup tables. The exported `.fnnx` file is untouched. +""" + +from __future__ import annotations + +from collections.abc import Iterator +from dataclasses import dataclass + +import onnx +from onnx import GraphProto, ModelProto, NodeProto, TensorProto, helper +from onnx.numpy_helper import to_array + +from falcon.codegen.bundle import CodegenError + +# Column types encoded by a lookup keyed on the whole input value, which is what makes an +# integer code a faithful stand-in for the string. +CODEABLE_COLUMN_TYPES = frozenset({"CAT_LOW_CARD", "CAT_HIGH_CARD"}) + +# Ops that carry a string through without inspecting it, so tracing walks past them. +VIEW_OPS = frozenset({"Identity", "Reshape", "Gather"}) + +LOOKUP_KEYS = {"OneHotEncoder": "cats_strings", "LabelEncoder": "keys_strings"} +LOOKUP_CODES = {"OneHotEncoder": "cats_int64s", "LabelEncoder": "keys_int64s"} + +# Attributes that hold string tensor data, as opposed to string attributes that only +# configure a kernel (a locale, a mode). +STRING_VALUED_ATTRIBUTES = frozenset( + { + "cats_strings", + "classlabels_strings", + "keys_strings", + "pool_strings", + "values_strings", + } +) + + +@dataclass(frozen=True) +class CategoricalMapping: + """How one categorical input's strings map to the codes the artifact consumes.""" + + name: str + column_type: str + categories: tuple[str, ...] + missing_tokens: tuple[str, ...] + missing_code: int + + @property + def imputed(self) -> bool: + return bool(self.missing_tokens) + + +@dataclass(frozen=True) +class StringMapping: + categoricals: tuple[CategoricalMapping, ...] + class_labels: tuple[str, ...] + + +def resolve_strings( + model: ModelProto, column_types: dict[str, str], output_names: list[str] +) -> StringMapping: + """Rewrite `model` in place so no tensor is a string, and return what was replaced. + + Raises `CodegenError` for a feature whose encoding reads inside the string rather than + looking the whole value up (free text, dates), since no integer stands in for those. + """ + graph = model.graph + _reject_uncodeable_columns(graph, column_types) + + categoricals = tuple( + _recode_categorical(graph, value_info.name, column_types) + for value_info in graph.input + if value_info.type.tensor_type.elem_type == TensorProto.STRING + ) + class_labels = _drop_label_decoder(graph) + + _prune(graph) + _name_batch_dimension(graph) + _rename_outputs(graph, output_names) + del graph.value_info[:] + + _reject_remaining_strings(graph) + onnx.checker.check_model(model, full_check=False) + return StringMapping(categoricals=categoricals, class_labels=class_labels) + + +def _reject_uncodeable_columns(graph: GraphProto, column_types: dict[str, str]) -> None: + offenders = [ + (value_info.name, column_types.get(value_info.name, "unknown")) + for value_info in graph.input + if value_info.type.tensor_type.elem_type == TensorProto.STRING + and column_types.get(value_info.name, "unknown") not in CODEABLE_COLUMN_TYPES + ] + if not offenders: + return + listed = ", ".join(f"`{name}` ({column_type})" for name, column_type in offenders) + raise CodegenError( + f"Cannot generate C for {listed}. Text and date features are encoded by " + "splitting and parsing the string itself, so no integer code can stand in for " + "it the way one can for a category. Drop these columns from `features`, or " + "deploy the model through the FNNX runtime instead." + ) + + +def _recode_categorical( + graph: GraphProto, name: str, column_types: dict[str, str] +) -> CategoricalMapping: + path, terminal = _trace(graph, name) + attribute_name = LOOKUP_KEYS[terminal.op_type] + keys = _attribute(terminal, attribute_name) + categories = tuple(value.decode() for value in keys.strings) + if terminal.op_type == "OneHotEncoder" and _zeros(terminal) != 1: + raise CodegenError( + f"The OneHotEncoder for `{name}` reports an unseen category as an error " + "rather than an all-zero row, which a code the artifact never saw would " + "trigger at inference." + ) + + terminal.attribute.remove(keys) + terminal.attribute.append( + helper.make_attribute( + LOOKUP_CODES[terminal.op_type], list(range(len(categories))) + ) + ) + + missing_tokens, fill_value = _elide_imputation(graph, path) + _set_input_type(graph, name, TensorProto.INT64) + return CategoricalMapping( + name=name, + column_type=column_types.get(name, "unknown"), + categories=categories, + missing_tokens=missing_tokens, + missing_code=categories.index(fill_value) if fill_value in categories else -1, + ) + + +def _trace(graph: GraphProto, name: str) -> tuple[list[NodeProto], NodeProto]: + """Walk from a string input to the node that looks its value up. + + The mask-building `Equal` nodes of the string imputer also read the input, so only the + edge carrying the value itself is followed. The nodes on the way are returned so the + imputer can be elided once the vocabulary is known. + """ + path: list[NodeProto] = [] + current = name + while True: + candidates = [ + node for node in _consumers(graph, current) if _reads_value(node, current) + ] + if len(candidates) != 1: + raise CodegenError( + f"Cannot generate C for `{name}`: expected its value to flow into exactly " + f"one operation, found {len(candidates)}." + ) + node = candidates[0] + if node.op_type in LOOKUP_KEYS: + return path, node + if node.op_type not in VIEW_OPS and node.op_type != "Where": + raise CodegenError( + f"Cannot generate C for `{name}`: it flows into `{node.op_type}`, which " + "is not a category lookup." + ) + path.append(node) + current = node.output[0] + + +def _reads_value(node: NodeProto, name: str) -> bool: + """Whether `name` reaches `node` as the data being encoded, not as a mask or index.""" + if node.op_type == "Where": + return len(node.input) > 2 and node.input[2] == name + if node.op_type == "Equal": + return False + return bool(node.input) and node.input[0] == name + + +def _elide_imputation( + graph: GraphProto, path: list[NodeProto] +) -> tuple[tuple[str, ...], str | None]: + """Turn the string imputer's `Where` into a pass-through and report what it filled. + + The fill is not lost: the caller maps a missing string to the fill value's code in the + generated helper, so the lookup still sees the category the `Where` produced. + """ + initializers = {tensor.name: tensor for tensor in graph.initializer} + tokens: tuple[str, ...] = () + fill_value: str | None = None + for node in path: + if node.op_type != "Where": + continue + fill_value = _string_constant(initializers, node.input[1]) + tokens = _missing_tokens(graph, initializers, node.input[0]) + data = node.input[2] + node.op_type = "Identity" + del node.input[:] + node.input.append(data) + del node.attribute[:] + return tokens, fill_value + + +def _missing_tokens( + graph: GraphProto, initializers: dict[str, TensorProto], mask: str +) -> tuple[str, ...]: + """The strings the imputer treats as missing, read off the `Equal`/`Or` mask.""" + produced_by = {output: node for node in graph.node for output in node.output} + tokens: list[str] = [] + pending = [mask] + while pending: + node = produced_by.get(pending.pop()) + if node is None: + continue + if node.op_type == "Or": + pending.extend(node.input) + elif node.op_type == "Equal": + for operand in node.input: + value = _string_constant(initializers, operand) + if value is not None: + tokens.append(value) + return tuple(dict.fromkeys(tokens)) + + +def _string_constant(initializers: dict[str, TensorProto], name: str) -> str | None: + tensor = initializers.get(name) + if tensor is None or tensor.data_type != TensorProto.STRING: + return None + value = to_array(tensor).reshape(-1) + return str(value[0].decode() if isinstance(value[0], bytes) else value[0]) + + +def _drop_label_decoder(graph: GraphProto) -> tuple[str, ...]: + """Remove the `LabelEncoder` decoding class indices, leaving the index as the output.""" + outputs = {output.name: output for output in graph.output} + for node in list(graph.node): + if node.op_type != "LabelEncoder" or node.output[0] not in outputs: + continue + values = next((a for a in node.attribute if a.name == "values_strings"), None) + if values is None: + continue + value_info = outputs[node.output[0]] + value_info.name = node.input[0] + value_info.type.tensor_type.elem_type = TensorProto.INT64 + graph.node.remove(node) + return tuple(label.decode() for label in values.strings) + return () + + +def _prune(graph: GraphProto) -> None: + produced_by = {output: node for node in graph.node for output in node.output} + seen: set[str] = {output.name for output in graph.output} + live_nodes: set[int] = set() + pending = list(seen) + while pending: + node = produced_by.get(pending.pop()) + if node is None or id(node) in live_nodes: + continue + live_nodes.add(id(node)) + for name in node.input: + if name and name not in seen: + seen.add(name) + pending.append(name) + + kept = [node for node in graph.node if id(node) in live_nodes] + del graph.node[:] + graph.node.extend(kept) + + reachable = {name for node in kept for name in node.input} + retained = [tensor for tensor in graph.initializer if tensor.name in reachable] + del graph.initializer[:] + graph.initializer.extend(retained) + + +def _name_batch_dimension(graph: GraphProto) -> None: + """Give the leading axis a name so the artifact can serve a range of batch sizes.""" + for value_info in graph.input: + dimensions = value_info.type.tensor_type.shape.dim + if dimensions and not dimensions[0].HasField("dim_value"): + dimensions[0].dim_param = "batch" + + +def _rename_outputs(graph: GraphProto, names: list[str]) -> None: + """Rename graph outputs to the names the manifest advertises (`y_pred`, ...). + + The compiler derives its C parameter names from these, so the artifact ends up naming + its outputs the way the Python API does rather than after internal pipeline tensors. + """ + if len(names) != len(graph.output): + return + taken = {name for node in graph.node for name in node.input} | { + name for node in graph.node for name in node.output + } + mapping = { + value_info.name: name + for value_info, name in zip(graph.output, names, strict=True) + if value_info.name != name and name not in taken + } + for node in graph.node: + for names in (node.input, node.output): + for index, name in enumerate(names): + if name in mapping: + names[index] = mapping[name] + for value_info in graph.output: + value_info.name = mapping.get(value_info.name, value_info.name) + + +def _reject_remaining_strings(graph: GraphProto) -> None: + offenders = sorted( + { + name + for name, elem_type in _element_types(graph) + if elem_type == TensorProto.STRING + } + ) + if offenders: + raise CodegenError( + "The graph still holds string tensors after recoding: " + f"{', '.join(f'`{name}`' for name in offenders)}." + ) + + +def _element_types(graph: GraphProto) -> Iterator[tuple[str, int]]: + for value_info in list(graph.input) + list(graph.output): + yield value_info.name, value_info.type.tensor_type.elem_type + for tensor in graph.initializer: + yield tensor.name, tensor.data_type + for node in graph.node: + for attribute in node.attribute: + if attribute.name in STRING_VALUED_ATTRIBUTES and attribute.strings: + yield f"{node.name}.{attribute.name}", TensorProto.STRING + + +def _set_input_type(graph: GraphProto, name: str, elem_type: int) -> None: + for value_info in graph.input: + if value_info.name == name: + value_info.type.tensor_type.elem_type = elem_type + + +def _consumers(graph: GraphProto, name: str) -> list[NodeProto]: + return [node for node in graph.node if name in node.input] + + +def _attribute(node: NodeProto, name: str) -> onnx.AttributeProto: + for attribute in node.attribute: + if attribute.name == name: + return attribute + raise CodegenError( + f"Node `{node.name}` (`{node.op_type}`) has no `{name}` attribute." + ) + + +def _zeros(node: NodeProto) -> int: + for attribute in node.attribute: + if attribute.name == "zeros": + return int(attribute.i) + return 1 diff --git a/falcon/codegen/helper.py b/falcon/codegen/helper.py new file mode 100644 index 0000000..115a6f3 --- /dev/null +++ b/falcon/codegen/helper.py @@ -0,0 +1,232 @@ +"""Rendering the C header that carries the string tables out of the compiled graph.""" + +from __future__ import annotations + +import textwrap +from collections.abc import Iterable, Sequence + +from falcon.codegen.graph import CategoricalMapping, StringMapping + +_HEADER = """/* {prefix}_falcon.h -- generated by falcon {version}; do not edit. + * + * Companion to `{prefix}.h`, which holds the compiled model itself. C has no string + * tensor, so the categorical features reach `{prefix}_run()` as int64 category codes + * and, for classification, the prediction comes back as an int64 class index. This + * header is where the strings went: one table per categorical feature, plus the class + * labels, and the lookups that move between them. + * + * Usage -- define the implementation macro in exactly one translation unit: + * + * #define {upper}_FALCON_IMPLEMENTATION + * #include "{prefix}_falcon.h" + * + * {prefix}_run() takes its inputs in this order: + * +{inputs} + * + * and writes these outputs: + * +{outputs} + */ + +#ifndef {upper}_FALCON_H_INCLUDED +#define {upper}_FALCON_H_INCLUDED + +#include + +#ifdef __cplusplus +extern "C" {{ +#endif + +/* Returned for a value that is in no category. The model was fitted with + * `handle_unknown="ignore"`, so an unseen category contributes an all-zero encoding + * rather than failing. */ +#define {upper}_FALCON_UNKNOWN_CATEGORY (-1) + +""" + +_FOOTER = """ +#ifdef __cplusplus +}} +#endif + +#endif /* {upper}_FALCON_H_INCLUDED */ + +#ifdef {upper}_FALCON_IMPLEMENTATION +#ifndef {upper}_FALCON_IMPLEMENTATION_INCLUDED +#define {upper}_FALCON_IMPLEMENTATION_INCLUDED + +#include + +{implementations} +#endif /* {upper}_FALCON_IMPLEMENTATION_INCLUDED */ +#endif /* {upper}_FALCON_IMPLEMENTATION */ +""" + +_ENCODE_BODY = """int64_t {symbol}(const char* value) +{{ + if (value == NULL{missing_test}) {{ + return {missing_code}; + }} + for (int64_t index = 0; index < {count}; index++) {{ + if (strcmp(value, {symbol}_categories[index]) == 0) {{ + return index; + }} + }} + return {upper}_FALCON_UNKNOWN_CATEGORY; +}} +""" + +_LABEL_BODY = """const char* {prefix}_class_label(int64_t index) +{{ + if (index < 0 || index >= {macro}) {{ + return NULL; + }} + return {prefix}_class_labels[index]; +}} +""" + + +def render_helper( + prefix: str, + mapping: StringMapping, + *, + version: str, + inputs: Sequence[tuple[str, str, str]], + outputs: Sequence[tuple[str, str]], +) -> str: + """Render `_falcon.h`. + + `inputs` are `(c_name, c_type, description)` triples and `outputs` `(c_name, c_type)` + pairs, both taken from the compiler's own report so that the signature documented in + the header is the one the artifact actually has. + """ + upper = prefix.upper() + sections = [ + _HEADER.format( + prefix=prefix, + upper=upper, + version=version, + inputs=_comment_list( + f"{c_name} ({c_type}) -- {note}" for c_name, c_type, note in inputs + ), + outputs=_comment_list(f"{c_name} ({c_type})" for c_name, c_type in outputs), + ) + ] + implementations: list[str] = [] + + for categorical in mapping.categoricals: + symbol = f"{prefix}_encode_{categorical.name}" + sections.append(_declare_categorical(symbol, categorical)) + implementations.append(_define_categorical(symbol, upper, categorical)) + + if mapping.class_labels: + macro = f"{upper}_FALCON_CLASS_COUNT" + sections.append(_declare_labels(prefix, macro, mapping.class_labels)) + table = _string_table( + f"const char* const {prefix}_class_labels[{macro}]", mapping.class_labels + ) + implementations.append( + f"{table}\n{_LABEL_BODY.format(prefix=prefix, macro=macro)}" + ) + + sections.append( + _FOOTER.format(upper=upper, implementations="\n".join(implementations)) + ) + return "".join(sections) + + +def _declare_categorical(symbol: str, categorical: CategoricalMapping) -> str: + macro = f"{symbol.upper()}_COUNT" + sentences = [ + f"`{categorical.name}` ({categorical.column_type}): " + f"{len(categorical.categories)} categories, in the order the model indexes them." + ] + if categorical.imputed: + filled = ( + f"category {categorical.missing_code} " + f"(`{categorical.categories[categorical.missing_code]}`)" + if categorical.missing_code >= 0 + else "no category, giving an all-zero encoding" + ) + tokens = ", ".join(f"`{token}`" for token in categorical.missing_tokens) + sentences.append( + f"A missing value -- NULL or {tokens} -- was filled during training and " + f"maps to {filled}." + ) + else: + sentences.append( + "The model was trained without imputation, so a missing value is an " + "ordinary category if it was seen during training and unknown otherwise." + ) + return ( + f"{_block_comment(sentences)}\n" + f"#define {macro} {len(categorical.categories)}\n" + f"extern const char* const {symbol}_categories[{macro}];\n" + f"int64_t {symbol}(const char* value);\n\n" + ) + + +def _define_categorical( + symbol: str, upper: str, categorical: CategoricalMapping +) -> str: + macro = f"{symbol.upper()}_COUNT" + table = _string_table( + f"const char* const {symbol}_categories[{macro}]", categorical.categories + ) + missing_test = "".join( + f" || strcmp(value, {_quote(token)}) == 0" + for token in categorical.missing_tokens + ) + encode = _ENCODE_BODY.format( + symbol=symbol, + upper=upper, + count=macro, + missing_code=( + categorical.missing_code + if categorical.imputed + else f"{upper}_FALCON_UNKNOWN_CATEGORY" + ), + missing_test=missing_test, + ) + return f"{table}\n{encode}" + + +def _declare_labels(prefix: str, macro: str, labels: Sequence[str]) -> str: + return ( + "/* The target's class labels, indexed by the `y_pred` class index the model\n" + " * returns. */\n" + f"#define {macro} {len(labels)}\n" + f"extern const char* const {prefix}_class_labels[{macro}];\n" + f"const char* {prefix}_class_label(int64_t index);\n\n" + ) + + +def _string_table(declaration: str, values: Iterable[str]) -> str: + entries = ",\n".join(f" {_quote(value)}" for value in values) + return f"{declaration} = {{\n{entries}\n}};\n" + + +def _quote(value: str) -> str: + escaped = value.replace("\\", "\\\\").replace('"', '\\"') + escaped = "".join( + character if 0x20 <= ord(character) < 0x7F else _escape(character) + for character in escaped + ) + return f'"{escaped}"' + + +def _escape(character: str) -> str: + if character in "\n\r\t": + return {"\n": "\\n", "\r": "\\r", "\t": "\\t"}[character] + return "".join(f"\\{byte:03o}" for byte in character.encode()) + + +def _comment_list(entries: Iterable[str]) -> str: + return "\n".join(f" * {entry}" for entry in entries) + + +def _block_comment(sentences: Sequence[str]) -> str: + lines = textwrap.wrap(" ".join(sentences), width=84) + body = "\n".join(f" * {line}" for line in lines[1:]) + return f"/* {lines[0]}\n{body}\n */" if body else f"/* {lines[0]} */" diff --git a/falcon/config.py b/falcon/config.py index 44907c8..dce3103 100644 --- a/falcon/config.py +++ b/falcon/config.py @@ -1,2 +1,457 @@ -ONNX_OPSET_VERSION = 15 -ML_ONNX_OPSET_VERSION = 2 \ No newline at end of file +from __future__ import annotations + +import math +from collections.abc import Callable, Sequence +from dataclasses import dataclass, field, replace +from numbers import Real +from typing import ( + TYPE_CHECKING, + Any, + ClassVar, + Literal, + Protocol, + TypeAlias, + TypeVar, + runtime_checkable, +) + +from numpy import typing as npt +from sklearn.model_selection import BaseCrossValidator + +from falcon.types import DatasetSchema + +if TYPE_CHECKING: + from falcon.tabular.candidates import EstimatorSpec + +ONNX_OPSET_VERSION = 21 +ML_ONNX_OPSET_VERSION = 4 +# `onnx` stamps exported models with its own newest IR version, which runtimes reject +# outright if they are older. Nothing we emit needs more than the IR that pairs with +# opset 21, and pinning it keeps models loadable by the oldest onnxruntime we support: +# Python 3.10 caps onnxruntime at 1.23 (max IR 11) while installing onnx 1.22 (IR 13). +ONNX_IR_VERSION = 10 +# This is promoted only after the benchmark comparison gate passes. +DATASET_AWARE_ORDERING_DEFAULT = False + +EvalStrategy: TypeAlias = ( + Literal["auto", "holdout", "cv"] | BaseCrossValidator | Callable[..., Any] | None +) +ClassWeight: TypeAlias = Literal["none", "balanced"] +DECISION_METRICS = frozenset({"balanced_accuracy", "f1", "mcc"}) +_Setting = TypeVar("_Setting") + + +class _Unset: + pass + + +_UNSET = _Unset() + + +def _value_or_default(value: _Setting | _Unset, default: _Setting) -> _Setting: + return default if isinstance(value, _Unset) else value + + +def _validate_positive_integer(value: int, name: str, minimum: int = 1) -> None: + if isinstance(value, bool) or not isinstance(value, int) or value < minimum: + raise ValueError(f"{name} must be at least {minimum}") + + +def _is_finite_number(value: object) -> bool: + return ( + not isinstance(value, bool) + and isinstance(value, Real) + and math.isfinite(float(value)) + ) + + +@runtime_checkable +class CandidateSource(Protocol): + def get_candidates( + self, + task: str, + *, + X: npt.NDArray[Any] | None = None, + y: npt.NDArray[Any] | None = None, + groups: npt.ArrayLike | None = None, + n_classes: int | None = None, + n_splits: int = 5, + time_limit: float | None = None, + random_state: int = 42, + schema: DatasetSchema | None = None, + dataset_aware_ordering: bool = False, + config: RunConfig | None = None, + ) -> tuple[EstimatorSpec, ...]: ... + + +@dataclass(frozen=True) +class PortfolioSource: + specs: tuple[EstimatorSpec, ...] | None = None + max_candidates: int | None = None + + def __post_init__(self) -> None: + if self.specs is not None: + specs = tuple(self.specs) + if not specs: + raise ValueError("PortfolioSource specs must not be empty") + object.__setattr__(self, "specs", specs) + if self.max_candidates is not None: + _validate_positive_integer(self.max_candidates, "max_candidates") + + def get_candidates( + self, + task: str, + *, + X: npt.NDArray[Any] | None = None, + y: npt.NDArray[Any] | None = None, + groups: npt.ArrayLike | None = None, + n_classes: int | None = None, + n_splits: int = 5, + time_limit: float | None = None, + random_state: int = 42, + schema: DatasetSchema | None = None, + dataset_aware_ordering: bool = False, + config: RunConfig | None = None, + ) -> tuple[EstimatorSpec, ...]: + del X, groups, n_splits, time_limit, config + if self.specs is None: + from falcon.tabular.candidates import default_portfolio + + specs = default_portfolio(task, n_classes=n_classes) + else: + specs = self.specs + if dataset_aware_ordering: + if y is None or schema is None: + raise ValueError( + "Dataset-aware ordering requires targets and a dataset schema" + ) + from falcon.tabular.portfolio_ordering import ( + extract_dataset_meta_features, + reorder_portfolio, + ) + + meta_features = extract_dataset_meta_features(y, schema) + specs = reorder_portfolio( + specs, + meta_features, + task, + random_state=random_state, + ) + if self.max_candidates is None: + return specs + return specs[: self.max_candidates] + + +@dataclass(frozen=True) +class HPOSource: + family: str + n_trials: int = 20 + top_n: int = 1 + time_budget_fraction: float = 0.25 + + def __post_init__(self) -> None: + if not isinstance(self.family, str) or not self.family: + raise ValueError("family must be a non-empty string") + _validate_positive_integer(self.n_trials, "n_trials") + _validate_positive_integer(self.top_n, "top_n") + if self.top_n > self.n_trials: + raise ValueError("top_n must not exceed n_trials") + if ( + not _is_finite_number(self.time_budget_fraction) + or not 0 < self.time_budget_fraction < 1 + ): + raise ValueError("time_budget_fraction must be between zero and one") + + def get_candidates( + self, + task: str, + *, + X: npt.NDArray[Any] | None = None, + y: npt.NDArray[Any] | None = None, + groups: npt.ArrayLike | None = None, + n_classes: int | None = None, + n_splits: int = 5, + time_limit: float | None = None, + random_state: int = 42, + schema: DatasetSchema | None = None, + dataset_aware_ordering: bool = False, + config: RunConfig | None = None, + ) -> tuple[EstimatorSpec, ...]: + del n_classes, schema, dataset_aware_ordering + if X is None or y is None: + raise ValueError("HPO candidate generation requires training data") + from falcon.tabular.hpo import generate_hpo_candidates + + resolved = RunConfig() if config is None else config + return generate_hpo_candidates( + task, + X, + y, + groups=groups, + family=self.family, + n_trials=self.n_trials, + top_n=self.top_n, + n_splits=n_splits, + time_limit=time_limit, + time_budget_fraction=self.time_budget_fraction, + random_state=random_state, + class_weight=resolved.class_weight, + prior_correct=resolved.prior_correct, + ) + + +@dataclass(frozen=True, init=False) +class RunConfig: + candidate_sources: tuple[CandidateSource, ...] = field( + default_factory=lambda: (PortfolioSource(),) + ) + ensemble_enabled: bool = True + ensemble_max_iterations: int = 100 + plateau_enabled: bool = True + plateau_patience: int = 3 + plateau_tolerance: float = 1e-4 + oof_folds: int = 5 + eval_strategy: EvalStrategy = "auto" + time_limit: float | None = None + random_state: int = 42 + dataset_aware_ordering: bool = DATASET_AWARE_ORDERING_DEFAULT + calibrate: bool = False + conformal_alpha: float | None = None + impute_missing: bool = True + class_weight: ClassWeight = "none" + decision_metric: str | None = "balanced_accuracy" + _provided_fields: ClassVar[frozenset[str]] = frozenset() + + def __init__( + self, + candidate_sources: Sequence[CandidateSource] | _Unset = _UNSET, + ensemble_enabled: bool | _Unset = _UNSET, + ensemble_max_iterations: int | _Unset = _UNSET, + plateau_enabled: bool | _Unset = _UNSET, + plateau_patience: int | _Unset = _UNSET, + plateau_tolerance: float | _Unset = _UNSET, + oof_folds: int | _Unset = _UNSET, + eval_strategy: EvalStrategy | _Unset = _UNSET, + time_limit: float | None | _Unset = _UNSET, + random_state: int | _Unset = _UNSET, + dataset_aware_ordering: bool | _Unset = _UNSET, + calibrate: bool | _Unset = _UNSET, + conformal_alpha: float | None | _Unset = _UNSET, + impute_missing: bool | _Unset = _UNSET, + class_weight: ClassWeight | _Unset = _UNSET, + decision_metric: str | None | _Unset = _UNSET, + ) -> None: + settings = { + "candidate_sources": candidate_sources, + "ensemble_enabled": ensemble_enabled, + "ensemble_max_iterations": ensemble_max_iterations, + "plateau_enabled": plateau_enabled, + "plateau_patience": plateau_patience, + "plateau_tolerance": plateau_tolerance, + "oof_folds": oof_folds, + "eval_strategy": eval_strategy, + "time_limit": time_limit, + "random_state": random_state, + "dataset_aware_ordering": dataset_aware_ordering, + "calibrate": calibrate, + "conformal_alpha": conformal_alpha, + "impute_missing": impute_missing, + "class_weight": class_weight, + "decision_metric": decision_metric, + } + object.__setattr__( + self, + "candidate_sources", + _value_or_default(candidate_sources, (PortfolioSource(),)), + ) + object.__setattr__( + self, + "ensemble_enabled", + _value_or_default(ensemble_enabled, True), + ) + object.__setattr__( + self, + "ensemble_max_iterations", + _value_or_default(ensemble_max_iterations, 100), + ) + object.__setattr__( + self, + "plateau_enabled", + _value_or_default(plateau_enabled, True), + ) + object.__setattr__( + self, + "plateau_patience", + _value_or_default(plateau_patience, 3), + ) + object.__setattr__( + self, + "plateau_tolerance", + _value_or_default(plateau_tolerance, 1e-4), + ) + object.__setattr__(self, "oof_folds", _value_or_default(oof_folds, 5)) + object.__setattr__( + self, + "eval_strategy", + _value_or_default(eval_strategy, "auto"), + ) + object.__setattr__( + self, + "time_limit", + _value_or_default(time_limit, None), + ) + object.__setattr__( + self, + "random_state", + _value_or_default(random_state, 42), + ) + object.__setattr__( + self, + "dataset_aware_ordering", + _value_or_default( + dataset_aware_ordering, + DATASET_AWARE_ORDERING_DEFAULT, + ), + ) + object.__setattr__( + self, + "calibrate", + _value_or_default(calibrate, False), + ) + object.__setattr__( + self, + "conformal_alpha", + _value_or_default(conformal_alpha, None), + ) + object.__setattr__( + self, + "impute_missing", + _value_or_default(impute_missing, True), + ) + object.__setattr__( + self, + "class_weight", + _value_or_default(class_weight, "none"), + ) + object.__setattr__( + self, + "decision_metric", + _value_or_default(decision_metric, "balanced_accuracy"), + ) + object.__setattr__( + self, + "_provided_fields", + frozenset( + name + for name, value in settings.items() + if not isinstance(value, _Unset) + ), + ) + self.__post_init__() + + def __post_init__(self) -> None: + candidate_sources = tuple(self.candidate_sources) + if not candidate_sources: + raise ValueError("candidate_sources must not be empty") + if not all(isinstance(source, CandidateSource) for source in candidate_sources): + raise ValueError( + "candidate_sources must contain only CandidateSource instances" + ) + object.__setattr__(self, "candidate_sources", candidate_sources) + + for name, value in ( + ("ensemble_enabled", self.ensemble_enabled), + ("plateau_enabled", self.plateau_enabled), + ("dataset_aware_ordering", self.dataset_aware_ordering), + ("calibrate", self.calibrate), + ("impute_missing", self.impute_missing), + ): + if not isinstance(value, bool): + raise ValueError(f"{name} must be a boolean") + + _validate_positive_integer( + self.ensemble_max_iterations, + "ensemble_max_iterations", + ) + _validate_positive_integer(self.plateau_patience, "plateau_patience") + if not _is_finite_number(self.plateau_tolerance) or self.plateau_tolerance < 0: + raise ValueError("plateau_tolerance must be a finite non-negative value") + _validate_positive_integer(self.oof_folds, "oof_folds", minimum=2) + + if self.eval_strategy is None: + valid_strategy = True + elif isinstance(self.eval_strategy, str): + valid_strategy = self.eval_strategy in {"auto", "holdout", "cv"} + else: + valid_strategy = isinstance( + self.eval_strategy, BaseCrossValidator + ) or callable(self.eval_strategy) + if not valid_strategy: + raise ValueError( + "eval_strategy must be 'auto', 'holdout', 'cv', None, a callable, " + "or a BaseCrossValidator" + ) + if self.time_limit is not None and ( + not _is_finite_number(self.time_limit) or self.time_limit <= 0 + ): + raise ValueError("time_limit must be a finite value greater than zero") + if ( + isinstance(self.random_state, bool) + or not isinstance(self.random_state, int) + or self.random_state < 0 + ): + raise ValueError("random_state must be a non-negative integer") + if self.conformal_alpha is not None and ( + not _is_finite_number(self.conformal_alpha) + or not 0 < self.conformal_alpha < 1 + ): + raise ValueError("conformal_alpha must be between zero and one") + if self.class_weight not in {"none", "balanced"}: + raise ValueError("class_weight must be either 'none' or 'balanced'") + if ( + self.decision_metric is not None + and self.decision_metric not in DECISION_METRICS + ): + raise ValueError( + "decision_metric must be None or one of " + f"{', '.join(sorted(DECISION_METRICS))}" + ) + + @property + def prior_correct(self) -> bool: + """Whether OOF selection scores decisions at `argmax p/pi` instead of `argmax p`.""" + return self.class_weight != "balanced" and self.decision_metric is not None + + def replaced(self, **overrides: Any) -> RunConfig: + """Like `dataclasses.replace`, but carrying `_provided_fields` forward. + + `replace` passes every field explicitly, which would otherwise mark an + untouched default as an explicit choice. + """ + updated = replace(self, **overrides) + object.__setattr__( + updated, + "_provided_fields", + self._provided_fields | frozenset(overrides), + ) + return updated + + def merged_with(self, overrides: RunConfig) -> RunConfig: + return self.replaced( + **{name: getattr(overrides, name) for name in overrides._provided_fields}, + ) + + +__all__ = [ + "CandidateSource", + "ClassWeight", + "DATASET_AWARE_ORDERING_DEFAULT", + "DECISION_METRICS", + "EvalStrategy", + "HPOSource", + "ML_ONNX_OPSET_VERSION", + "ONNX_IR_VERSION", + "ONNX_OPSET_VERSION", + "PortfolioSource", + "RunConfig", +] diff --git a/falcon/constants.py b/falcon/constants.py index 12f5419..53411a1 100644 --- a/falcon/constants.py +++ b/falcon/constants.py @@ -1,2 +1,6 @@ -TABULAR_CLASSIFICATION_TASK = 'tabular_classification' -TABULAR_REGRESSION_TASK = 'tabular_regression' \ No newline at end of file +TABULAR_CLASSIFICATION_TASK = "tabular_classification" +TABULAR_REGRESSION_TASK = "tabular_regression" + +# Every tag falcon writes into an FNNX manifest is namespaced `:::`, +# so readers can tell falcon's tags apart from those of any other producer. +DEFAULT_PRODUCER_NAME: str = "falcon.fnnx.ai" diff --git a/falcon/datasets.py b/falcon/datasets.py index ef5a941..50b0e1d 100644 --- a/falcon/datasets.py +++ b/falcon/datasets.py @@ -1,18 +1,22 @@ +from typing import Any + import pandas as pd -from typing import Union -import numpy as np -from falcon.utils import print_ +from numpy import typing as npt + +from falcon.utils import logger -def load_churn_dataset(mode: str = "training") -> Union[pd.DataFrame, np.ndarray]: - print_("Loading churn dataset ...") +def load_churn_dataset( + mode: str = "training", +) -> pd.DataFrame | npt.NDArray[Any]: + logger.info("Loading churn dataset ...") df = pd.read_csv( "https://gist.githubusercontent.com/OKUA1/b5faf7b5b3fa9d69bbb64b52670ecf10/raw/d5f87274ad244f3da4b9e330bf7fc9a8d3015f0b/churn.csv" ) if mode == "training": - print_(df.head(5)) - print_(f"Dataset shape: {df.shape}") - print_("This dataset can be used for `tabular_classification` task") + logger.info("%s", df.head(5)) + logger.info("Dataset shape: %s", df.shape) + logger.info("This dataset can be used for `tabular_classification` task") elif mode == "inference": df.pop("churn") df = df.to_numpy() @@ -21,20 +25,21 @@ def load_churn_dataset(mode: str = "training") -> Union[pd.DataFrame, np.ndarray return df -def load_insurance_dataset(mode: str = "training") -> Union[pd.DataFrame, np.ndarray]: - print_("Loading insurance dataset ...") +def load_insurance_dataset( + mode: str = "training", +) -> pd.DataFrame | npt.NDArray[Any]: + logger.info("Loading insurance dataset ...") df = pd.read_csv( "https://gist.githubusercontent.com/OKUA1/b5faf7b5b3fa9d69bbb64b52670ecf10/raw/d5f87274ad244f3da4b9e330bf7fc9a8d3015f0b/insurance.csv" ) if mode == "training": - print_(df.head(5)) - print_(f"Dataset shape: {df.shape}") - print_("This dataset can be used for `tabular_regression` task") + logger.info("%s", df.head(5)) + logger.info("Dataset shape: %s", df.shape) + logger.info("This dataset can be used for `tabular_regression` task") elif mode == "inference": df.pop("charges") df = df.to_numpy() - print(df) + logger.debug("Loaded inference data: %s", df) else: raise ValueError(f"Unknown mode {mode}, expected `training` or `inference`") return df - diff --git a/falcon/main.py b/falcon/main.py index 9275ca7..aed3ede 100644 --- a/falcon/main.py +++ b/falcon/main.py @@ -1,155 +1,106 @@ -from falcon.abstract import TaskManager -from falcon.tabular import TabularTaskManager -from typing import Any, Optional, Dict, Type, Union, Callable -from sklearn.model_selection import BaseCrossValidator -from falcon.abstract import Pipeline, TaskManager -import warnings -import datetime -from falcon.task_configurations import get_task_configuration, TaskConfigurationRegistry -from falcon.utils import set_eval_strategy - - -def warn(*args: Any, **kwargs: Any) -> None: - pass - - -def initialize( - task: str, - data: Any, - pipeline: Optional[Type[Pipeline]] = None, - pipeline_options: Optional[Dict] = None, - extra_pipeline_options: Optional[Dict] = None, - features: Any = None, - target: Any = None, - **options: Any, -) -> TaskManager: - """ - Initializes and returns a task manager object for a given task. - - Parameters - ---------- - task : str - type of the task - data : Any - data to be used for training - pipeline : Optional[Type[Pipeline]], optional - class to be used as pipeline, by default None - pipeline_options : Optional[Dict], optional - arguments to be passed to the pipeline, by default None. - These options will overwrite the ones from `default_pipeline_options` attribute - extra_pipeline_options : Optional[Dict], optional - arguments to be passed to the pipeline, by default None. - These options will be passed in addition to the ones from `default_pipeline_options` attribute. - This argument is ignored if `pipeline_options` is not None - features : Any, optional - features to be used for training, by default None - target : Any, optional - target to be used for training, by default None - - Returns - ------- - TaskManager - Initialized task manager object - """ - warnings.warn = warn - - Manager = TaskConfigurationRegistry.get_task_manager(task) - - manager = Manager( - task=task, - data=data, - pipeline=pipeline, - pipeline_options=pipeline_options, - extra_pipeline_options=extra_pipeline_options, - features=features, - target=target, - **options, - ) +from __future__ import annotations - return manager +import datetime +from typing import Any, Literal + +from falcon.config import EvalStrategy, RunConfig +from falcon.predictor import UNSPECIFIED, Predictor, _Unspecified +from falcon.tabular.ingestion import TabularData +from falcon.tabular.splitting import GroupBy +from falcon.utils import logger + +_REMOVED_KWARGS = { + "manager_configuration": ( + "`manager_configuration` was removed in 0.9 — use `preset` or " + "`config=RunConfig(...)`" + ), + "pipeline": "`pipeline` was removed in 0.9 — use `preset` or `RunConfig`", + "pipeline_options": ( + "`pipeline_options` was removed in 0.9 — use `preset` or `RunConfig`" + ), + "extra_pipeline_options": ( + "`extra_pipeline_options` was removed in 0.9 — use `preset` or `RunConfig`" + ), +} + + +def _reject_unknown_kwargs(kwargs: dict[str, Any]) -> None: + if not kwargs: + return + name = next(iter(kwargs)) + if name in _REMOVED_KWARGS: + raise TypeError(_REMOVED_KWARGS[name]) + raise TypeError(f"AutoML() got an unexpected keyword argument `{name}`") def AutoML( task: str, - train_data: Any, - test_data: Any = None, - features: Any = None, - target: Any = None, - manager_configuration: Optional[Union[Dict, str]] = None, - config: Optional[Union[Dict, str]] = None, - eval_strategy: Optional[Union[str, Callable, BaseCrossValidator]] = "dynamic", -) -> TaskManager: - """ - High level API for one line model training and evaluation. - - When calling the following steps will be executed: - 1) task manager object will be initialized; - 2) the model will be trained; - 3) performance summary table is printed (if test set is not provided, random split is done); - 4) the model is saved as an onnx file. - - Parameters - ---------- - task : str - type of the task, currently supported tasks are [`tabular_classification`, `tabular_regression`] - train_data : Any - data to be used for training, for tabular classification and regression this can be: path to .csv or .parquet file, pandas dataframe, numpy array, tuple (X,y) - test_data : Any, optional - data to be used for evaluation, for tabular classification and regression this can be: path to .csv or .parquet file, pandas dataframe, numpy array, tuple (X,y) - features : Any, optional - features to be used for training, for tabular classification and regression this can be: list of column names or indexes, by default None - target : Any, optional - target to be used for training, for tabular classification and regression this can be: column name or index, by default None - manager_configuration : Union[Dict, str], optional - task manager configuration to be used (can be used to replace pipeline/learner and/or their arguments), by default None - config : Union[Dict, str], optional - alias for `manager_configuration` argument - eval_strategy : Optional[Union[str, Callable, BaseCrossValidator]], optional - evaluation strategy, by default "dynamic" - - "dynamic" - if test_data is provided, evaluation will be done on test_data, otherwise "auto" will be used - - "auto" - random split / cv will be done - - "cv" - cross validation will be done - - "holdout" - random split will be done - - None - no evaluation will be done - - Callable - custom function for performing train/eval split - - BaseCrossValidator - custom cross validator - - Returns - ------- - TaskManager - Task Manager object for the corresponding task. - """ - task = task.lower() - if config is not None and manager_configuration is not None: - print( - "Both `config` and `manager_configuration` were set; `manager_configuration` will be ignored in this case." - ) - if config is not None: - manager_configuration = config - if manager_configuration is None: - manager_configuration_ = {} - elif isinstance(manager_configuration, dict): - manager_configuration_ = manager_configuration - elif isinstance(manager_configuration, str): - manager_configuration_ = get_task_configuration( - task=task, configuration_name=manager_configuration - ) - - set_eval_strategy(eval_strategy, manager_configuration_, test_data) - - manager = initialize( - task=task, - data=train_data, + train_data: TabularData, + test_data: TabularData | None = None, + features: list[str] | list[int] | None = None, + target: str | int | None = None, + save_model: bool = True, + eval_strategy: EvalStrategy | Literal["dynamic"] = "dynamic", + config: RunConfig | str | None = None, + preset: str = "balanced", + time_limit: float | None | _Unspecified = UNSPECIFIED, + random_state: int | _Unspecified = UNSPECIFIED, + group_by: GroupBy | None = None, + **kwargs: Any, +) -> Predictor: + _reject_unknown_kwargs(kwargs) + if not isinstance(save_model, bool): + raise TypeError("save_model must be a boolean") + + run_config: RunConfig | None + resolved_preset = preset + if isinstance(config, str): + if preset != "balanced" and preset != config: + raise ValueError( + "Pass a preset name through either `preset` or `config`, not both" + ) + resolved_preset = config + run_config = None + elif config is None or isinstance(config, RunConfig): + run_config = config + else: + if isinstance(config, dict): + raise TypeError( + "Dictionary configs were removed in 0.9 — use `config=RunConfig(...)`" + ) + raise TypeError("config must be a RunConfig or preset name") + + resolved_eval_strategy: EvalStrategy + if eval_strategy == "dynamic": + resolved_eval_strategy = None if test_data is not None else "auto" + else: + resolved_eval_strategy = eval_strategy + predictor_options: dict[str, Any] = { + "task": task, + "preset": resolved_preset, + "config": run_config, + "eval_strategy": resolved_eval_strategy, + } + if not isinstance(time_limit, _Unspecified): + predictor_options["time_limit"] = time_limit + if not isinstance(random_state, _Unspecified): + predictor_options["random_state"] = random_state + + predictor = Predictor(**predictor_options) + predictor.fit( + train_data, features=features, target=target, - **manager_configuration_, + group_by=group_by, ) + predictor._performance_summary(test_data) + if save_model: + timestamp = datetime.datetime.now().strftime("%Y%m%d.%H%M%S") + filename = f"falcon_{predictor.task}_{timestamp}.fnnx" + logger.info("Saving the model ...") + predictor.save(filename) + logger.info("The model was saved as `%s`", filename) + return predictor + - manager.train() - manager.performance_summary(test_data=test_data) - print("Saving the model ...") - ts = datetime.datetime.now().strftime("%Y%m%d.%H%M%S") - filename = f"falcon_{ts}.onnx" - manager.save_model(format="onnx", filename=filename) - print(f"The model was saved as `{filename}`") - return manager +__all__ = ["AutoML"] diff --git a/falcon/predictor.py b/falcon/predictor.py new file mode 100644 index 0000000..397711a --- /dev/null +++ b/falcon/predictor.py @@ -0,0 +1,450 @@ +from __future__ import annotations + +import os +from collections.abc import Callable +from dataclasses import dataclass, replace +from typing import Any + +import numpy as np +import pandas as pd +from numpy import typing as npt +from numpy.random import default_rng +from sklearn import metrics +from sklearn.model_selection import BaseCrossValidator + +from falcon import types as ft +from falcon.config import EvalStrategy, RunConfig +from falcon.constants import TABULAR_CLASSIFICATION_TASK, TABULAR_REGRESSION_TASK +from falcon.presets import resolve_run_config +from falcon.tabular.evaluation import ( + classification_metrics, + regression_metrics, +) +from falcon.tabular.ingestion import ( + TabularData, + ingest_data, + ingest_data_with_row_selection, + read_data, +) +from falcon.tabular.pipelines.simple_tabular_pipeline import SimpleTabularPipeline +from falcon.tabular.splitting import ( + GroupBy, + callable_holdout_indices, + cross_validation_indices, + holdout_indices, + resolve_evaluation_strategy, + resolve_groups, +) +from falcon.tabular.training import CandidateLearner, SplitIndices +from falcon.types import DatasetSchema + + +class _Unspecified: + pass + + +UNSPECIFIED = _Unspecified() + +_REMOVED_KWARGS = { + "manager_configuration": ( + "`manager_configuration` was removed in 0.9 — use `preset` or " + "`config=RunConfig(...)`" + ), + "pipeline": "`pipeline` was removed in 0.9 — use `preset` or `RunConfig`", + "pipeline_options": ( + "`pipeline_options` was removed in 0.9 — use `preset` or `RunConfig`" + ), + "extra_pipeline_options": ( + "`extra_pipeline_options` was removed in 0.9 — use `preset` or `RunConfig`" + ), +} + + +@dataclass(frozen=True) +class _TrainingData: + X: npt.NDArray[np.object_] + y: npt.NDArray[np.object_] + schema: DatasetSchema + groups: npt.NDArray[np.int64] + + +class Predictor: + def __init__( + self, + task: str, + preset: str = "balanced", + config: RunConfig | None = None, + time_limit: float | None | _Unspecified = UNSPECIFIED, + random_state: int | _Unspecified = UNSPECIFIED, + eval_strategy: EvalStrategy | _Unspecified = UNSPECIFIED, + **kwargs: Any, + ) -> None: + if kwargs: + name = next(iter(kwargs)) + if name in _REMOVED_KWARGS: + raise TypeError(_REMOVED_KWARGS[name]) + raise TypeError(f"Predictor() got an unexpected keyword argument `{name}`") + if not isinstance(task, str): + raise TypeError("task must be a string") + if not isinstance(preset, str): + raise TypeError("preset must be a string") + if config is not None and not isinstance(config, RunConfig): + if isinstance(config, dict): + raise TypeError( + "Dictionary configs were removed in 0.9 — use " + "`config=RunConfig(...)`" + ) + raise TypeError("config must be a RunConfig") + normalized_task = task.lower() + if normalized_task not in { + TABULAR_CLASSIFICATION_TASK, + TABULAR_REGRESSION_TASK, + }: + raise ValueError(f"Unknown task `{task}`") + overrides: dict[str, Any] = {"config": config} + if not isinstance(time_limit, _Unspecified): + overrides["time_limit"] = time_limit + if not isinstance(random_state, _Unspecified): + overrides["random_state"] = random_state + if not isinstance(eval_strategy, _Unspecified): + overrides["eval_strategy"] = eval_strategy + + self.task = normalized_task + self.preset = preset + self.config = resolve_run_config(normalized_task, preset, **overrides) + if self.config.calibrate and normalized_task != TABULAR_CLASSIFICATION_TASK: + raise ValueError( + "Probability calibration is only available for classification" + ) + if ( + self.config.conformal_alpha is not None + and normalized_task != TABULAR_REGRESSION_TASK + ): + raise ValueError( + "Conformal prediction intervals are only available for regression" + ) + self._pipeline: SimpleTabularPipeline | None = None + self._learner: CandidateLearner | None = None + self._training_data: _TrainingData | None = None + self._fit_indices: npt.NDArray[np.int64] | None = None + self._eval_indices: npt.NDArray[np.int64] | None = None + self._performance_metrics: dict[str, dict[str, Any]] = {} + self._features: ft.ColumnsList | None = None + self._target: str | int | None = None + self.classes_: npt.NDArray[Any] | None = None + + def _evaluation_split( + self, + data: _TrainingData, + ) -> tuple[ + npt.NDArray[np.int64], + npt.NDArray[np.int64] | None, + tuple[SplitIndices, ...] | None, + ]: + strategy = self.config.eval_strategy + if strategy is None: + return np.arange(len(data.X), dtype=np.int64), None, None + if strategy == "auto": + strategy = resolve_evaluation_strategy(len(data.X)) + if strategy == "holdout": + train_indices, eval_indices = holdout_indices( + data.X, + data.y, + self.task, + data.groups, + random_state=self.config.random_state, + ) + return train_indices, eval_indices, None + if callable(strategy) and not isinstance(strategy, BaseCrossValidator): + train_indices, eval_indices = callable_holdout_indices( + strategy, + data.X, + data.y, + data.groups, + ) + return train_indices, eval_indices, None + if strategy == "cv" or isinstance(strategy, BaseCrossValidator): + cv = strategy if isinstance(strategy, BaseCrossValidator) else None + splits = cross_validation_indices( + data.X, + data.y, + self.task, + data.groups, + cv=cv, + n_splits=self.config.oof_folds, + random_state=self.config.random_state, + ) + return ( + np.arange(len(data.X), dtype=np.int64), + None, + tuple(splits), + ) + raise RuntimeError("The resolved evaluation strategy is invalid") + + def fit( + self, + data: TabularData, + features: ft.ColumnsList | None = None, + target: str | int | None = None, + group_by: GroupBy | None = None, + ) -> Predictor: + X, y, schema, row_indices, source_row_count = ingest_data_with_row_selection( + data, + task=self.task, + features=features, + target=target, + ) + groups = resolve_groups( + X, + schema.column_names, + group_by, + source_row_indices=row_indices, + source_row_count=source_row_count, + ) + training_data = _TrainingData(X, y, schema, groups) + fit_indices, eval_indices, evaluation_splits = self._evaluation_split( + training_data + ) + fit_schema = replace( + schema, + dimensions=(len(fit_indices), schema.n_features), + ) + pipeline = SimpleTabularPipeline( + task=self.task, + dataset_size=fit_schema.dimensions, + schema=fit_schema, + learner=CandidateLearner, + impute_missing=self.config.impute_missing, + learner_kwargs={ + "config": self.config, + "evaluation_splits": evaluation_splits, + }, + ) + pipeline.fit( + X[fit_indices], + y[fit_indices], + fit_schema, + groups=groups[fit_indices], + ) + learner = pipeline.steps[1] + if not isinstance(learner, CandidateLearner): + raise RuntimeError("The candidate learner was not assembled correctly") + + self._pipeline = pipeline + self._learner = learner + self._training_data = training_data + self._fit_indices = fit_indices + self._eval_indices = eval_indices + self._features = features + self._target = target + if pipeline.labels_transformer is not None: + self.classes_ = pipeline.labels_transformer.le.classes_.copy() + else: + self.classes_ = None + self._collect_fit_metrics(evaluation_splits is not None) + return self + + def _require_fitted( + self, + ) -> tuple[SimpleTabularPipeline, CandidateLearner, _TrainingData]: + if ( + self._pipeline is None + or self._learner is None + or self._training_data is None + ): + raise RuntimeError("Predictor.fit must be called before this operation") + return self._pipeline, self._learner, self._training_data + + def _prediction_array(self, data: Any) -> npt.NDArray[np.object_]: + _, _, training_data = self._require_fitted() + if isinstance(data, (str, os.PathLike)): + data = read_data(os.fspath(data)) + if isinstance(data, pd.DataFrame): + missing = [ + name + for name in training_data.schema.column_names + if name not in data.columns + ] + if missing: + raise ValueError(f"Prediction data is missing feature `{missing[0]}`") + values = data.loc[:, list(training_data.schema.column_names)].to_numpy( + dtype=np.object_ + ) + else: + values = np.asarray(data, dtype=np.object_) + if values.ndim != 2: + raise ValueError("Prediction data must be two-dimensional") + if values.shape[1] != training_data.schema.n_features: + raise ValueError( + "Prediction data must contain exactly " + f"{training_data.schema.n_features} features" + ) + return values + + def predict(self, data: Any) -> npt.NDArray[Any]: + pipeline, _, _ = self._require_fitted() + return np.asarray(pipeline.predict(self._prediction_array(data))).reshape(-1) + + def predict_proba(self, data: Any) -> npt.NDArray[np.float32]: + if self.task != TABULAR_CLASSIFICATION_TASK: + raise RuntimeError("predict_proba is only available for classification") + pipeline, learner, _ = self._require_fitted() + encoded = pipeline.steps[0].transform(self._prediction_array(data)) + return learner.predict_proba(encoded) + + def _report( + self, + y: npt.NDArray[Any], + predictions: npt.NDArray[Any], + ) -> dict[str, Any]: + if self.task == TABULAR_CLASSIFICATION_TASK: + return classification_metrics(y, predictions) + return regression_metrics(y, predictions) + + def _collect_fit_metrics(self, has_cv_evaluation: bool) -> None: + pipeline, learner, training_data = self._require_fitted() + if self._fit_indices is None: + raise RuntimeError("Training indices are unavailable") + fit_X = training_data.X[self._fit_indices] + fit_y = training_data.y[self._fit_indices] + performance = {"train": self._report(fit_y, pipeline.predict(fit_X))} + if self._eval_indices is not None: + eval_X = training_data.X[self._eval_indices] + eval_y = training_data.y[self._eval_indices] + performance["eval"] = self._report(eval_y, pipeline.predict(eval_X)) + elif has_cv_evaluation: + oof_result = learner.oof_predictions() + if oof_result is None: + raise RuntimeError("Cross-validation predictions are unavailable") + evaluation_indices, predictions = oof_result + if pipeline.labels_transformer is not None: + predictions = pipeline.labels_transformer.transform(predictions) + performance["eval_cv"] = self._report( + fit_y[evaluation_indices], predictions + ) + self._performance_metrics = performance + + def _evaluation_data( + self, + data: TabularData, + ) -> tuple[npt.NDArray[np.object_], npt.NDArray[np.object_]]: + _, _, training_data = self._require_fitted() + if isinstance(data, (str, os.PathLike)): + data = read_data(os.fspath(data)) + features: ft.ColumnsList | None + target: str | int | None + if isinstance(data, tuple): + features = None + target = None + elif isinstance(data, pd.DataFrame): + features = list(training_data.schema.column_names) + target = training_data.schema.target_name + else: + features = self._features + target = self._target + X, y, _ = ingest_data( + data, + task=self.task, + features=features, + target=target, + ) + return X, y + + def evaluate(self, test_data: TabularData) -> dict[str, Any]: + X, y = self._evaluation_data(test_data) + result = self._report(y, self.predict(X)) + self._performance_metrics["test"] = result + return result + + def leaderboard(self) -> pd.DataFrame: + _, learner, _ = self._require_fitted() + columns = ["candidate", "family", "score", "fit_time", "weight"] + return pd.DataFrame(learner.leaderboard_records(), columns=columns) + + def feature_importance( + self, + n_repeats: int = 10, + ) -> list[dict[str, str | float]]: + if n_repeats < 1: + raise ValueError("n_repeats must be at least 1") + _, _, training_data = self._require_fitted() + if self._fit_indices is None: + raise RuntimeError("Training indices are unavailable") + X = training_data.X[self._fit_indices] + y = training_data.y[self._fit_indices] + scoring: Callable[[npt.ArrayLike, npt.ArrayLike], float] + if self.task == TABULAR_CLASSIFICATION_TASK: + scoring = metrics.balanced_accuracy_score + y = y.astype(np.str_) + else: + scoring = metrics.r2_score + baseline = scoring(y, self.predict(X)) + rng = default_rng(self.config.random_state) + importances = np.zeros((n_repeats, X.shape[1]), dtype=np.float64) + for feature_index in range(X.shape[1]): + for repeat_index in range(n_repeats): + permuted = X.copy() + order = rng.permutation(len(X)) + permuted[:, feature_index] = permuted[order, feature_index] + importances[repeat_index, feature_index] = baseline - scoring( + y, self.predict(permuted) + ) + means = np.mean(importances, axis=0) + standard_deviations = np.std(importances, axis=0) + denominator = float(np.abs(means.sum())) + scaled = means / denominator if denominator else np.zeros_like(means) + result: list[dict[str, str | float]] = [ + { + "feature_name": feature_name, + "importance": float(mean), + "std": float(standard_deviation), + "scaled_importance": float(scaled_importance), + } + for feature_name, mean, standard_deviation, scaled_importance in zip( + training_data.schema.column_names, + means, + standard_deviations, + scaled, + strict=True, + ) + ] + return sorted( + result, + key=lambda item: float(item["importance"]), + reverse=True, + ) + + def save(self, path: str | os.PathLike[str] | None = None) -> bytes: + pipeline, _, training_data = self._require_fitted() + serializer = pipeline.save( + feature_names=list(training_data.schema.column_names), + schema=training_data.schema, + ) + if self._performance_metrics: + serializer.metadata_payload["metrics"] = { + "performance": self._performance_metrics + } + serialized = serializer.serialize() + if path is not None: + with open(path, "wb") as model_file: + model_file.write(serialized) + return serialized + + def _performance_summary( + self, + test_data: TabularData | None = None, + ) -> dict[str, dict[str, Any]]: + self._require_fitted() + if test_data is not None: + self.evaluate(test_data) + print( + "\n", + pd.DataFrame.from_dict(self._performance_metrics, orient="index"), + "\n", + ) + return { + name: values.copy() for name, values in self._performance_metrics.items() + } + + +__all__ = ["Predictor"] diff --git a/falcon/presets.py b/falcon/presets.py new file mode 100644 index 0000000..3c0200d --- /dev/null +++ b/falcon/presets.py @@ -0,0 +1,179 @@ +from __future__ import annotations + +import os +from collections.abc import Mapping +from typing import Any + +from falcon.config import EvalStrategy, PortfolioSource, RunConfig +from falcon.constants import TABULAR_CLASSIFICATION_TASK, TABULAR_REGRESSION_TASK +from falcon.utils import logger + +_EXTENSION_PREFIX = "falcon_ml_" +_LEGACY_PRESET_PREFIXES = ("SuperLearner", "OptunaLearner", "PlainLearner") + + +def _prevent_extension_load() -> bool: + return bool(os.getenv("FALCON_PREVENT_EXTENSION_AUTO_LOAD", False)) + + +class PresetRegistry: + _PRESETS: dict[str, dict[str, RunConfig]] = { + TABULAR_CLASSIFICATION_TASK: {}, + TABULAR_REGRESSION_TASK: {}, + } + + @classmethod + def register_presets( + cls, + task: str, + presets: Mapping[str, RunConfig], + silent: bool = False, + ) -> None: + if task not in cls._PRESETS: + raise ValueError(f"Unknown task `{task}`") + if not presets: + raise ValueError("At least one preset must be provided") + if any(not name for name in presets): + raise ValueError("Preset names must not be empty") + invalid_configs = [ + name + for name, config in presets.items() + if not isinstance(config, RunConfig) + ] + if invalid_configs: + raise TypeError( + f"Preset `{invalid_configs[0]}` must be registered with a RunConfig" + ) + cls._PRESETS[task].update( + {name: config.replaced() for name, config in presets.items()} + ) + if not silent: + logger.info("Registered presets %s for task %s", list(presets), task) + + @classmethod + def get_preset( + cls, + task: str, + preset_name: str, + allow_extensions_discovery: bool = True, + ) -> RunConfig: + if task not in cls._PRESETS: + raise ValueError(f"Unknown task `{task}`") + if preset_name in cls._PRESETS[task]: + return cls._PRESETS[task][preset_name] + should_load = ( + allow_extensions_discovery + and not _prevent_extension_load() + and "::" in preset_name + ) + if should_load: + extension_name = preset_name.split("::", maxsplit=1)[0] + logger.info( + "Extension `%s` does not seem to be loaded. Will try to load " + "automatically.", + _EXTENSION_PREFIX + extension_name.lower(), + ) + cls.load_extension(extension_name) + return cls.get_preset(task, preset_name, False) + + available = ", ".join(cls.get_registered_preset_names(task)) + if preset_name.startswith(_LEGACY_PRESET_PREFIXES): + raise ValueError( + f"Preset `{preset_name}` was removed in 0.9. Available presets: " + f"{available}. Use `preset` or `config=RunConfig(...)`." + ) + raise ValueError( + f"Preset `{preset_name}` does not exist. Available presets: {available}." + ) + + @classmethod + def get_registered_preset_names(cls, task: str) -> tuple[str, ...]: + if task not in cls._PRESETS: + raise ValueError(f"Unknown task `{task}`") + return tuple(cls._PRESETS[task]) + + @classmethod + def load_extension(cls, extension_name: str) -> None: + normalized_name = extension_name.lower() + module_name = _EXTENSION_PREFIX + normalized_name + logger.info("Attempting to load %s...", module_name) + try: + __import__(module_name).self_register() + except ModuleNotFoundError: + logger.warning( + "Seems like the extension `%s` is not installed. Try installing it " + "first using `pip install %s`.", + normalized_name, + module_name, + ) + + +class _Unset: + pass + + +_UNSET = _Unset() + + +def resolve_run_config( + task: str, + preset: str = "balanced", + *, + config: RunConfig | None = None, + time_limit: float | None | _Unset = _UNSET, + random_state: int | _Unset = _UNSET, + eval_strategy: EvalStrategy | _Unset = _UNSET, +) -> RunConfig: + preset_config = PresetRegistry.get_preset(task, preset) + if config is not None and not isinstance(config, RunConfig): + raise TypeError("config must be a RunConfig") + resolved = preset_config if config is None else preset_config.merged_with(config) + overrides: dict[str, Any] = {} + if not isinstance(time_limit, _Unset): + overrides["time_limit"] = time_limit + if not isinstance(random_state, _Unset): + overrides["random_state"] = random_state + if not isinstance(eval_strategy, _Unset): + overrides["eval_strategy"] = eval_strategy + return resolved.replaced(**overrides) + + +def _builtin_presets() -> dict[str, RunConfig]: + return { + "fast": RunConfig( + candidate_sources=(PortfolioSource(max_candidates=1),), + ensemble_enabled=False, + ensemble_max_iterations=1, + plateau_enabled=False, + oof_folds=2, + ), + "balanced": RunConfig( + candidate_sources=(PortfolioSource(max_candidates=4),), + ensemble_max_iterations=50, + plateau_enabled=True, + plateau_patience=2, + oof_folds=5, + ), + "best": RunConfig( + candidate_sources=(PortfolioSource(),), + ensemble_max_iterations=100, + plateau_enabled=False, + oof_folds=10, + ), + } + + +PresetRegistry.register_presets( + TABULAR_CLASSIFICATION_TASK, + _builtin_presets(), + silent=True, +) +PresetRegistry.register_presets( + TABULAR_REGRESSION_TASK, + _builtin_presets(), + silent=True, +) + +get_run_config = PresetRegistry.get_preset + +__all__ = ["PresetRegistry", "get_run_config", "resolve_run_config"] diff --git a/falcon/runtime.py b/falcon/runtime.py index 0391475..0bbd3c9 100644 --- a/falcon/runtime.py +++ b/falcon/runtime.py @@ -1,44 +1,88 @@ -from typing import Any, Union, Dict, List, Type, Optional +from typing import Any -try: - from phonnx.runtime import Runtime as _PhonnxRuntime -except (ImportError, ModuleNotFoundError): - print("ONNXRuntime/PHONNX is not installed. Inference modules will not work.") - _PhonnxRuntime = None import numpy as np +import pandas as pd +from numpy import typing as npt + +try: + from fnnx.handlers.local import LocalHandler as _LocalHandler + from fnnx.runtime import Runtime as _Runtime +except ImportError: + _Runtime = None +from falcon.constants import ( + DEFAULT_PRODUCER_NAME, + TABULAR_CLASSIFICATION_TASK, + TABULAR_REGRESSION_TASK, +) -class ONNXRuntime: - """ - Runtime for ONNX models based on PHONNX. - """ - - def __init__(self, model: Union[bytes, str]): - if _PhonnxRuntime is None: - raise ImportError("PHONNX is not installed.") - self.runtime = _PhonnxRuntime(model=model) - - def run( - self, X: np.ndarray, outputs: str = "final", **kwargs: Any - ) -> List[np.ndarray]: - """ - Runs the model. - - Parameters - ---------- - X : np.ndarray - model - outputs : str, optional - when set to "all", all onnx output nodes will be returned; when "final" only the last layer outputs are returned, by default "final" - - Returns - ------- - List[np.ndarray] - model predictions - """ - if outputs not in ["all", "final"]: - raise ValueError( - f"Expected `outputs` to be one of [all, final], got `{outputs}`." - ) +class Runtime: + def __init__(self, model_path: str) -> None: + if _Runtime is None: + raise ImportError("FNNX is not installed.") + self.runtime = _Runtime(model_path) + handler: _LocalHandler = self.runtime.handler + + self._input_names = list(handler.input_specs.keys()) + self._output_names = list(handler.output_specs.keys()) + + producer_tags = handler.manifest.get("producer_tags", []) + + self.task = self._detect_task(producer_tags) + + if self.task is None: + raise RuntimeError("Could not detect task from model tags.") - return self.runtime.run(X, outputs_to_return=outputs) \ No newline at end of file + def _detect_task(self, producer_tags: list[str]) -> str | None: + for tag in producer_tags: + if tag.startswith( + f"{DEFAULT_PRODUCER_NAME}::{TABULAR_CLASSIFICATION_TASK}" + ): + return TABULAR_CLASSIFICATION_TASK + elif tag.startswith(f"{DEFAULT_PRODUCER_NAME}::{TABULAR_REGRESSION_TASK}"): + return TABULAR_REGRESSION_TASK + return None + + def _predict( + self, + X: npt.NDArray[Any] | pd.DataFrame | dict[str, npt.NDArray[Any]], + ) -> dict[str, npt.NDArray[Any]]: + if isinstance(X, pd.DataFrame): + inputs = X.to_dict(orient="list") + elif isinstance(X, np.ndarray): + inputs = {name: X[:, i] for i, name in enumerate(self._input_names)} + else: + inputs = X + + reshaped_inputs = { + name: np.asarray(values).reshape(-1, 1) for name, values in inputs.items() + } + + return self.runtime.compute(reshaped_inputs, {}) + + def predict( + self, + X: npt.NDArray[Any] | pd.DataFrame | dict[str, npt.NDArray[Any]], + ) -> npt.NDArray[Any]: + return self._predict(X)["y_pred"] + + def predict_proba( + self, + X: npt.NDArray[Any] | pd.DataFrame | dict[str, npt.NDArray[Any]], + ) -> npt.NDArray[Any]: + if self.task != TABULAR_CLASSIFICATION_TASK: + raise RuntimeError(f"Called predict_proba on a model for {self.task} task.") + return self._predict(X)["probabilities"] + + def predict_interval( + self, + X: npt.NDArray[Any] | pd.DataFrame | dict[str, npt.NDArray[Any]], + ) -> tuple[npt.NDArray[Any], npt.NDArray[Any]]: + if self.task != TABULAR_REGRESSION_TASK: + raise RuntimeError( + f"Called predict_interval on a model for {self.task} task." + ) + if not {"y_lower", "y_upper"}.issubset(self._output_names): + raise RuntimeError("This model does not expose prediction intervals.") + outputs = self._predict(X) + return outputs["y_lower"], outputs["y_upper"] diff --git a/falcon/serialization.py b/falcon/serialization.py index 7e11751..b18d998 100644 --- a/falcon/serialization.py +++ b/falcon/serialization.py @@ -1,16 +1,45 @@ -from typing import List, Optional, Dict -from falcon.utils import print_ -from typing import List, Tuple, Optional -from numpy import typing as npt -from onnx import ModelProto, load_from_string +import io +import json +import tarfile +from collections.abc import Iterator, Sequence +from copy import copy, deepcopy +from dataclasses import dataclass +from typing import Any + +import onnx +from onnx import ModelProto +from onnx import helper as h from onnx.compose import add_prefix, merge_models from onnx.helper import make_model -from falcon.config import ONNX_OPSET_VERSION, ML_ONNX_OPSET_VERSION + from falcon import __version__ as falcon_version -import numpy as np -from typing import Any, Dict, Union -import onnx -from onnx import TensorProto, helper as h, OperatorSetIdProto +from falcon.config import ( + ML_ONNX_OPSET_VERSION, + ONNX_IR_VERSION, + ONNX_OPSET_VERSION, +) +from falcon.constants import DEFAULT_PRODUCER_NAME +from falcon.types import ColumnTypes, DatasetSchema +from falcon.utils import logger + +onnx_type_map: dict[int, str] = { + onnx.TensorProto.FLOAT: "float32", + onnx.TensorProto.UINT8: "uint8", + onnx.TensorProto.INT8: "int8", + onnx.TensorProto.UINT16: "uint16", + onnx.TensorProto.INT16: "int16", + onnx.TensorProto.INT32: "int32", + onnx.TensorProto.INT64: "int64", + onnx.TensorProto.STRING: "string", + onnx.TensorProto.BOOL: "bool", + onnx.TensorProto.FLOAT16: "float16", + onnx.TensorProto.DOUBLE: "float64", + onnx.TensorProto.UINT32: "uint32", + onnx.TensorProto.UINT64: "uint64", + onnx.TensorProto.COMPLEX64: "complex64", + onnx.TensorProto.COMPLEX128: "complex128", + onnx.TensorProto.BFLOAT16: "bfloat16", +} class SerializedModelRepr: @@ -19,10 +48,10 @@ def __init__( model: onnx.ModelProto, n_inputs: int, n_outputs: int, - initial_types: List[str], - initial_shapes: List[List[Optional[int]]], + initial_types: list[str], + initial_shapes: list[list[int | None]], type_: str = "onnx", - ): + ) -> None: self._model = model self._n_inputs = n_inputs self._n_outputs = n_outputs @@ -39,16 +68,16 @@ def get_n_inputs(self) -> int: def get_n_outputs(self) -> int: return self._n_outputs - def get_initial_types(self) -> List[str]: + def get_initial_types(self) -> list[str]: return self._initial_types - def get_initial_shapes(self) -> List[List[Optional[int]]]: + def get_initial_shapes(self) -> list[list[int | None]]: return self._initial_shapes def get_type(self) -> str: return self._type - def to_dict(self) -> Dict: + def to_dict(self) -> dict[str, Any]: return { "n_inputs": self._n_inputs, "n_outputs": self._n_outputs, @@ -59,92 +88,714 @@ def to_dict(self) -> Dict: } -def _make_self_name(name: Any) -> str: - name = str(name) - name = "".join([c for c in name if c.isalpha() or c.isdigit() or c == " "]).rstrip() - name = name.replace(" ", "_") - return name +def _sanitize_feature_name(name: Any, index: int) -> str: + sanitized = "".join( + character if character.isalnum() else "_" for character in str(name) + ) + sanitized = "_".join(part for part in sanitized.split("_") if part) + return sanitized or f"feature_{index}" + + +def _sanitized_feature_names(feature_names: list[Any], count: int) -> list[str]: + if len(feature_names) != count: + feature_names = [f"feature_{index}" for index in range(count)] + + sanitized_names: list[str] = [] + used_names: set[str] = set() + for index, feature_name in enumerate(feature_names): + base_name = _sanitize_feature_name(feature_name, index) + unique_name = base_name + if unique_name in used_names: + unique_name = f"{base_name}_{index}" + suffix = 1 + while unique_name in used_names: + unique_name = f"{base_name}_{index}_{suffix}" + suffix += 1 + sanitized_names.append(unique_name) + used_names.add(unique_name) + return sanitized_names def _rename_inputs( - model: onnx.ModelProto, feature_names: List[Any], feature_types: List + model: onnx.ModelProto, + feature_names: list[Any], ) -> None: - # print(feature_types, len(model.graph.input)) - if len(feature_types) != len(model.graph.input): - feature_types = ["9999" for _ in range(len(model.graph.input))] - else: - feature_types = [str(i.value) for i in feature_types] - if len(feature_names) != len(feature_types): - feature_names = [str(i) for i in range(len(feature_types))] - mapping = {} + sanitized_names = _sanitized_feature_names(feature_names, len(model.graph.input)) + mapping: dict[str, str] = {} for i, inp in enumerate(model.graph.input): - new_name = f"falcon-input-{str(i)}_{_make_self_name(feature_names[i])}_{feature_types[i]}" + new_name = sanitized_names[i] mapping[inp.name] = new_name inp.name = new_name for node in model.graph.node: - for key in mapping.keys(): - for ii, input in enumerate(node.input): - if input == key: - node.input[ii] = mapping[key] + for input_index, input_name in enumerate(node.input): + if input_name in mapping: + node.input[input_index] = mapping[input_name] + + +def _iter_graph_nodes(graph: onnx.GraphProto) -> Iterator[onnx.NodeProto]: + for node in graph.node: + yield node + for attribute in node.attribute: + if attribute.HasField("g"): + yield from _iter_graph_nodes(attribute.g) + for nested_graph in attribute.graphs: + yield from _iter_graph_nodes(nested_graph) + + +def _normalized_onnx_domain(domain: str) -> str: + return "" if domain in {"", "ai.onnx"} else domain + + +def _opset_imports_for_models( + models: Sequence[onnx.ModelProto], +) -> list[onnx.OperatorSetIdProto]: + used_domains = { + _normalized_onnx_domain(node.domain) + for model in models + for node in _iter_graph_nodes(model.graph) + } + declared_versions: dict[str, list[int]] = {} + for model in models: + for opset in model.opset_import: + domain = _normalized_onnx_domain(opset.domain) + declared_versions.setdefault(domain, []).append(opset.version) + + imports: list[onnx.OperatorSetIdProto] = [] + for domain in sorted( + used_domains, + key=lambda item: (item != "", item != "ai.onnx.ml", item), + ): + if domain == "": + version = ONNX_OPSET_VERSION + elif domain == "ai.onnx.ml": + version = ML_ONNX_OPSET_VERSION + elif domain in declared_versions: + version = max(declared_versions[domain]) + else: + raise ValueError(f"No opset version is declared for domain {domain!r}") + imports.append(h.make_operatorsetid(domain, version)) + return imports + + +def _normalize_regression_output(model: onnx.ModelProto) -> None: + if not model.graph.output: + raise RuntimeError("A regression graph must expose an output") + + output_count = len(model.graph.output) + for index, source_output in enumerate(model.graph.output): + if not source_output.type.HasField("tensor_type"): + raise RuntimeError("A regression graph must expose tensor outputs") + + source_name = source_output.name + shape_name = f"{source_name}_falcon_shape" + normalized_name = f"{source_name}_falcon_normalized" + node_suffix = "" if output_count == 1 else f"_{index}" + model.graph.initializer.append( + h.make_tensor(shape_name, onnx.TensorProto.INT64, [1], [-1]) + ) + model.graph.node.append( + h.make_node( + "Reshape", + [source_name, shape_name], + [normalized_name], + name=f"falcon_normalize_regression_output{node_suffix}", + ) + ) + normalized_output = h.make_tensor_value_info( + normalized_name, + source_output.type.tensor_type.elem_type, + [None], + ) + source_output.CopyFrom(normalized_output) + + +def _normalized_ensemble_weights(weights: Sequence[float]) -> list[float]: + if not weights or any(weight < 0 for weight in weights): + raise ValueError("Ensemble weights must be non-negative and not empty") + total = sum(weights) + if total <= 0: + raise ValueError("At least one ensemble weight must be positive") + return [weight / total for weight in weights] + + +def _replace_graph_input( + graph: onnx.GraphProto, + source_name: str, + target_name: str, +) -> None: + for node in _iter_graph_nodes(graph): + for index, input_name in enumerate(node.input): + if input_name == source_name: + node.input[index] = target_name + + +def _append_average( + nodes: list[onnx.NodeProto], + initializers: list[onnx.TensorProto], + inputs: Sequence[str], + output: str, + name: str, +) -> None: + if len(inputs) == 1: + nodes.append(h.make_node("Identity", list(inputs), [output], name=name)) + return + + summed = f"{output}_sum" + divisor = f"{output}_divisor" + nodes.append(h.make_node("Sum", list(inputs), [summed], name=f"{name}_sum")) + initializers.append( + h.make_tensor(divisor, onnx.TensorProto.FLOAT, [], [float(len(inputs))]) + ) + nodes.append(h.make_node("Div", [summed, divisor], [output], name=f"{name}_divide")) + + +def serialize_parallel_ensemble( + fold_models: Sequence[Sequence[SerializedModelRepr]], + weights: Sequence[float], + task: str, + *, + classes: Sequence[int] | None = None, +) -> SerializedModelRepr: + if task not in {"tabular_classification", "tabular_regression"}: + raise ValueError(f"Unknown task `{task}`") + if len(fold_models) != len(weights): + raise ValueError("Each ensemble member must have one weight") + if not fold_models or any(not models for models in fold_models): + raise ValueError("Each ensemble member must contain at least one fold model") + normalized_weights = _normalized_ensemble_weights(weights) + serialized_models = [model for models in fold_models for model in models] + model_protos = [model.get_model() for model in serialized_models] + if any(len(model.graph.input) != 1 for model in model_protos): + raise ValueError("Ensemble fold models must expose exactly one input") + + shared_input = deepcopy(model_protos[0].graph.input[0]) + shared_input.name = "ensemble_input" + expected_input_type = shared_input.type.SerializeToString() + nodes: list[onnx.NodeProto] = [] + initializers: list[onnx.TensorProto] = [] + sparse_initializers: list[onnx.SparseTensorProto] = [] + value_info: list[onnx.ValueInfoProto] = [] + functions: list[onnx.FunctionProto] = [] + member_outputs: list[str] = [] + model_index = 0 + + for member_index, models in enumerate(fold_models): + fold_outputs: list[str] = [] + for fold_index, _ in enumerate(models): + model = model_protos[model_index] + model_index += 1 + if model.graph.input[0].type.SerializeToString() != expected_input_type: + raise ValueError("Ensemble fold model input types must match") + if task == "tabular_classification" and len(model.graph.output) < 2: + raise ValueError( + "Classification fold models must expose labels and probabilities" + ) + if task == "tabular_regression" and len(model.graph.output) != 1: + raise ValueError( + "Regression fold models must expose exactly one prediction output" + ) + + prefix = f"falcon-ensemble/member-{member_index}/fold-{fold_index}/" + prefixed = add_prefix(model, prefix=prefix) + branch_input = prefixed.graph.input[0].name + _replace_graph_input(prefixed.graph, branch_input, shared_input.name) + nodes.extend(deepcopy(prefixed.graph.node)) + initializers.extend(deepcopy(prefixed.graph.initializer)) + sparse_initializers.extend(deepcopy(prefixed.graph.sparse_initializer)) + value_info.extend(deepcopy(prefixed.graph.value_info)) + functions.extend(deepcopy(prefixed.functions)) + + source_output = ( + prefixed.graph.output[-1].name + if task == "tabular_classification" + else prefixed.graph.output[0].name + ) + cast_output = f"{prefix}falcon_float_output" + nodes.append( + h.make_node( + "Cast", + [source_output], + [cast_output], + to=onnx.TensorProto.FLOAT, + name=f"{prefix}falcon_cast_output", + ) + ) + if task == "tabular_regression": + shape_name = f"{prefix}falcon_output_shape" + normalized_output = f"{prefix}falcon_normalized_output" + initializers.append( + h.make_tensor(shape_name, onnx.TensorProto.INT64, [1], [-1]) + ) + nodes.append( + h.make_node( + "Reshape", + [cast_output, shape_name], + [normalized_output], + name=f"{prefix}falcon_reshape_output", + ) + ) + fold_outputs.append(normalized_output) + else: + fold_outputs.append(cast_output) + + member_output = f"falcon-ensemble/member-{member_index}/fold_average" + _append_average( + nodes, + initializers, + fold_outputs, + member_output, + f"falcon-ensemble/member-{member_index}/average_folds", + ) + weighted_output = f"falcon-ensemble/member-{member_index}/weighted" + weight_name = f"falcon-ensemble/member-{member_index}/weight" + initializers.append( + h.make_tensor( + weight_name, + onnx.TensorProto.FLOAT, + [], + [normalized_weights[member_index]], + ) + ) + nodes.append( + h.make_node( + "Mul", + [member_output, weight_name], + [weighted_output], + name=f"falcon-ensemble/member-{member_index}/apply_weight", + ) + ) + member_outputs.append(weighted_output) + + ensemble_output = ( + "ensemble_probabilities" + if task == "tabular_classification" + else "ensemble_prediction" + ) + if len(member_outputs) == 1: + nodes.append( + h.make_node( + "Identity", + member_outputs, + [ensemble_output], + name="falcon-ensemble/weighted_mean", + ) + ) + else: + nodes.append( + h.make_node( + "Sum", + member_outputs, + [ensemble_output], + name="falcon-ensemble/weighted_mean", + ) + ) + + outputs: list[onnx.ValueInfoProto] + if task == "tabular_classification": + if classes is None or not classes: + raise ValueError("Classification ensembles require encoded class labels") + class_values = [int(label) for label in classes] + class_indices = "ensemble_class_indices" + class_labels = "ensemble_labels" + class_initializer = "ensemble_classes" + initializers.append( + h.make_tensor( + class_initializer, + onnx.TensorProto.INT64, + [len(class_values)], + class_values, + ) + ) + nodes.extend( + [ + h.make_node( + "ArgMax", + [ensemble_output], + [class_indices], + axis=1, + keepdims=0, + name="falcon-ensemble/predict_class_index", + ), + h.make_node( + "Gather", + [class_initializer, class_indices], + [class_labels], + axis=0, + name="falcon-ensemble/decode_class_label", + ), + ] + ) + outputs = [ + h.make_tensor_value_info(class_labels, onnx.TensorProto.INT64, [None]), + h.make_tensor_value_info( + ensemble_output, + onnx.TensorProto.FLOAT, + [None, len(class_values)], + ), + ] + else: + outputs = [ + h.make_tensor_value_info( + ensemble_output, + onnx.TensorProto.FLOAT, + [None], + ) + ] + + graph = h.make_graph( + nodes, + "falcon_parallel_ensemble", + [shared_input], + outputs, + initializer=initializers, + value_info=value_info, + sparse_initializer=sparse_initializers, + ) + opset_imports = _opset_imports_for_models(model_protos) + if not any(opset.domain in {"", "ai.onnx"} for opset in opset_imports): + opset_imports.append(h.make_operatorsetid("", ONNX_OPSET_VERSION)) + ensemble_model = h.make_model( + graph, + producer_name="Falcon ML", + producer_version=falcon_version, + opset_imports=opset_imports, + ir_version=ONNX_IR_VERSION, + ) + ensemble_model.functions.extend(functions) + return SerializedModelRepr( + ensemble_model, + n_inputs=1, + n_outputs=len(outputs), + initial_types=serialized_models[0].get_initial_types(), + initial_shapes=serialized_models[0].get_initial_shapes(), + ) def serialize_to_onnx( - models_: List[SerializedModelRepr], - init_types: Optional[List] = None, - init_feature_names: Optional[List] = None, - task: Optional[str] = None, + models_: list[SerializedModelRepr], + init_types: list[ColumnTypes] | None = None, + init_feature_names: list[Any] | None = None, + task: str | None = None, ) -> onnx.ModelProto: if init_types is None: init_types = [] if init_feature_names is None: init_feature_names = [] - print_("Serializing to onnx...") if len(models_) == 0: raise ValueError("List of models cannot be empty") - # Updating the models by resetting the opset and adding prefix to node names - updated_models: List[ModelProto] = [] + updated_models: list[ModelProto] = [] models = [m.get_model() for m in models_] + opset_imports = _opset_imports_for_models(models) + if task == "tabular_regression" and not any( + opset.domain in {"", "ai.onnx"} for opset in opset_imports + ): + opset_imports.append(h.make_operatorsetid("", ONNX_OPSET_VERSION)) for i, model in enumerate(models): - op1 = h.make_operatorsetid("", ONNX_OPSET_VERSION) - op2 = h.make_operatorsetid("ai.onnx.ml", ML_ONNX_OPSET_VERSION) - op3 = h.make_operatorsetid("com.microsoft", 1) - updated_model = make_model(model.graph, opset_imports=[op1, op2, op3]) + updated_model = make_model( + model.graph, opset_imports=opset_imports, ir_version=ONNX_IR_VERSION + ) updated_model = add_prefix(updated_model, prefix=f"falcon-pl-{i}/") updated_models.append(updated_model) - # Merging the models sequentially prev: ModelProto = updated_models[0] for i in range(1, len(updated_models)): current: ModelProto = updated_models[i] - prev_outputs: List = prev.graph.output - current_inputs: List = current.graph.input + prev_outputs = prev.graph.output + current_inputs = current.graph.input if len(prev_outputs) > len(current_inputs): prev_outputs = prev_outputs[: len(current_inputs)] if len(prev_outputs) < len(current_inputs): raise RuntimeError( "When merging, previous model should have at least as many outputs as inputs in the next model." ) - io_map: List[Tuple[str, str]] = [] - outputs: List[str] = [o.name for o in current.graph.output] - for p, c in zip(prev_outputs, current_inputs): - mapping: Tuple[str, str] = (p.name, c.name) + io_map: list[tuple[str, str]] = [] + for p, c in zip(prev_outputs, current_inputs, strict=True): + mapping: tuple[str, str] = (p.name, c.name) io_map.append(mapping) - print_(f"\t -> Merging step {i} ::: io_map {io_map} ::: outputs: {outputs}") combined_model: ModelProto = merge_models( - prev, current, io_map=io_map # , outputs=outputs + prev, + current, + io_map=io_map, ) prev = combined_model combined_model = prev + if task == "tabular_regression": + _normalize_regression_output(combined_model) # TODO: Rename the inputs here description = {} if task is not None: description["task"] = task - _rename_inputs(combined_model, init_feature_names, init_types) + _rename_inputs(combined_model, init_feature_names) combined_model.graph.doc_string = str(description) combined_model.producer_name = "Falcon ML" combined_model.producer_version = falcon_version - print_("Serialization completed.") + combined_model.ir_version = ONNX_IR_VERSION + logger.info("Serialization completed.") return combined_model + + +input_tags: dict[ColumnTypes, list[str]] = { + ColumnTypes.NUMERIC_REGULAR: [f"{DEFAULT_PRODUCER_NAME}::numeric:v1"], + ColumnTypes.CAT_LOW_CARD: [f"{DEFAULT_PRODUCER_NAME}::categorical_lc:v1"], + ColumnTypes.CAT_HIGH_CARD: [f"{DEFAULT_PRODUCER_NAME}::categorical_hc:v1"], + ColumnTypes.TEXT_UTF8: [f"{DEFAULT_PRODUCER_NAME}::text:v1"], + ColumnTypes.DATE_YMD_ISO8601: [f"{DEFAULT_PRODUCER_NAME}::date_ymd_iso8601:v1"], + ColumnTypes.DATETIME_YMDHMS_ISO8601: [ + f"{DEFAULT_PRODUCER_NAME}::datetime_ymdhms_iso8601:v1" + ], +} + + +@dataclass +class ModelIO: + name: str + dtype: str + shape: list[int | str] + tags: list[str] | None = None + + +class FNNXSerializer: + out_names: dict[str, list[str]] = { + "tabular_classification": ["probabilities", "y_pred"], + "tabular_regression": ["y_pred", "y_lower", "y_upper"], + } + + artifact_dirs: list[str] = [ + "meta_artifacts", + "ops_artifacts", + "variant_artifacts", + ] + + def __init__( + self, + models: list[SerializedModelRepr], + init_types: list[ColumnTypes] | None = None, + init_feature_names: list[Any] | None = None, + task: str | None = None, + producer_name: str | None = DEFAULT_PRODUCER_NAME, + producer_version: str | None = falcon_version, + producer_extra_tags: list[str] | None = None, + description: str = "", + schema: DatasetSchema | None = None, + ) -> None: + self.models: list[SerializedModelRepr] = models + if schema is not None: + init_types = list(schema.column_types) + init_feature_names = list(schema.column_names) + self.init_types: list[ColumnTypes] | None = init_types + self.schema = schema + + self.task: str | None = task + self.description: str = description + + self.model_proto: onnx.ModelProto = serialize_to_onnx( + models, init_types, init_feature_names, task + ) + + self.init_feature_names: list[Any] = init_feature_names or [ + f"input_{i}" for i in range(len(self.model_proto.graph.input)) + ] + + self.inputs: list[ModelIO] = self._map_io( + self.model_proto.graph.input, self.init_feature_names + ) + output_names = self.out_names.get(task or "") + if output_names is None: + raise ValueError(f"Unknown task `{task}`") + output_count = len(self.model_proto.graph.output) + valid_output_counts = {2} if task == "tabular_classification" else {1, 3} + if output_count not in valid_output_counts: + raise ValueError( + f"Task `{task}` produced an unexpected number of outputs: " + f"{output_count}" + ) + self.outputs: list[ModelIO] = self._map_io( + self.model_proto.graph.output, + output_names[:output_count], + ) + + if init_types is not None and len(init_types) == len(self.inputs): + for i, t in enumerate(init_types): + self.inputs[i].tags = copy(input_tags.get(t, [])) + + self.producer_name: str | None = producer_name + self.producer_version: str | None = producer_version + self.producer_extra_tags: list[str] = producer_extra_tags or [] + self.metadata_container: dict[str, Any] = { + "id": "falcon_metrics", + "producer": DEFAULT_PRODUCER_NAME, + "producer_version": falcon_version, + "producer_tags": [f"{DEFAULT_PRODUCER_NAME}::{self.task}_metrics:v1"], + } + self.metadata_payload: dict[str, Any] = {} + self.metadata_container["payload"] = self.metadata_payload + self.fh: TarHandler = TarHandler() + + def _map_io( + self, io_: Sequence[onnx.ValueInfoProto], names: Sequence[str] + ) -> list[ModelIO]: + processed: list[ModelIO] = [] + for el, name in zip(io_, names, strict=True): + shape = [ + ( + dim.dim_value + if dim.dim_value != 0 + else (dim.dim_param if dim.dim_param else "batch") + ) + for dim in el.type.tensor_type.shape.dim + ] + elem_type = onnx_type_map.get(el.type.tensor_type.elem_type) + if elem_type is None: + raise ValueError( + f"Unknown element type: {el.type.tensor_type.elem_type}" + ) + processed.append(ModelIO(name, f"Array[{elem_type}]", shape)) + return processed + + def serialize(self) -> bytes: + self._add_manifest() + self._add_env() + self._add_variant_config() + for d in self.artifact_dirs: + self.fh.add_directory(d) + self._add_onnx() + self.fh.add_json("dtypes.json", {}) + if len(self.metadata_payload.keys()) > 0: + metadata = [self.metadata_container] + else: + metadata = [] + self.fh.add_json("meta.json", metadata) + return self.fh.finalize() + + def _add_manifest(self) -> None: + manifest: dict[str, Any] = { + "variant": "pipeline", + "description": self.description, + "producer_name": self.producer_name, + "producer_version": self.producer_version, + "producer_tags": [f"{DEFAULT_PRODUCER_NAME}::{self.task}:v1"] + + self.producer_extra_tags, + "inputs": [], + "outputs": [], + "dynamic_attributes": [], + "env_vars": [], + } + if self.schema is not None: + manifest["schema"] = self.schema.to_dict() + + for io_ in self.inputs: + inp: dict[str, Any] = { + "name": io_.name, + "content_type": "NDJSON", + "dtype": io_.dtype, + "shape": io_.shape, + } + if io_.tags: + inp["tags"] = io_.tags + manifest["inputs"].append(inp) + + for io_ in self.outputs: + manifest["outputs"].append( + { + "name": io_.name, + "content_type": "NDJSON", + "dtype": io_.dtype, + "shape": io_.shape, + } + ) + + self.fh.add_json("manifest.json", manifest) + + def _add_variant_config(self) -> None: + config: dict[str, Any] = { + "nodes": [ + { + "op_instance_id": "onnx_main", + "extra_dynattrs": {}, + "inputs": [i.name for i in self.inputs], + "outputs": [o.name for o in self.outputs], + } + ] + } + + self.fh.add_json("variant_config.json", config) + + def _add_onnx(self) -> None: + op: list[dict[str, Any]] = [ + { + "id": "onnx_main", + "op": "ONNX_v1", + "inputs": [{"dtype": i.dtype, "shape": i.shape} for i in self.inputs], + "outputs": [{"dtype": o.dtype, "shape": o.shape} for o in self.outputs], + "dynamic_attributes": {}, + "attributes": { + "opsets": [ + { + "domain": ( + "ai.onnx" + if opset.domain in {"", "ai.onnx"} + else opset.domain + ), + "version": opset.version, + } + for opset in self.model_proto.opset_import + ], + "requires_ort_extensions": False, + "has_external_data": False, # TODO + "onnx_ir_version": self.model_proto.ir_version, + }, + } + ] + + self.fh.add_json("ops.json", op) + + onnx_model = self.model_proto.SerializeToString() + + self.fh.add_directory("ops_artifacts/onnx_main") + self.fh.add_file("ops_artifacts/onnx_main/model.onnx", onnx_model) + + def _add_env(self) -> None: + self.fh.add_json("env.json", {}) # TODO + + +class TarHandler: + def __init__(self) -> None: + self.tar_buffer: io.BytesIO = io.BytesIO() + self.tar: tarfile.TarFile = tarfile.open(fileobj=self.tar_buffer, mode="w") + self.finalized: bool = False + + def _assert_not_finalized(self) -> None: + if self.finalized: + raise RuntimeError("Tar file is already finalized.") + + def add_directory(self, directory_name: str) -> None: + self._assert_not_finalized() + if not directory_name.endswith("/"): + directory_name += "/" + folder_info = tarfile.TarInfo(name=directory_name) + folder_info.type = tarfile.DIRTYPE + folder_info.mode = 0o755 + self.tar.addfile(tarinfo=folder_info) + + def add_file(self, file_path: str, content: bytes | str) -> None: + self._assert_not_finalized() + if isinstance(content, str): + content = content.encode() + file_data = io.BytesIO(content) + file_info = tarfile.TarInfo(name=file_path) + file_info.size = len(file_data.getvalue()) + file_info.mode = 0o644 + self.tar.addfile(tarinfo=file_info, fileobj=file_data) + + def add_json(self, file_path: str, data: Any) -> None: + json_content = json.dumps(data, indent=4) + self.add_file(file_path, json_content) + + def finalize(self) -> bytes: + self.tar.close() + self.tar_buffer.seek(0) + self.finalized = True + return self.tar_buffer.getvalue() diff --git a/falcon/sklapi.py b/falcon/sklapi.py index aa4d945..6b96d85 100644 --- a/falcon/sklapi.py +++ b/falcon/sklapi.py @@ -1,148 +1,144 @@ -from sklearn.base import ( - BaseEstimator as _BaseEstimator, - ClassifierMixin as _ClassifierMixin, - RegressorMixin as _RegressorMixin, -) -from typing import Optional, Union, Dict, Callable +from __future__ import annotations + +from collections.abc import Callable +from datetime import datetime +from typing import Any + import pandas as pd from numpy import typing as npt -from sklearn.utils.validation import check_X_y, check_array, check_is_fitted -from sklearn.utils.multiclass import unique_labels -from falcon.main import initialize -from falcon.task_configurations import get_task_configuration -from datetime import datetime +from sklearn.base import BaseEstimator as _BaseEstimator +from sklearn.base import ClassifierMixin as _ClassifierMixin +from sklearn.base import RegressorMixin as _RegressorMixin from sklearn.model_selection import BaseCrossValidator -from falcon.utils import set_eval_strategy +from sklearn.utils.multiclass import unique_labels +from sklearn.utils.validation import check_array, check_is_fitted, check_X_y + +from falcon.config import RunConfig +from falcon.predictor import Predictor +from falcon.tabular.splitting import GroupBy, resolve_groups +from falcon.utils import logger class _FalconBaseEstimator(_BaseEstimator): + _task: str + def __init__( self, - config: Union[str, Dict] = "SuperLearner", - eval_strategy: Optional[Union[str, Callable, BaseCrossValidator]] = "dynamic", + preset: str | RunConfig = "balanced", + eval_strategy: str | Callable[..., Any] | BaseCrossValidator | None = "dynamic", ) -> None: - """ - Parameters - ---------- - config : Union[str, Dict], optional - configuration to be used, by default "SuperLearner" - eval_strategy : Optional[Union[str, Callable, BaseCrossValidator]], optional - evaluation strategy, can be one of {'auto', 'holdout' 'cv', BaseCrossValidator, Callable} by default 'dynamic'. - If 'auto', uses 5 fold CV for small datasets and holdout for large ones. - If 'holdout', uses holdout strategy with 25% of data for validation. - If 'cv', uses 5 fold CV. - If BaseCrossValidator, uses the specified cross-validator. - If Callable, uses the specified function to split data into train and validation sets. - If None, no evaluation will be performed. - """ - self.config = config + self.preset = preset self.eval_strategy = eval_strategy - def _get_tags(self) -> Dict: + def __sklearn_tags__(self) -> Any: + tags = super().__sklearn_tags__() + tags.input_tags.string = True + return tags + + def _get_tags(self) -> dict[str, Any]: tags = super()._get_tags() if "string" not in tags["X_types"]: tags["X_types"].append("string") - tags["non_deterministic"] = True return tags - def _get_task_config(self) -> Dict: - if isinstance(self.config, dict): - config = self.config - elif isinstance(self.config, str): - config = get_task_configuration("tabular_classification", self.config) - else: - raise ValueError("Invalid configuration") - return config - - def predict(self, X: Union[pd.DataFrame, npt.NDArray]) -> npt.NDArray: - check_is_fitted(self) - X = check_array(X, dtype=None) - y = self.manager_.predict(X) - return y - - def save_model(self, filename: Optional[str]) -> None: - """ - Saves model in onnx format - - Parameters - ---------- - filename : str, optional - filename of the saved model - """ + def _resolve_fit_groups( + self, + X: pd.DataFrame | npt.NDArray[Any], + group_by: GroupBy | None, + ) -> GroupBy | None: + if group_by is None or not isinstance(X, pd.DataFrame): + return group_by + return resolve_groups( + X.to_numpy(dtype=object), + tuple(str(column) for column in X.columns), + group_by, + ) + + def _new_predictor(self) -> Predictor: + eval_strategy = ( + "auto" if self.eval_strategy == "dynamic" else self.eval_strategy + ) + if isinstance(self.preset, RunConfig): + return Predictor( + task=self._task, + config=self.preset, + eval_strategy=eval_strategy, + ) + return Predictor( + task=self._task, + preset=self.preset, + eval_strategy=eval_strategy, + ) + + def _fit_predictor( + self, + X: pd.DataFrame | npt.NDArray[Any], + y: pd.DataFrame | npt.NDArray[Any], + group_by: GroupBy | None, + ) -> None: + resolved_groups = self._resolve_fit_groups(X, group_by) + checked_X, checked_y = check_X_y(X, y, dtype=None) + self.n_features_in_ = checked_X.shape[1] + self.predictor_ = self._new_predictor() + self.predictor_.fit((checked_X, checked_y), group_by=resolved_groups) + + def predict(self, X: pd.DataFrame | npt.NDArray[Any]) -> npt.NDArray[Any]: + check_is_fitted(self, "predictor_") + checked_X = check_array(X, dtype=None) + return self.predictor_.predict(checked_X) + + def save_model(self, filename: str | None = None) -> None: + check_is_fitted(self, "predictor_") if filename is None: - ts = datetime.now().strftime("%Y%m%d.%H%M%S") - filename = f"falcon_{ts}.onnx" - print("Saving the model ...") - self.manager_.save_model(format="onnx", filename=filename) - print(f"The model was saved as `{filename}`") + timestamp = datetime.now().strftime("%Y%m%d.%H%M%S") + filename = f"falcon_{timestamp}.fnnx" + elif not filename.endswith(".fnnx"): + filename = f"{filename}.fnnx" + self.predictor_.save(filename) + logger.info("The model was saved as `%s`", filename) -class FalconTabularClassifier(_FalconBaseEstimator, _ClassifierMixin): - """ - Falcon sklearn wrapper to be used for tabular classification tasks. - Alternatively, `FalconClassifier` can be used as an alias. - """ +class FalconTabularClassifier(_ClassifierMixin, _FalconBaseEstimator): + _task = "tabular_classification" def fit( - self, X: Union[pd.DataFrame, npt.NDArray], y: Union[pd.DataFrame, npt.NDArray] - ) -> _FalconBaseEstimator: - """ - Fits the classifier - - Parameters - ---------- - X : Union[pd.DataFrame, npt.NDArray] - data - y : Union[pd.DataFrame, npt.NDArray] - labels - """ - X, y = check_X_y(X, y, dtype=None) + self, + X: pd.DataFrame | npt.NDArray[Any], + y: pd.DataFrame | npt.NDArray[Any], + group_by: GroupBy | None = None, + ) -> FalconTabularClassifier: self.classes_ = unique_labels(y) - config = self._get_task_config() - self.n_features_in_ = X.shape[1] - set_eval_strategy(self.eval_strategy, config, None) - self.manager_ = initialize( - task="tabular_classification", - data=(X, y), - **config, - ) - self.manager_.train() - self.manager_.performance_summary(None) + self._fit_predictor(X, y, group_by) return self + def predict_proba( + self, + X: pd.DataFrame | npt.NDArray[Any], + ) -> npt.NDArray[Any]: + check_is_fitted(self, "predictor_") + checked_X = check_array(X, dtype=None) + return self.predictor_.predict_proba(checked_X) + -class FalconTabularRegressor(_FalconBaseEstimator, _RegressorMixin): - """ - Falcon sklearn wrapper to be used for tabular regression tasks. - Alternatively, `FalconRegressor` can be used as an alias. - """ +class FalconTabularRegressor(_RegressorMixin, _FalconBaseEstimator): + _task = "tabular_regression" def fit( - self, X: Union[pd.DataFrame, npt.NDArray], y: Union[pd.DataFrame, npt.NDArray] - ) -> _FalconBaseEstimator: - """ - Fits the regressor - - Parameters - ---------- - X : Union[pd.DataFrame, npt.NDArray] - data - y : Union[pd.DataFrame, npt.NDArray] - labels - """ - X, y = check_X_y(X, y, dtype=None) - config = self._get_task_config() - self.n_features_in_ = X.shape[1] - set_eval_strategy(self.eval_strategy, config, None) - self.manager_ = initialize( - task="tabular_regression", - data=(X, y), - **config, - ) - self.manager_.train() - self.manager_.performance_summary(None) + self, + X: pd.DataFrame | npt.NDArray[Any], + y: pd.DataFrame | npt.NDArray[Any], + group_by: GroupBy | None = None, + ) -> FalconTabularRegressor: + self._fit_predictor(X, y, group_by) return self FalconClassifier = FalconTabularClassifier FalconRegressor = FalconTabularRegressor + +__all__ = [ + "FalconClassifier", + "FalconRegressor", + "FalconTabularClassifier", + "FalconTabularRegressor", +] diff --git a/falcon/tabular/__init__.py b/falcon/tabular/__init__.py index 85f8ed6..c9c2ef6 100644 --- a/falcon/tabular/__init__.py +++ b/falcon/tabular/__init__.py @@ -1,5 +1 @@ -from falcon.tabular.tabular_manager import TabularTaskManager -from falcon.tabular import pipelines -from falcon.tabular import processors -from falcon.tabular import learners -from falcon.tabular.adapters.ts.adapter import TSAdapter \ No newline at end of file +__all__: list[str] = [] diff --git a/falcon/tabular/adapters/ts/adapter.py b/falcon/tabular/adapters/ts/adapter.py deleted file mode 100644 index a3e8d97..0000000 --- a/falcon/tabular/adapters/ts/adapter.py +++ /dev/null @@ -1,127 +0,0 @@ -import pandas as pd -from typing import Union, List, Dict, Tuple, Optional -import numpy as np -from falcon.task_configurations import get_task_configuration -from falcon.tabular.adapters.ts.auxiliary import _create_window, _split_fn -from falcon.tabular.adapters.ts.pipeline import TSAdapterPipeline -from falcon.tabular.adapters.ts.plot_errors import _plot_errors -from falcon.abstract import TaskManager -from sklearn.metrics import mean_absolute_error, r2_score - - -class TSAdapter: - def __init__( - self, - dataframe: pd.DataFrame, - target: str, - window_size: int = 8, - adapt_for: str = "tabular_regression", - config: Union[str, Dict] = "PlainLearner", - eval_size: float = 0.2, - ): - self.dataframe = dataframe - self.window_size = window_size - if adapt_for not in ("tabular_classification", "tabular_regression"): - raise ValueError( - "adapt_for should be one of (tabular_classification, tabular_regression" - ) - if adapt_for == "tabular_classification": - raise NotImplementedError("tabular_classification is not yet implemented") - if not isinstance(dataframe, pd.DataFrame): - raise ValueError("invalid dataframe type, only pd.DataFrame is supported") - if target not in dataframe.columns: - raise ValueError(f"Provided target {target} was not found") - if window_size <= 1 or window_size >= dataframe.shape[0] - 1: - raise ValueError( - "Invalid window size. Minimum value is 2, maximum value is (the number of rows in the dataframe - 1)" - ) - self.target = target - self._adapt_for = adapt_for - self.config = config - if eval_size <= 0.0 or eval_size >= 1.0: - raise ValueError("eval_size should be in the range (O., 1.)") - self.eval_size = eval_size - self._manager: Optional[TaskManager] = None - - def adapt(self, target_function: str = "AutoML") -> Dict: - if target_function not in ("AutoML", "initialize"): - raise ValueError("target_function should be one of (AutoML, initialize)") - data = self.dataframe[self.target].astype(np.float32) - df = pd.DataFrame({"y": data}) - df = _create_window(df, self.window_size) - if isinstance(self.config, str): - config = get_task_configuration(self._adapt_for, self.config) - else: - config = self.config - eval_strategy_fn = lambda X, y: _split_fn(X, y, self.eval_size) - wrapped_pipeline = config["pipeline"] - wrapped_pipeline_options = config["extra_pipeline_options"] - config["pipeline"] = TSAdapterPipeline - config["extra_pipeline_options"] = { - "wrapped_pipeline": wrapped_pipeline, - "wrapped_pipeline_options": wrapped_pipeline_options, - } - config["eval_strategy"] = eval_strategy_fn - if target_function == "AutoML": - config = {"config": config} - config["train_data"] = df - else: - config["data"] = df - config["task"] = self._adapt_for - config["features"] = list(df.columns[:-1]) - config["target"] = "y" - return config - - def bind(self, manager: TaskManager) -> None: - self._manager = manager - - def evaluate( - self, forecast_period: int = 1, visualize: bool = False - ) -> Optional[pd.DataFrame]: - if self._manager is None: - raise ValueError("Manager is not bound. Please call .bind() method first") - if not hasattr(self._manager, "_eval_set"): - print("Cannot evaluate. No evaluation set was provided") - return None - _train = self._manager._data - _eval = self._manager._eval_set # type: ignore - _train = _train[1].squeeze() - - forecast_period_m1 = forecast_period - 1 - predictions = np.zeros(shape=(len(_eval[1]) - forecast_period_m1,)) - - trunc = forecast_period_m1 if forecast_period_m1 > 0 else -len(_eval[1]) - for i, datapoint in enumerate(_eval[0][:-trunc]): - datapoint = datapoint.reshape(1, -1) - predictions[i] = self.predict(datapoint, forecast_period=forecast_period)[ - -1 - ] - - metrics = { - "FORECAST_WINDOW": [forecast_period], - "N_FORECASTS": [len(predictions)], - "MAE": [mean_absolute_error(_eval[1][forecast_period_m1:], predictions)], - "R2": [r2_score(_eval[1][forecast_period_m1:], predictions)], - } - df_metrics = pd.DataFrame(metrics) - print(df_metrics) - if visualize: - _plot_errors(_train, _eval[1], predictions) - return df_metrics - - def predict( - self, X: Union[pd.DataFrame, np.ndarray], forecast_period: int = 3 - ) -> np.ndarray: - if self._manager is None: - raise ValueError("Manager is not bound. Please call .bind() method first") - if isinstance(X, pd.DataFrame): - X = X.to_numpy() - X = X.copy() - predictions = np.zeros(shape=(forecast_period,)) - for i in range(forecast_period): - # print(X, X.shape) - pred = self._manager.predict(X)[0] - predictions[i] = pred - X = np.roll(X, -1) - X[0, -1] = pred - return predictions diff --git a/falcon/tabular/adapters/ts/auxiliary.py b/falcon/tabular/adapters/ts/auxiliary.py deleted file mode 100644 index abe02ac..0000000 --- a/falcon/tabular/adapters/ts/auxiliary.py +++ /dev/null @@ -1,117 +0,0 @@ -import pandas as pd -import numpy as np -from typing import Tuple, List -from onnx import ModelProto, TensorProto, helper as h, NodeProto - - -def _create_window(df: pd.DataFrame, window_size: int = 5) -> pd.DataFrame: - for i in range(window_size): - col_name = f"X{window_size - i}" - df[col_name] = df["y"].shift(i + 1) - df = df.iloc[window_size:] - return df[df.columns[::-1]] - - -def _split_fn( - X: np.ndarray, y: np.ndarray, eval_size: float = 0.2 -) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: - train_ind = int(len(X) * (1 - eval_size)) - X_train, X_test = X[:train_ind], X[train_ind:] - y_train, y_test = y[:train_ind], y[train_ind:] - return X_train, X_test, y_train, y_test - - -def _wrap_onnx(m: ModelProto) -> None: - input_names_inorder = [i.name for i in m.graph.input] - new_names = [f"new_input_{i}" for i in range(len(input_names_inorder))] - - for i, _ in reversed(list(enumerate(m.graph.input))): - del m.graph.input[i] - - additional_nodes: List[NodeProto] = [] - - inputs = [ - h.make_tensor_value_info(new_names[i], TensorProto.FLOAT, [None, 1]) - for i in range(len(input_names_inorder)) - ] - - sample_mean = h.make_node( - "Mean", - inputs=new_names, - outputs=["sample_mean"], - domain="", - name="calculate_sample_mean", - ) - additional_nodes.insert(0, sample_mean) - reshape_shape = h.make_tensor( - name="shape_", - data_type=TensorProto.INT64, - dims=[2], - vals=np.asarray([-1, 1]).astype(np.int64), - ) - m.graph.initializer.append(reshape_shape) - reshape_mean = h.make_node( - "Reshape", - inputs=["sample_mean", "shape_"], - outputs=["sample_mean_reshaped"], - domain="", - name="reshape_mean", - ) - additional_nodes.insert(0, reshape_mean) - array_feature_extract_out_names = [] - for i in range(len(new_names)): - node_out_name = f"sample_normalized_{i}" - sub = h.make_node( - "Sub", - inputs=[new_names[i], "sample_mean_reshaped"], - outputs=[node_out_name], - domain="", - name=f"normalize_sample_{i}", - ) - additional_nodes.insert(0, sub) - array_feature_extract_out_names.append(node_out_name) - - reshape_output = h.make_node( - "Reshape", - inputs=["model_result", "shape_"], - outputs=["model_result_reshaped"], - domain="", - name="reshape_model_result", - ) - additional_nodes.insert(0, reshape_output) - add_ = h.make_node( - "Add", - inputs=["model_result_reshaped", "sample_mean_reshaped"], - outputs=["result_denormalized"], - domain="", - name="add_mean", - ) - additional_nodes.insert(0, add_) - squeeze = h.make_node( - "Squeeze", - inputs=["result_denormalized"], - outputs=["ts_output"], - domain="", - name="squeeze_result", - ) - additional_nodes.insert(0, squeeze) - - for n in inputs: - m.graph.input.append(n) - - for i in additional_nodes: - m.graph.node.insert(0, i) - - new_output = h.make_tensor_value_info("ts_output", TensorProto.FLOAT, [None, 1]) - old_output_name = m.graph.output[0].name - del m.graph.output[0] - m.graph.output.append(new_output) - - for n in m.graph.node: - for (old_n, new_n) in zip(input_names_inorder, array_feature_extract_out_names): - for i, inp in enumerate(n.input): - if inp == old_n: - n.input[i] = new_n - for i, out in enumerate(n.output): - if out == old_output_name: - n.output[i] = "model_result" diff --git a/falcon/tabular/adapters/ts/learner.py b/falcon/tabular/adapters/ts/learner.py deleted file mode 100644 index f366ee2..0000000 --- a/falcon/tabular/adapters/ts/learner.py +++ /dev/null @@ -1,78 +0,0 @@ -import numpy as np -from typing import Dict, Any, Type, List, Tuple, Optional -from falcon.types import Float32Array, Int64Array -from falcon.abstract.task_pipeline import Pipeline -from falcon.abstract.learner import Learner -from falcon.abstract.onnx_convertible import ONNXConvertible -from falcon.serialization import SerializedModelRepr -from falcon.tabular.adapters.ts.auxiliary import _wrap_onnx - - -class TSAdapterLearner(Learner, ONNXConvertible): - def __init__( - self, - task: str, - dataset_size: Tuple[int, ...], - mask: List, - wrapped_pipeline: Type[Pipeline], - wrapped_pipeline_options: Dict, - ) -> None: - self.task = task - self.mask = mask - self.dataset_size = dataset_size - - self.wrapped_pipeline = wrapped_pipeline - self.wrapped_pipeline_options = wrapped_pipeline_options - self.data_shape: List[Optional[int]] = [] - self.n_inputs: int = 1 - - def fit(self, X: np.ndarray, y: np.ndarray, *args: Any, **kwargs: Any) -> None: - mean = np.mean(X, axis=1).reshape(-1, 1) - y = y.reshape(-1, 1) - self._pipeline = self.wrapped_pipeline( - task=self.task, - mask=self.mask, - dataset_size=self.dataset_size, - **self.wrapped_pipeline_options - ) - self.data_shape = [None, X.shape[1]] - self._pipeline.fit(X - mean, y - mean) - - def predict(self, X: np.ndarray, *args: Any, **kwargs: Any) -> np.ndarray: - mean = np.mean(X, axis=1).reshape(-1, 1) - pred = self._pipeline.predict(X - mean).reshape(-1, 1) - pred = pred + mean - return pred.squeeze(1) - - def to_onnx(self) -> SerializedModelRepr: - onx = self._pipeline.save() - _wrap_onnx(onx) - _last_axis: int = ( - self.data_shape[1] if self.data_shape[1] else 1 - ) # just for type compatibility - sm = SerializedModelRepr( - model=onx, - n_inputs=_last_axis, - n_outputs=1, - initial_types=["float32" for _ in range(_last_axis)], - initial_shapes=[self.data_shape], - ) - return sm - - def get_input_type(self) -> Type: - """ - Returns - ------- - Type - Float32Array - """ - return Float32Array - - def get_output_type(self) -> Type: - """ - Returns - ------- - Type - Float32Array for regression, Int64Array for classification - """ - return Float32Array if self.task == "tabular_regression" else Int64Array diff --git a/falcon/tabular/adapters/ts/pipeline.py b/falcon/tabular/adapters/ts/pipeline.py deleted file mode 100644 index dafddc1..0000000 --- a/falcon/tabular/adapters/ts/pipeline.py +++ /dev/null @@ -1,37 +0,0 @@ -import numpy as np -from numpy import typing as npt -from typing import Dict, Type, List, Any, Tuple -from falcon.abstract.task_pipeline import Pipeline -from falcon.abstract.learner import Learner -from falcon.abstract.onnx_convertible import ONNXConvertible -from falcon.tabular.adapters.ts.learner import TSAdapterLearner - - -class TSAdapterPipeline(Pipeline): - def __init__( - self, - task: str, - dataset_size: Tuple[int], - mask: List[Any], - wrapped_pipeline: Type[Pipeline], - wrapped_pipeline_options: Dict, - **kwargs: Any - ) -> None: - super().__init__(task, dataset_size, mask) - self.wrapped_pipeline = wrapped_pipeline - self.wrapped_pipeline_options = wrapped_pipeline_options - - def fit(self, X: np.ndarray, y: np.ndarray, *args: Any, **kwargs: Any) -> None: - self._pipeline = [] - learner = TSAdapterLearner( - self.task, - self.dataset_size, - self.mask, - self.wrapped_pipeline, - self.wrapped_pipeline_options, - ) - self.add_element(learner) - learner.fit(X, y) - - def predict(self, X: npt.NDArray, *args: Any, **kwargs: Any) -> npt.NDArray: - return self._pipeline[0].predict(X) diff --git a/falcon/tabular/adapters/ts/plot_errors.py b/falcon/tabular/adapters/ts/plot_errors.py deleted file mode 100644 index e0e33d3..0000000 --- a/falcon/tabular/adapters/ts/plot_errors.py +++ /dev/null @@ -1,18 +0,0 @@ -try: - from matplotlib import pyplot as plt -except (ImportError, ModuleNotFoundError): - plt = None -import numpy as np - -def _plot_errors(train_data: np.ndarray, test_data: np.ndarray, pred: np.ndarray) -> None: - if plt is None: - print("matplotlib is not installed") - return None - window_size = len(test_data) - len(pred) - - plt.figure(figsize=(10, 5)) - plt.plot(np.arange(len(train_data)), train_data, label="train") - plt.plot(np.arange(len(train_data), len(train_data) + len(test_data)), test_data, label="test", alpha = 0.75) - plt.plot(np.arange(len(train_data) + window_size, len(train_data) + len(test_data)), pred, label="forecast", alpha = 0.75) - plt.legend() - plt.show() \ No newline at end of file diff --git a/falcon/tabular/calibration.py b/falcon/tabular/calibration.py new file mode 100644 index 0000000..b8dcdcc --- /dev/null +++ b/falcon/tabular/calibration.py @@ -0,0 +1,193 @@ +from __future__ import annotations + +from copy import deepcopy +from typing import Any + +import numpy as np +import onnx +from numpy import typing as npt +from onnx import helper +from scipy.optimize import minimize_scalar + +from falcon.config import ONNX_OPSET_VERSION +from falcon.serialization import SerializedModelRepr + +_PROBABILITY_FLOOR = np.float32(1e-7) +_MIN_LOG_TEMPERATURE = -5.0 +_MAX_LOG_TEMPERATURE = 5.0 + + +def temperature_scale_probabilities( + probabilities: npt.NDArray[Any], + temperature: float, +) -> npt.NDArray[np.float32]: + values = np.asarray(probabilities, dtype=np.float32) + if values.ndim != 2 or values.shape[0] == 0 or values.shape[1] < 2: + raise ValueError( + "Classification probabilities must contain at least two class columns" + ) + if not np.isfinite(values).all() or (values < 0).any(): + raise ValueError("Classification probabilities must be finite and non-negative") + if (values.sum(axis=1) <= 0).any(): + raise ValueError("Each probability row must contain a positive value") + if not np.isfinite(temperature) or temperature <= 0: + raise ValueError("Temperature must be a finite value greater than zero") + + logits = np.log(np.maximum(values, _PROBABILITY_FLOOR)) / np.float32(temperature) + logits -= np.max(logits, axis=1, keepdims=True) + exponentials = np.exp(logits) + return np.asarray( + exponentials / np.sum(exponentials, axis=1, keepdims=True), + dtype=np.float32, + ) + + +def _validated_targets( + targets: npt.NDArray[Any], + n_rows: int, + n_classes: int, +) -> npt.NDArray[np.int64]: + values = np.asarray(targets) + if values.ndim == 2 and values.shape[1] == 1: + values = values[:, 0] + if values.ndim != 1 or len(values) != n_rows: + raise ValueError( + "Calibration targets must contain one value per probability row" + ) + if not np.issubdtype(values.dtype, np.integer): + raise ValueError("Calibration targets must be integer encoded") + encoded = values.astype(np.int64, copy=False) + if (encoded < 0).any() or (encoded >= n_classes).any(): + raise ValueError("Calibration targets contain an unknown class index") + return encoded + + +def _negative_log_likelihood( + probabilities: npt.NDArray[np.float32], + targets: npt.NDArray[np.int64], +) -> float: + selected = probabilities[np.arange(len(targets)), targets] + return -float(np.mean(np.log(np.maximum(selected, _PROBABILITY_FLOOR)))) + + +def fit_temperature( + probabilities: npt.NDArray[Any], + targets: npt.NDArray[Any], +) -> float: + values = temperature_scale_probabilities(probabilities, 1.0) + encoded_targets = _validated_targets(targets, len(values), values.shape[1]) + baseline_loss = _negative_log_likelihood(values, encoded_targets) + + def objective(log_temperature: float) -> float: + scaled = temperature_scale_probabilities( + values, + float(np.exp(log_temperature)), + ) + return _negative_log_likelihood(scaled, encoded_targets) + + result = minimize_scalar( + objective, + bounds=(_MIN_LOG_TEMPERATURE, _MAX_LOG_TEMPERATURE), + method="bounded", + options={"xatol": 1e-5}, + ) + if not result.success: + return 1.0 + temperature = float(np.float32(np.exp(result.x))) + if objective(float(np.log(temperature))) >= baseline_loss - 1e-7: + return 1.0 + return temperature + + +def serialize_temperature_scaling( + serialized: SerializedModelRepr, + temperature: float, +) -> SerializedModelRepr: + if not np.isfinite(temperature) or temperature <= 0: + raise ValueError("Temperature must be a finite value greater than zero") + + model = deepcopy(serialized.get_model()) + if len(model.graph.output) < 2: + raise ValueError( + "A calibrated classifier graph must expose labels and probabilities" + ) + probability_output = model.graph.output[-1] + if ( + not probability_output.type.HasField("tensor_type") + or probability_output.type.tensor_type.elem_type != onnx.TensorProto.FLOAT + ): + raise ValueError("Classifier probabilities must be a float tensor") + + source_name = probability_output.name + floor_name = "falcon_temperature_probability_floor" + temperature_name = "falcon_temperature_value" + clipped_name = "falcon_temperature_clipped_probabilities" + logits_name = "falcon_temperature_log_probabilities" + scaled_logits_name = "falcon_temperature_scaled_logits" + calibrated_name = "falcon_calibrated_probabilities" + model.graph.initializer.extend( + [ + helper.make_tensor( + floor_name, + onnx.TensorProto.FLOAT, + [], + [float(_PROBABILITY_FLOOR)], + ), + helper.make_tensor( + temperature_name, + onnx.TensorProto.FLOAT, + [], + [temperature], + ), + ] + ) + model.graph.node.extend( + [ + helper.make_node( + "Clip", + [source_name, floor_name], + [clipped_name], + name="falcon_temperature/clip", + ), + helper.make_node( + "Log", + [clipped_name], + [logits_name], + name="falcon_temperature/log", + ), + helper.make_node( + "Div", + [logits_name, temperature_name], + [scaled_logits_name], + name="falcon_temperature/divide", + ), + helper.make_node( + "Softmax", + [scaled_logits_name], + [calibrated_name], + axis=1, + name="falcon_temperature/softmax", + ), + ] + ) + calibrated_output = deepcopy(probability_output) + calibrated_output.name = calibrated_name + model.graph.output[-1].CopyFrom(calibrated_output) + if not any(opset.domain in {"", "ai.onnx"} for opset in model.opset_import): + model.opset_import.append(helper.make_opsetid("", ONNX_OPSET_VERSION)) + + return SerializedModelRepr( + model, + serialized.get_n_inputs(), + serialized.get_n_outputs(), + serialized.get_initial_types().copy(), + [shape.copy() for shape in serialized.get_initial_shapes()], + serialized.get_type(), + ) + + +__all__ = [ + "fit_temperature", + "serialize_temperature_scaling", + "temperature_scale_probabilities", +] diff --git a/falcon/tabular/candidates.py b/falcon/tabular/candidates.py new file mode 100644 index 0000000..e4d3b07 --- /dev/null +++ b/falcon/tabular/candidates.py @@ -0,0 +1,1276 @@ +from __future__ import annotations + +from collections.abc import Callable, Mapping, Sequence +from dataclasses import dataclass, field +from time import monotonic +from types import MappingProxyType +from typing import Any, Protocol + +import numpy as np +from numpy import typing as npt +from sklearn.ensemble import ( + ExtraTreesClassifier, + ExtraTreesRegressor, + HistGradientBoostingClassifier, + HistGradientBoostingRegressor, + RandomForestClassifier, + RandomForestRegressor, +) +from sklearn.linear_model import LogisticRegression, Ridge +from sklearn.metrics import balanced_accuracy_score +from sklearn.utils.class_weight import compute_sample_weight +from sklearn.utils.validation import has_fit_parameter + +from falcon.config import ClassWeight +from falcon.constants import TABULAR_CLASSIFICATION_TASK, TABULAR_REGRESSION_TASK +from falcon.serialization import SerializedModelRepr, serialize_parallel_ensemble +from falcon.tabular.models.gbdt import GBDTModel, get_gbdt_model_classes +from falcon.tabular.models.sklearn_model import SklearnModel +from falcon.tabular.splitting import holdout_indices, out_of_fold_indices +from falcon.types import Float32Array, Int64Array +from falcon.utils import logger + +_DEFAULT_RESERVE_FRACTION = 0.2 +_EARLY_STOPPING_TEST_SIZE = 0.2 +_SKLEARN_FAMILIES = { + "extra_trees", + "hist_gradient_boosting", + "linear", + "random_forest", +} + + +class CandidateModel(Protocol): + def fit( + self, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + *, + sample_weight: npt.NDArray[np.float64] | None = None, + validation_data: tuple[npt.NDArray[Any], npt.NDArray[Any]] | None = None, + early_stopping_rounds: int | None = None, + ) -> None: ... + + def predict(self, X: npt.NDArray[Any]) -> npt.NDArray[Any]: ... + + def serialize(self) -> SerializedModelRepr: ... + + +class CandidateClassifierModel(CandidateModel, Protocol): + def predict_proba(self, X: npt.NDArray[Any]) -> npt.NDArray[Any]: ... + + +CandidateModelFactory = Callable[ + ["EstimatorSpec", str, int, int | None], CandidateModel +] + + +@dataclass(frozen=True) +class EstimatorSpec: + name: str + family: str + parameters: Mapping[str, object] = field(default_factory=dict) + min_rows: int = 1 + max_rows: int | None = None + max_features: int | None = None + early_stopping_rounds: int | None = None + + def __post_init__(self) -> None: + if not self.name: + raise ValueError("Estimator name must not be empty") + if not self.family: + raise ValueError("Estimator family must not be empty") + if self.min_rows < 1: + raise ValueError("min_rows must be at least 1") + if self.max_rows is not None and self.max_rows < self.min_rows: + raise ValueError("max_rows must be greater than or equal to min_rows") + if self.max_features is not None and self.max_features < 1: + raise ValueError("max_features must be at least 1") + if self.early_stopping_rounds is not None and self.early_stopping_rounds < 1: + raise ValueError("early_stopping_rounds must be at least 1") + object.__setattr__( + self, + "parameters", + MappingProxyType(dict(self.parameters)), + ) + + def is_applicable(self, *, n_rows: int, n_features: int) -> bool: + if n_rows < self.min_rows: + return False + if self.max_rows is not None and n_rows > self.max_rows: + return False + return self.max_features is None or n_features <= self.max_features + + +@dataclass(frozen=True) +class TrainedCandidate: + spec: EstimatorSpec + model: CandidateModel + fit_time: float + + +@dataclass(frozen=True) +class CandidateRun: + candidates: tuple[TrainedCandidate, ...] + elapsed_time: float + stopped_for_budget: bool + + +def _sklearn_portfolio(task: str) -> list[EstimatorSpec]: + tree_criterion = "gini" if task == TABULAR_CLASSIFICATION_TASK else "squared_error" + linear_parameters: dict[str, object] + if task == TABULAR_CLASSIFICATION_TASK: + linear_parameters = {"C": 1.0, "max_iter": 1_000} + else: + linear_parameters = {"alpha": 1.0} + return [ + EstimatorSpec( + "hist_gradient_boosting_default", + "hist_gradient_boosting", + { + "learning_rate": 0.08, + "l2_regularization": 0.1, + "max_iter": 200, + "max_leaf_nodes": 31, + "min_samples_leaf": 20, + }, + early_stopping_rounds=20, + ), + EstimatorSpec( + "extra_trees_zeroshot", + "extra_trees", + { + "criterion": tree_criterion, + "max_features": 0.75, + "min_samples_leaf": 1, + "n_estimators": 300, + }, + ), + EstimatorSpec("linear_default", "linear", linear_parameters), + EstimatorSpec( + "random_forest_zeroshot", + "random_forest", + { + "criterion": tree_criterion, + "max_features": 0.75, + "min_samples_leaf": 1, + "n_estimators": 300, + }, + ), + EstimatorSpec( + "hist_gradient_boosting_large", + "hist_gradient_boosting", + { + "learning_rate": 0.04, + "l2_regularization": 0.1, + "max_iter": 300, + "max_leaf_nodes": 63, + "min_samples_leaf": 10, + }, + early_stopping_rounds=20, + ), + ] + + +def _gbdt_portfolio(available_families: set[str]) -> list[EstimatorSpec]: + primary: dict[str, EstimatorSpec] = { + "lightgbm": EstimatorSpec( + "lightgbm_default", + "lightgbm", + {"n_estimators": 300}, + early_stopping_rounds=20, + ), + "xgboost": EstimatorSpec( + "xgboost_default", + "xgboost", + {"n_estimators": 300}, + early_stopping_rounds=20, + ), + "catboost": EstimatorSpec( + "catboost_default", + "catboost", + {"iterations": 300}, + early_stopping_rounds=20, + ), + } + zeroshot: dict[str, EstimatorSpec] = { + "lightgbm": EstimatorSpec( + "lightgbm_zeroshot_large", + "lightgbm", + { + "colsample_bytree": 0.9, + "learning_rate": 0.03, + "min_child_samples": 3, + "n_estimators": 500, + "num_leaves": 128, + }, + early_stopping_rounds=20, + ), + "xgboost": EstimatorSpec( + "xgboost_zeroshot_r33", + "xgboost", + { + "colsample_bytree": 0.6917311125174739, + "learning_rate": 0.018063876087523967, + "max_depth": 10, + "min_child_weight": 0.6028633586934382, + "n_estimators": 500, + }, + early_stopping_rounds=20, + ), + "catboost": EstimatorSpec( + "catboost_zeroshot_r177", + "catboost", + { + "depth": 6, + "grow_policy": "SymmetricTree", + "iterations": 500, + "l2_leaf_reg": 2.1542798306067823, + "learning_rate": 0.06864209415792857, + }, + early_stopping_rounds=20, + ), + } + family_order = ("lightgbm", "xgboost", "catboost") + return [ + portfolio[family] + for portfolio in (primary, zeroshot) + for family in family_order + if family in available_families + ] + + +def _interleave_portfolios( + gbdt_specs: Sequence[EstimatorSpec], + sklearn_specs: Sequence[EstimatorSpec], +) -> tuple[EstimatorSpec, ...]: + ordered: list[EstimatorSpec] = [] + for index in range(max(len(gbdt_specs), len(sklearn_specs))): + if index < len(gbdt_specs): + ordered.append(gbdt_specs[index]) + if index < len(sklearn_specs): + ordered.append(sklearn_specs[index]) + return tuple(ordered) + + +def _portfolio_from_families( + task: str, + available_families: set[str], +) -> tuple[EstimatorSpec, ...]: + sklearn_specs = _sklearn_portfolio(task) + if not available_families: + logger.info( + "Optional GBDT libraries are unavailable; using sklearn-only candidate " + "portfolio." + ) + return tuple(sklearn_specs) + return _interleave_portfolios( + _gbdt_portfolio(available_families), + sklearn_specs, + ) + + +def _validate_task(task: str) -> None: + if task not in {TABULAR_CLASSIFICATION_TASK, TABULAR_REGRESSION_TASK}: + raise ValueError(f"Unknown task `{task}`") + + +def default_portfolio( + task: str, + *, + n_classes: int | None = None, +) -> tuple[EstimatorSpec, ...]: + _validate_task(task) + available = set(get_gbdt_model_classes(task, n_classes=n_classes)) + return _portfolio_from_families(task, available) + + +def _build_sklearn_model( + spec: EstimatorSpec, + task: str, + random_state: int, +) -> SklearnModel: + parameters = dict(spec.parameters) + parameters.pop("random_seed", None) + parameters.pop("random_state", None) + estimator: Any + if spec.family == "hist_gradient_boosting": + model_class = ( + HistGradientBoostingClassifier + if task == TABULAR_CLASSIFICATION_TASK + else HistGradientBoostingRegressor + ) + parameters["early_stopping"] = False + parameters["random_state"] = random_state + estimator = model_class(**parameters) + elif spec.family == "extra_trees": + model_class = ( + ExtraTreesClassifier + if task == TABULAR_CLASSIFICATION_TASK + else ExtraTreesRegressor + ) + parameters.setdefault("n_jobs", 1) + parameters["random_state"] = random_state + estimator = model_class(**parameters) + elif spec.family == "random_forest": + model_class = ( + RandomForestClassifier + if task == TABULAR_CLASSIFICATION_TASK + else RandomForestRegressor + ) + parameters.setdefault("n_jobs", 1) + parameters["random_state"] = random_state + estimator = model_class(**parameters) + elif spec.family == "linear" and task == TABULAR_CLASSIFICATION_TASK: + parameters["random_state"] = random_state + estimator = LogisticRegression(**parameters) + elif spec.family == "linear": + estimator = Ridge(**parameters) + else: + raise ValueError(f"Unknown sklearn estimator family `{spec.family}`") + return SklearnModel(estimator, task) + + +def _build_candidate_model( + spec: EstimatorSpec, + task: str, + random_state: int, + available_gbdt: Mapping[str, type[GBDTModel]], +) -> CandidateModel: + if spec.family in _SKLEARN_FAMILIES: + return _build_sklearn_model(spec, task, random_state) + if spec.family not in available_gbdt: + raise ImportError( + f"Estimator family `{spec.family}` is unavailable; install falcon-ml[gbdt]" + ) + parameters = dict(spec.parameters) + parameters.pop("random_seed", None) + parameters.pop("random_state", None) + model_class: Any = available_gbdt[spec.family] + return model_class(random_state=random_state, **parameters) + + +def _supports_sample_weight(model: CandidateModel) -> bool: + """Report whether the underlying estimator accepts `sample_weight` in `fit`. + + Wrappers may declare support explicitly; introspecting the wrapper itself would + always report support because the `CandidateModel` protocol requires the keyword. + A model exposing neither a declaration nor an estimator is allowed through. + """ + declared = getattr(model, "supports_sample_weight", None) + if isinstance(declared, bool): + return declared + estimator = getattr(model, "estimator", None) + if estimator is None: + return True + return bool(has_fit_parameter(estimator, "sample_weight")) + + +def _validated_training_data( + X: npt.NDArray[Any], + y: npt.NDArray[Any], + groups: npt.ArrayLike | None, +) -> tuple[npt.NDArray[Any], npt.NDArray[Any], npt.NDArray[Any] | None]: + feature_values = np.asarray(X) + target_values = np.asarray(y) + if feature_values.ndim != 2: + raise ValueError("Features must be two-dimensional") + if target_values.ndim == 2 and target_values.shape[1] == 1: + target_values = target_values[:, 0] + if target_values.ndim != 1: + raise ValueError("Targets must be one-dimensional") + if len(feature_values) != len(target_values): + raise ValueError("Features and targets must contain the same number of rows") + if len(feature_values) == 0: + raise ValueError("Training data must not be empty") + if groups is None: + return feature_values, target_values, None + group_values = np.asarray(groups) + if group_values.ndim == 2 and group_values.shape[1] == 1: + group_values = group_values[:, 0] + if group_values.ndim != 1 or len(group_values) != len(feature_values): + raise ValueError("Groups must contain one value per feature row") + return feature_values, target_values, group_values + + +class CandidateTrainer: + def __init__( + self, + task: str, + *, + time_limit: float | None = None, + reserve_fraction: float = _DEFAULT_RESERVE_FRACTION, + random_state: int = 42, + class_weight: ClassWeight = "none", + model_factory: CandidateModelFactory | None = None, + clock: Callable[[], float] | None = None, + ) -> None: + _validate_task(task) + if time_limit is not None and time_limit <= 0: + raise ValueError("time_limit must be greater than zero") + if not 0 <= reserve_fraction < 1: + raise ValueError("reserve_fraction must be in the range [0, 1)") + if class_weight not in {"none", "balanced"}: + raise ValueError("class_weight must be either 'none' or 'balanced'") + self.task = task + self.time_limit = time_limit + self.reserve_fraction = reserve_fraction + self.random_state = random_state + self.class_weight = class_weight + self.model_factory = model_factory + self.clock = monotonic if clock is None else clock + + def _candidate_budget(self) -> float | None: + if self.time_limit is None: + return None + return self.time_limit * (1 - self.reserve_fraction) + + def _budget_prevents_next_fit( + self, + elapsed: float, + observed_fit_times: Sequence[float], + ) -> bool: + budget = self._candidate_budget() + if budget is None: + return False + remaining = budget - elapsed + if remaining <= 0: + return True + if not observed_fit_times: + return False + return float(np.mean(observed_fit_times)) > remaining + + def _fit_inputs( + self, + spec: EstimatorSpec, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + groups: npt.NDArray[Any] | None, + *, + random_state: int | None = None, + ) -> tuple[ + npt.NDArray[Any], + npt.NDArray[Any], + npt.NDArray[np.float64] | None, + tuple[npt.NDArray[Any], npt.NDArray[Any]] | None, + int | None, + ]: + train_X = X + train_y = y + validation_data = None + early_stopping_rounds = None + if spec.early_stopping_rounds is not None: + split_random_state = ( + self.random_state if random_state is None else random_state + ) + try: + train_indices, validation_indices = holdout_indices( + X, + y, + self.task, + groups, + test_size=_EARLY_STOPPING_TEST_SIZE, + random_state=split_random_state, + ) + except ValueError as error: + logger.info( + "Candidate %s will train without early stopping: %s", + spec.name, + error, + ) + else: + train_X = X[train_indices] + train_y = y[train_indices] + validation_data = (X[validation_indices], y[validation_indices]) + early_stopping_rounds = spec.early_stopping_rounds + sample_weight = None + if self.task == TABULAR_CLASSIFICATION_TASK and self.class_weight == "balanced": + sample_weight = np.asarray( + compute_sample_weight(class_weight="balanced", y=train_y), + dtype=np.float64, + ) + return ( + train_X, + train_y, + sample_weight, + validation_data, + early_stopping_rounds, + ) + + def _available_gbdt( + self, + specs: Sequence[EstimatorSpec] | None, + n_classes: int | None, + ) -> dict[str, type[GBDTModel]]: + if self.model_factory is not None: + return {} + if specs is not None and all( + spec.family in _SKLEARN_FAMILIES for spec in specs + ): + return {} + return get_gbdt_model_classes(self.task, n_classes=n_classes) + + def _model( + self, + spec: EstimatorSpec, + n_classes: int | None, + available_gbdt: Mapping[str, type[GBDTModel]], + *, + random_state: int | None = None, + ) -> CandidateModel: + model_random_state = self.random_state if random_state is None else random_state + if self.model_factory is not None: + return self.model_factory( + spec, + self.task, + model_random_state, + n_classes, + ) + return _build_candidate_model( + spec, + self.task, + model_random_state, + available_gbdt, + ) + + def _assert_sample_weight_support( + self, + specs: Sequence[EstimatorSpec], + n_classes: int | None, + available_gbdt: Mapping[str, type[GBDTModel]], + ) -> None: + if self.class_weight != "balanced" or self.task != TABULAR_CLASSIFICATION_TASK: + return + unsupported = sorted( + { + spec.family + for spec in specs + if not _supports_sample_weight( + self._model(spec, n_classes, available_gbdt) + ) + } + ) + if unsupported: + raise ValueError( + "class_weight='balanced' requires estimators accepting sample_weight; " + f"these families do not: {', '.join(unsupported)}" + ) + + def fit( + self, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + *, + specs: Sequence[EstimatorSpec] | None = None, + groups: npt.ArrayLike | None = None, + ) -> CandidateRun: + feature_values, target_values, group_values = _validated_training_data( + X, y, groups + ) + n_classes = None + if self.task == TABULAR_CLASSIFICATION_TASK: + n_classes = int(np.unique(target_values).size) + if n_classes < 2: + raise ValueError("Classification requires at least two target classes") + available_gbdt = self._available_gbdt(specs, n_classes) + selected_specs = ( + _portfolio_from_families(self.task, set(available_gbdt)) + if specs is None + else tuple(specs) + ) + applicable_specs = tuple( + spec + for spec in selected_specs + if spec.is_applicable( + n_rows=len(feature_values), + n_features=feature_values.shape[1], + ) + ) + if not applicable_specs: + raise ValueError("No candidate specifications apply to this dataset") + self._assert_sample_weight_support(applicable_specs, n_classes, available_gbdt) + + started_at = self.clock() + observed_fit_times: list[float] = [] + trained: list[TrainedCandidate] = [] + stopped_for_budget = False + last_error: Exception | None = None + total_candidates = len(applicable_specs) + for index, spec in enumerate(applicable_specs, start=1): + elapsed = self.clock() - started_at + if trained and self._budget_prevents_next_fit(elapsed, observed_fit_times): + stopped_for_budget = True + logger.info( + "Candidate training stopped to preserve the time reserved for " + "ensembling and export." + ) + break + model = self._model(spec, n_classes, available_gbdt) + ( + train_X, + train_y, + sample_weight, + validation_data, + early_stopping_rounds, + ) = self._fit_inputs( + spec, + feature_values, + target_values, + group_values, + ) + fit_started_at = self.clock() + try: + model.fit( + train_X, + train_y, + sample_weight=sample_weight, + validation_data=validation_data, + early_stopping_rounds=early_stopping_rounds, + ) + except Exception as error: + last_error = error + logger.warning( + "Candidate %d/%d (%s) failed: %s", + index, + total_candidates, + spec.name, + error, + ) + else: + fit_time = self.clock() - fit_started_at + trained.append(TrainedCandidate(spec, model, fit_time)) + fit_time = self.clock() - fit_started_at + observed_fit_times.append(fit_time) + estimated_remaining = float(np.mean(observed_fit_times)) * ( + total_candidates - index + ) + logger.info( + "Candidate %d/%d (%s) completed in %.2fs; estimated remaining time " + "%.2fs.", + index, + total_candidates, + spec.name, + fit_time, + estimated_remaining, + ) + candidate_budget = self._candidate_budget() + if ( + index == 1 + and candidate_budget is not None + and fit_time > candidate_budget + ): + logger.warning( + "The time limit is insufficient for the first candidate: it took " + "%.2fs with %.2fs available for candidate training. Continuing with " + "the fitted candidate.", + fit_time, + candidate_budget, + ) + + if not trained: + raise RuntimeError("No candidate model could be trained") from last_error + return CandidateRun( + tuple(trained), + self.clock() - started_at, + stopped_for_budget, + ) + + +def score_oof_predictions( + predictions: npt.NDArray[Any], + y: npt.NDArray[Any], + task: str, + *, + prior_correct: bool = True, +) -> float: + """Return a higher-is-better OOF score; regression uses negative RMSE. + + With `prior_correct`, classification decisions are taken at `argmax p_c / pi_c` + rather than plain argmax. That is the asymptotic Bayes rule for balanced accuracy, + and it approximates the tuned decision rule the exported model carries. + """ + _validate_task(task) + target_values = np.asarray(y).reshape(-1) + prediction_values = np.asarray(predictions) + if not np.isfinite(prediction_values).all(): + raise ValueError("OOF predictions must contain only finite values") + if task == TABULAR_CLASSIFICATION_TASK: + classes, counts = np.unique(target_values, return_counts=True) + if classes.size < 2: + raise ValueError("Classification requires at least two target classes") + if prediction_values.ndim != 2: + raise ValueError("Classification OOF predictions must be two-dimensional") + if prediction_values.shape != (len(target_values), len(classes)): + raise ValueError( + "Classification OOF predictions must contain one column per class" + ) + scores = prediction_values + if prior_correct: + scores = prediction_values / (counts / len(target_values)) + predicted_labels = classes[np.argmax(scores, axis=1)] + return float(balanced_accuracy_score(target_values, predicted_labels)) + + prediction_values = prediction_values.reshape(-1) + if prediction_values.shape != target_values.shape: + raise ValueError("Regression OOF predictions must match the target shape") + residuals = prediction_values.astype(np.float64) - target_values.astype(np.float64) + return -float(np.sqrt(np.mean(np.square(residuals)))) + + +@dataclass(frozen=True) +class GreedySelection: + weights: tuple[float, ...] + score: float + iterations: int + + +def greedy_weighted_selection( + predictions: Sequence[npt.NDArray[Any]], + y: npt.NDArray[Any], + task: str, + *, + max_iterations: int = 100, + prior_correct: bool = True, +) -> GreedySelection: + if max_iterations < 1: + raise ValueError("max_iterations must be at least 1") + if not predictions: + raise ValueError("At least one candidate prediction is required") + candidate_predictions = tuple( + np.asarray(candidate, dtype=np.float64) for candidate in predictions + ) + expected_shape = candidate_predictions[0].shape + if any(candidate.shape != expected_shape for candidate in candidate_predictions): + raise ValueError("All candidate OOF predictions must have the same shape") + + individual_scores = np.asarray( + [ + score_oof_predictions(candidate, y, task, prior_correct=prior_correct) + for candidate in candidate_predictions + ] + ) + first_index = int(np.argmax(individual_scores)) + counts = np.zeros(len(candidate_predictions), dtype=np.int64) + counts[first_index] = 1 + prediction_sum = candidate_predictions[first_index].copy() + current_score = float(individual_scores[first_index]) + iterations = 1 + + for iteration in range(2, max_iterations + 1): + trial_scores = np.asarray( + [ + score_oof_predictions( + (prediction_sum + candidate) / iteration, + y, + task, + prior_correct=prior_correct, + ) + for candidate in candidate_predictions + ] + ) + best_index = int(np.argmax(trial_scores)) + best_score = float(trial_scores[best_index]) + improvement_floor = np.finfo(np.float64).eps * max(1.0, abs(current_score)) + if best_score <= current_score + improvement_floor: + break + counts[best_index] += 1 + prediction_sum += candidate_predictions[best_index] + current_score = best_score + iterations = iteration + + weights = tuple(float(count / iterations) for count in counts) + return GreedySelection(weights, current_score, iterations) + + +@dataclass(frozen=True) +class OOFCandidate: + spec: EstimatorSpec + models: tuple[CandidateModel, ...] + oof_predictions: npt.NDArray[np.float32] + oof_score: float + fit_time: float + + +@dataclass(frozen=True) +class EnsembleMember: + spec: EstimatorSpec + models: tuple[CandidateModel, ...] + weight: float + + +class GreedyWeightedEnsemble: + def __init__( + self, + task: str, + candidates: Sequence[OOFCandidate], + selection: GreedySelection, + *, + classes: Sequence[int] | None = None, + ) -> None: + _validate_task(task) + if len(candidates) != len(selection.weights): + raise ValueError("The selection must contain one weight per candidate") + if not candidates: + raise ValueError("An ensemble requires at least one candidate") + if any(weight < 0 for weight in selection.weights): + raise ValueError("Ensemble weights must be non-negative") + total_weight = sum(selection.weights) + if total_weight <= 0: + raise ValueError("At least one ensemble weight must be positive") + if task == TABULAR_CLASSIFICATION_TASK: + if classes is None or len(classes) < 2: + raise ValueError( + "Classification ensembles require at least two classes" + ) + class_values = np.asarray(classes) + if not np.issubdtype(class_values.dtype, np.integer): + raise ValueError("Classification targets must be integer encoded") + self.classes: npt.NDArray[np.int64] | None = class_values.astype(np.int64) + else: + self.classes = None + + self.task = task + self.score = selection.score + self.iterations = selection.iterations + self.weights = tuple(weight / total_weight for weight in selection.weights) + self.members = tuple( + EnsembleMember(candidate.spec, candidate.models, weight) + for candidate, weight in zip(candidates, self.weights, strict=True) + if weight > 0 + ) + + def _member_prediction( + self, + member: EnsembleMember, + X: npt.NDArray[Any], + ) -> npt.NDArray[np.float32]: + if self.task == TABULAR_CLASSIFICATION_TASK: + fold_predictions = [ + np.asarray( + model.predict_proba(X), + dtype=np.float32, + ) + for model in ( + model for model in member.models if hasattr(model, "predict_proba") + ) + ] + if len(fold_predictions) != len(member.models): + raise RuntimeError( + "A classification fold model does not expose probabilities" + ) + else: + fold_predictions = [ + np.asarray(model.predict(X), dtype=np.float32).reshape(-1) + for model in member.models + ] + return np.mean( + np.stack(fold_predictions, axis=0), + axis=0, + dtype=np.float32, + ) + + def predict_proba(self, X: npt.NDArray[Any]) -> npt.NDArray[np.float32]: + if self.task != TABULAR_CLASSIFICATION_TASK: + raise RuntimeError("Regression ensembles do not expose probabilities") + weighted = [ + self._member_prediction(member, X) * np.float32(member.weight) + for member in self.members + ] + return np.sum(np.stack(weighted, axis=0), axis=0, dtype=np.float32) + + def predict(self, X: npt.NDArray[Any]) -> npt.NDArray[Any]: + if self.task == TABULAR_CLASSIFICATION_TASK: + if self.classes is None: + raise RuntimeError("Classification ensemble classes are unavailable") + return self.classes[np.argmax(self.predict_proba(X), axis=1)] + weighted = [ + self._member_prediction(member, X) * np.float32(member.weight) + for member in self.members + ] + return np.asarray(np.sum(np.stack(weighted, axis=0), axis=0, dtype=np.float32)) + + def serialize(self) -> SerializedModelRepr: + return serialize_parallel_ensemble( + [[model.serialize() for model in member.models] for member in self.members], + [member.weight for member in self.members], + self.task, + classes=None if self.classes is None else self.classes.tolist(), + ) + + def get_input_type(self) -> object: + return Float32Array + + def get_output_type(self) -> object: + return Int64Array if self.task == TABULAR_CLASSIFICATION_TASK else Float32Array + + +@dataclass(frozen=True) +class EnsembleRun: + candidates: tuple[OOFCandidate, ...] + ensemble: GreedyWeightedEnsemble + evaluation_indices: npt.NDArray[np.int64] + ensemble_score_history: tuple[float, ...] + elapsed_time: float + stopped_for_budget: bool + stopped_for_plateau: bool + + +class OOFEnsembleTrainer: + def __init__( + self, + task: str, + *, + max_iterations: int = 100, + plateau_enabled: bool = True, + plateau_patience: int = 3, + plateau_tolerance: float = 1e-4, + n_splits: int = 5, + time_limit: float | None = None, + reserve_fraction: float = _DEFAULT_RESERVE_FRACTION, + random_state: int = 42, + class_weight: ClassWeight = "none", + prior_correct: bool = True, + model_factory: CandidateModelFactory | None = None, + clock: Callable[[], float] | None = None, + ) -> None: + self._candidate_trainer = CandidateTrainer( + task, + time_limit=time_limit, + reserve_fraction=reserve_fraction, + random_state=random_state, + class_weight=class_weight, + model_factory=model_factory, + clock=clock, + ) + if max_iterations < 1: + raise ValueError("max_iterations must be at least 1") + if plateau_patience < 1: + raise ValueError("plateau_patience must be at least 1") + if plateau_tolerance < 0: + raise ValueError("plateau_tolerance must be non-negative") + if n_splits < 2: + raise ValueError("n_splits must be at least 2") + self.max_iterations = max_iterations + self.task = task + self.random_state = random_state + self.prior_correct = prior_correct + self.clock = self._candidate_trainer.clock + self.plateau_enabled = plateau_enabled + self.plateau_patience = plateau_patience + self.plateau_tolerance = plateau_tolerance + self.n_splits = n_splits + + def _fit_oof_candidate( + self, + spec: EstimatorSpec, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + groups: npt.NDArray[Any] | None, + splits: Sequence[tuple[npt.NDArray[np.int64], npt.NDArray[np.int64]]], + evaluation_indices: npt.NDArray[np.int64], + n_classes: int | None, + available_gbdt: Mapping[str, type[GBDTModel]], + *, + warn_for_budget_shortfall: bool = False, + ) -> OOFCandidate: + output_shape: tuple[int, ...] + if self.task == TABULAR_CLASSIFICATION_TASK: + if n_classes is None: + raise RuntimeError("Classification class count is unavailable") + output_shape = (len(X), n_classes) + else: + output_shape = (len(X),) + all_predictions = np.full(output_shape, np.nan, dtype=np.float32) + models: list[CandidateModel] = [] + started_at = self.clock() + budget_shortfall_warned = False + for fold_index, (train_indices, eval_indices) in enumerate(splits): + fold_seed = self.random_state + fold_index + model = self._candidate_trainer._model( + spec, + n_classes, + available_gbdt, + random_state=fold_seed, + ) + fold_groups = None if groups is None else groups[train_indices] + ( + train_X, + train_y, + sample_weight, + validation_data, + early_stopping_rounds, + ) = self._candidate_trainer._fit_inputs( + spec, + X[train_indices], + y[train_indices], + fold_groups, + random_state=fold_seed, + ) + model.fit( + train_X, + train_y, + sample_weight=sample_weight, + validation_data=validation_data, + early_stopping_rounds=early_stopping_rounds, + ) + expected_shape: tuple[int, ...] + if self.task == TABULAR_CLASSIFICATION_TASK: + if n_classes is None: + raise RuntimeError("Classification class count is unavailable") + predict_proba = getattr(model, "predict_proba", None) + if not callable(predict_proba): + raise TypeError( + "Classification candidate models must expose predict_proba" + ) + fold_predictions = np.asarray( + predict_proba(X[eval_indices]), + dtype=np.float32, + ) + expected_shape = (len(eval_indices), n_classes) + else: + fold_predictions = np.asarray( + model.predict(X[eval_indices]), + dtype=np.float32, + ).reshape(-1) + expected_shape = (len(eval_indices),) + if fold_predictions.shape != expected_shape: + raise ValueError( + f"Candidate {spec.name} produced OOF predictions with shape " + f"{fold_predictions.shape}; expected {expected_shape}" + ) + all_predictions[eval_indices] = fold_predictions + models.append(model) + + candidate_budget = self._candidate_trainer._candidate_budget() + completed_folds = fold_index + 1 + elapsed = self.clock() - started_at + estimated_fit_time = elapsed / completed_folds * len(splits) + if ( + warn_for_budget_shortfall + and not budget_shortfall_warned + and candidate_budget is not None + and estimated_fit_time > candidate_budget + ): + logger.warning( + "The time limit is insufficient for the first candidate: " + "%d/%d OOF folds took %.2fs, estimating %.2fs with %.2fs " + "available for candidate training. Continuing with the first " + "candidate.", + completed_folds, + len(splits), + elapsed, + estimated_fit_time, + candidate_budget, + ) + budget_shortfall_warned = True + + oof_predictions = all_predictions[evaluation_indices] + score = score_oof_predictions( + oof_predictions, + y[evaluation_indices], + self.task, + prior_correct=self.prior_correct, + ) + return OOFCandidate( + spec, + tuple(models), + oof_predictions, + score, + self.clock() - started_at, + ) + + def fit( + self, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + *, + specs: Sequence[EstimatorSpec] | None = None, + groups: npt.ArrayLike | None = None, + splits: Sequence[tuple[npt.NDArray[np.int64], npt.NDArray[np.int64]]] + | None = None, + ) -> EnsembleRun: + feature_values, target_values, group_values = _validated_training_data( + X, y, groups + ) + classes: npt.NDArray[np.int64] | None = None + n_classes = None + if self.task == TABULAR_CLASSIFICATION_TASK: + if not np.issubdtype(target_values.dtype, np.integer): + raise ValueError("Classification targets must be integer encoded") + classes = np.unique(target_values).astype(np.int64) + n_classes = len(classes) + if n_classes < 2: + raise ValueError("Classification requires at least two target classes") + available_gbdt = self._candidate_trainer._available_gbdt(specs, n_classes) + selected_specs = ( + _portfolio_from_families(self.task, set(available_gbdt)) + if specs is None + else tuple(specs) + ) + applicable_specs = tuple( + spec + for spec in selected_specs + if spec.is_applicable( + n_rows=len(feature_values), + n_features=feature_values.shape[1], + ) + ) + if not applicable_specs: + raise ValueError("No candidate specifications apply to this dataset") + self._candidate_trainer._assert_sample_weight_support( + applicable_specs, + n_classes, + available_gbdt, + ) + + if splits is None: + resolved_splits = out_of_fold_indices( + feature_values, + target_values, + self.task, + group_values, + n_splits=self.n_splits, + random_state=self.random_state, + ) + else: + resolved_splits = list(splits) + if not resolved_splits: + raise ValueError("At least one OOF split is required") + evaluation_indices = np.sort( + np.concatenate([eval_indices for _, eval_indices in resolved_splits]) + ).astype(np.int64, copy=False) + if np.unique(evaluation_indices).size != evaluation_indices.size: + raise RuntimeError("OOF evaluation folds contain duplicate rows") + if len(resolved_splits) > 1 and evaluation_indices.size != len(feature_values): + raise RuntimeError("Cross-validation OOF folds do not cover every row") + + started_at = self.clock() + observed_fit_times: list[float] = [] + candidates: list[OOFCandidate] = [] + score_history: list[float] = [] + current_selection: GreedySelection | None = None + best_ensemble_score: float | None = None + without_improvement = 0 + stopped_for_budget = False + stopped_for_plateau = False + last_error: Exception | None = None + total_candidates = len(applicable_specs) + + for index, spec in enumerate(applicable_specs, start=1): + elapsed = self.clock() - started_at + if candidates and self._candidate_trainer._budget_prevents_next_fit( + elapsed, observed_fit_times + ): + stopped_for_budget = True + logger.info( + "Candidate training stopped to preserve the time reserved for " + "ensembling and export." + ) + break + + fit_started_at = self.clock() + try: + candidate = self._fit_oof_candidate( + spec, + feature_values, + target_values, + group_values, + resolved_splits, + evaluation_indices, + n_classes, + available_gbdt, + warn_for_budget_shortfall=index == 1, + ) + except Exception as error: + last_error = error + logger.warning( + "Candidate %d/%d (%s) failed: %s", + index, + total_candidates, + spec.name, + error, + ) + else: + candidates.append(candidate) + current_selection = greedy_weighted_selection( + [trained.oof_predictions for trained in candidates], + target_values[evaluation_indices], + self.task, + max_iterations=self.max_iterations, + prior_correct=self.prior_correct, + ) + score_history.append(current_selection.score) + if ( + best_ensemble_score is None + or current_selection.score + > best_ensemble_score + self.plateau_tolerance + ): + best_ensemble_score = current_selection.score + without_improvement = 0 + else: + without_improvement += 1 + + fit_time = self.clock() - fit_started_at + observed_fit_times.append(fit_time) + estimated_remaining = float(np.mean(observed_fit_times)) * ( + total_candidates - index + ) + logger.info( + "Candidate %d/%d (%s) completed in %.2fs; estimated remaining time " + "%.2fs.", + index, + total_candidates, + spec.name, + fit_time, + estimated_remaining, + ) + if ( + self.plateau_enabled + and candidates + and without_improvement >= self.plateau_patience + ): + stopped_for_plateau = True + logger.info( + "Candidate training stopped after an OOF score plateau: no " + "improvement greater than %.6g for %d candidates.", + self.plateau_tolerance, + self.plateau_patience, + ) + break + + if not candidates or current_selection is None: + raise RuntimeError("No candidate model could be trained") from last_error + ensemble = GreedyWeightedEnsemble( + self.task, + candidates, + current_selection, + classes=None if classes is None else classes.tolist(), + ) + return EnsembleRun( + tuple(candidates), + ensemble, + evaluation_indices, + tuple(score_history), + self.clock() - started_at, + stopped_for_budget, + stopped_for_plateau, + ) + + +__all__ = [ + "CandidateClassifierModel", + "CandidateModel", + "CandidateRun", + "CandidateTrainer", + "EnsembleMember", + "EnsembleRun", + "EstimatorSpec", + "GreedySelection", + "GreedyWeightedEnsemble", + "OOFCandidate", + "OOFEnsembleTrainer", + "TrainedCandidate", + "default_portfolio", + "greedy_weighted_selection", + "score_oof_predictions", +] diff --git a/falcon/tabular/configurations.py b/falcon/tabular/configurations.py deleted file mode 100644 index c8caca9..0000000 --- a/falcon/tabular/configurations.py +++ /dev/null @@ -1,149 +0,0 @@ -from falcon.tabular.pipelines import SimpleTabularPipeline -from falcon.tabular.learners import SuperLearner, OptunaLearner, PlainLearner -from falcon.tabular.learners.super_learner import _default_estimators -from falcon.tabular.models.hist_gbt import ( - HistGradientBoostingClassifier, - HistGradientBoostingRegressor, -) -from typing import Dict - -_SUPER_LEARNER_DEFAULT_CONFIG = { - "pipeline": SimpleTabularPipeline, - "extra_pipeline_options": {"learner": SuperLearner, "learner_kwargs": {}}, -} - -_OPTUNA_LEARNER_DEFAULT_CONFIG = { - "pipeline": SimpleTabularPipeline, - "extra_pipeline_options": {"learner": OptunaLearner, "learner_kwargs": {}}, -} - -_PLAIN_LEARNER_DEFAULT_CONFIG = { - "pipeline": SimpleTabularPipeline, - "extra_pipeline_options": {"learner": PlainLearner, "learner_kwargs": {}}, -} - -TABULAR_CLASSIFICATION_CONFIGURATIONS: Dict[str, Dict] = { - "SuperLearner.mini": { - "pipeline": SimpleTabularPipeline, - "extra_pipeline_options": { - "learner": SuperLearner, - "learner_kwargs": { - "cv": 10, - "base_estimators": _default_estimators["tabular_classification"][ - "mini" - ], - }, - }, - }, - "SuperLearner.mid": { - "pipeline": SimpleTabularPipeline, - "extra_pipeline_options": { - "learner": SuperLearner, - "learner_kwargs": { - "cv": 5, - "base_estimators": _default_estimators["tabular_classification"]["mid"], - }, - }, - }, - "SuperLearner.large": { - "pipeline": SimpleTabularPipeline, - "extra_pipeline_options": { - "learner": SuperLearner, - "learner_kwargs": { - "cv": 3, - "base_estimators": _default_estimators["tabular_classification"][ - "large" - ], - }, - }, - }, - "SuperLearner.xlarge": { - "pipeline": SimpleTabularPipeline, - "extra_pipeline_options": { - "learner": SuperLearner, - "learner_kwargs": { - "cv": 3, - "base_estimators": _default_estimators["tabular_classification"][ - "x-large" - ], - }, - }, - }, - "OptunaLearner.hgbt": { - "pipeline": SimpleTabularPipeline, - "extra_pipeline_options": { - "learner": OptunaLearner, - "learner_kwargs": {"model_class": HistGradientBoostingClassifier}, - }, - }, - "PlainLearner.hgbt": { - "pipeline": SimpleTabularPipeline, - "extra_pipeline_options": { - "learner": PlainLearner, - "learner_kwargs": {"model_class": HistGradientBoostingClassifier}, - }, - }, - "SuperLearner": _SUPER_LEARNER_DEFAULT_CONFIG, - "OptunaLearner": _OPTUNA_LEARNER_DEFAULT_CONFIG, - "PlainLearner": _PLAIN_LEARNER_DEFAULT_CONFIG, -} - -TABULAR_REGRESSION_CONFIGURATIONS: Dict[str, Dict] = { - "SuperLearner.mini": { - "pipeline": SimpleTabularPipeline, - "extra_pipeline_options": { - "learner": SuperLearner, - "learner_kwargs": { - "cv": 10, - "base_estimators": _default_estimators["tabular_regression"]["mini"], - }, - }, - }, - "SuperLearner.mid": { - "pipeline": SimpleTabularPipeline, - "extra_pipeline_options": { - "learner": SuperLearner, - "learner_kwargs": { - "cv": 5, - "base_estimators": _default_estimators["tabular_regression"]["mid"], - }, - }, - }, - "SuperLearner.large": { - "pipeline": SimpleTabularPipeline, - "extra_pipeline_options": { - "learner": SuperLearner, - "learner_kwargs": { - "cv": 3, - "base_estimators": _default_estimators["tabular_regression"]["large"], - }, - }, - }, - "SuperLearner.xlarge": { - "pipeline": SimpleTabularPipeline, - "extra_pipeline_options": { - "learner": SuperLearner, - "learner_kwargs": { - "cv": 3, - "base_estimators": _default_estimators["tabular_regression"]["x-large"], - }, - }, - }, - "OptunaLearner.hgbt": { - "pipeline": SimpleTabularPipeline, - "extra_pipeline_options": { - "learner": OptunaLearner, - "learner_kwargs": {"model_class": HistGradientBoostingRegressor}, - }, - }, - "PlainLearner.hgbt": { - "pipeline": SimpleTabularPipeline, - "extra_pipeline_options": { - "learner": PlainLearner, - "learner_kwargs": {"model_class": HistGradientBoostingRegressor}, - }, - }, - "SuperLearner": _SUPER_LEARNER_DEFAULT_CONFIG, - "OptunaLearner": _OPTUNA_LEARNER_DEFAULT_CONFIG, - "PlainLearner": _PLAIN_LEARNER_DEFAULT_CONFIG, -} diff --git a/falcon/tabular/conformal.py b/falcon/tabular/conformal.py new file mode 100644 index 0000000..52bf303 --- /dev/null +++ b/falcon/tabular/conformal.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +import math +from copy import deepcopy +from typing import Any + +import numpy as np +import onnx +from numpy import typing as npt +from onnx import helper + +from falcon.config import ONNX_OPSET_VERSION +from falcon.serialization import SerializedModelRepr + + +def _validated_vector( + values: npt.NDArray[Any], + name: str, +) -> npt.NDArray[np.float64]: + vector = np.asarray(values, dtype=np.float64) + if vector.ndim == 2 and vector.shape[1] == 1: + vector = vector[:, 0] + if vector.ndim != 1 or vector.size == 0: + raise ValueError(f"Conformal {name} must be a non-empty vector") + if not np.isfinite(vector).all(): + raise ValueError(f"Conformal {name} must contain only finite values") + return vector + + +def fit_conformal_quantile( + predictions: npt.NDArray[Any], + targets: npt.NDArray[Any], + alpha: float, +) -> float: + if isinstance(alpha, bool) or not np.isfinite(alpha) or not 0 < alpha < 1: + raise ValueError("Conformal alpha must be between zero and one") + predicted = _validated_vector(predictions, "predictions") + actual = _validated_vector(targets, "targets") + if predicted.shape != actual.shape: + raise ValueError("Conformal predictions and targets must have the same shape") + + residuals = np.abs(actual - predicted) + rank = min(math.ceil((len(residuals) + 1) * (1 - alpha)), len(residuals)) + quantile = float(np.partition(residuals, rank - 1)[rank - 1]) + if quantile > np.finfo(np.float32).max: + raise ValueError("The conformal quantile cannot be represented as float32") + graph_quantile = np.float32(quantile) + if graph_quantile < quantile: + graph_quantile = np.nextafter(graph_quantile, np.float32(np.inf)) + return float(graph_quantile) + + +def serialize_conformal_interval( + serialized: SerializedModelRepr, + quantile: float, +) -> SerializedModelRepr: + if not np.isfinite(quantile) or quantile < 0: + raise ValueError("The conformal quantile must be finite and non-negative") + + model = deepcopy(serialized.get_model()) + if len(model.graph.output) != 1: + raise ValueError("A conformal regression graph must expose one prediction") + prediction_output = model.graph.output[0] + if ( + not prediction_output.type.HasField("tensor_type") + or prediction_output.type.tensor_type.elem_type != onnx.TensorProto.FLOAT + ): + raise ValueError("Regression predictions must be a float tensor") + + source_name = prediction_output.name + quantile_name = "falcon_conformal_quantile" + lower_name = "falcon_conformal_lower" + upper_name = "falcon_conformal_upper" + model.graph.initializer.append( + helper.make_tensor( + quantile_name, + onnx.TensorProto.FLOAT, + [], + [quantile], + ) + ) + model.graph.node.extend( + [ + helper.make_node( + "Sub", + [source_name, quantile_name], + [lower_name], + name="falcon_conformal/subtract_quantile", + ), + helper.make_node( + "Add", + [source_name, quantile_name], + [upper_name], + name="falcon_conformal/add_quantile", + ), + ] + ) + lower_output = deepcopy(prediction_output) + lower_output.name = lower_name + upper_output = deepcopy(prediction_output) + upper_output.name = upper_name + model.graph.output.extend([lower_output, upper_output]) + if not any(opset.domain in {"", "ai.onnx"} for opset in model.opset_import): + model.opset_import.append(helper.make_opsetid("", ONNX_OPSET_VERSION)) + + return SerializedModelRepr( + model, + serialized.get_n_inputs(), + serialized.get_n_outputs() + 2, + serialized.get_initial_types().copy(), + [shape.copy() for shape in serialized.get_initial_shapes()], + serialized.get_type(), + ) + + +__all__ = ["fit_conformal_quantile", "serialize_conformal_interval"] diff --git a/falcon/tabular/decision.py b/falcon/tabular/decision.py new file mode 100644 index 0000000..2dcd9d3 --- /dev/null +++ b/falcon/tabular/decision.py @@ -0,0 +1,249 @@ +from __future__ import annotations + +from copy import deepcopy +from typing import Any + +import numpy as np +import onnx +from numpy import typing as npt +from onnx import helper +from sklearn.metrics import balanced_accuracy_score, f1_score, matthews_corrcoef + +from falcon.config import DECISION_METRICS, ONNX_OPSET_VERSION +from falcon.serialization import SerializedModelRepr + +_MIN_CLASS_COUNT = 50 +_THRESHOLD_GRID_SIZE = 101 +_LOG_WEIGHT_GRID = np.linspace(-2.0, 2.0, 33) +_COORDINATE_SWEEPS = 3 + + +def _validated_probabilities( + probabilities: npt.NDArray[Any], +) -> npt.NDArray[np.float32]: + values = np.asarray(probabilities, dtype=np.float32) + if values.ndim != 2 or values.shape[0] == 0 or values.shape[1] < 2: + raise ValueError( + "Decision probabilities must contain at least two class columns" + ) + if not np.isfinite(values).all() or (values < 0).any(): + raise ValueError("Decision probabilities must be finite and non-negative") + if (values.sum(axis=1) <= 0).any(): + raise ValueError("Each probability row must contain a positive value") + return values + + +def _validated_targets( + targets: npt.NDArray[Any], + n_rows: int, + n_classes: int, +) -> npt.NDArray[np.int64]: + values = np.asarray(targets) + if values.ndim == 2 and values.shape[1] == 1: + values = values[:, 0] + if values.ndim != 1 or len(values) != n_rows: + raise ValueError("Decision targets must contain one value per probability row") + if not np.issubdtype(values.dtype, np.integer): + raise ValueError("Decision targets must be integer encoded") + encoded = values.astype(np.int64, copy=False) + if (encoded < 0).any() or (encoded >= n_classes).any(): + raise ValueError("Decision targets contain an unknown class index") + return encoded + + +def _metric_score( + metric: str, + targets: npt.NDArray[np.int64], + predicted: npt.NDArray[np.int64], +) -> float: + if metric == "balanced_accuracy": + return float(balanced_accuracy_score(targets, predicted)) + if metric == "mcc": + return float(matthews_corrcoef(targets, predicted)) + # Macro on binary too: which label encodes to 1 is an artefact of alphabetical label + # encoding, so a positive-class F1 would optimise an arbitrary class. + return float(f1_score(targets, predicted, average="macro", zero_division=0.0)) + + +def _decision_labels( + probabilities: npt.NDArray[np.float32], + weights: npt.NDArray[np.float32], +) -> npt.NDArray[np.int64]: + return np.argmax(probabilities * weights, axis=1).astype(np.int64) + + +def _tune_binary( + probabilities: npt.NDArray[np.float32], + targets: npt.NDArray[np.int64], + metric: str, +) -> tuple[npt.NDArray[np.float32], float]: + # Thresholds are drawn from quantiles of the positive score: at low prevalence a + # uniform grid over [0, 1] spends nearly every point where no row ever lands. + quantiles = np.quantile( + probabilities[:, 1], np.linspace(0.0, 1.0, _THRESHOLD_GRID_SIZE) + ) + best_weights = np.asarray([1.0, 1.0], dtype=np.float32) + best_score = -np.inf + for threshold in np.unique(quantiles): + weights = np.asarray([threshold, 1.0 - threshold], dtype=np.float32) + score = _metric_score(metric, targets, _decision_labels(probabilities, weights)) + if score > best_score: + best_weights, best_score = weights, score + return best_weights, best_score + + +def _tune_multiclass( + probabilities: npt.NDArray[np.float32], + targets: npt.NDArray[np.int64], + metric: str, + n_classes: int, +) -> tuple[npt.NDArray[np.float32], float]: + log_weights = np.zeros(n_classes, dtype=np.float64) + best_weights: npt.NDArray[np.float32] = np.ones(n_classes, dtype=np.float32) + best_score = _metric_score( + metric, + targets, + _decision_labels(probabilities, best_weights), + ) + for _ in range(_COORDINATE_SWEEPS): + improved = False + for index in range(1, n_classes): + selected = log_weights[index] + for value in _LOG_WEIGHT_GRID: + log_weights[index] = value + weights = np.exp(log_weights).astype(np.float32) + score = _metric_score( + metric, + targets, + _decision_labels(probabilities, weights), + ) + if score > best_score: + best_weights, best_score, selected = weights, score, value + improved = True + log_weights[index] = selected + if not improved: + break + # The rule is scale-invariant; rescaling to a mean of one keeps reported weights + # readable without changing any decision. + normalized = (best_weights.astype(np.float64) / best_weights.mean()).astype( + np.float32 + ) + return normalized, best_score + + +def fit_decision_weights( + probabilities: npt.NDArray[Any], + targets: npt.NDArray[Any], + metric: str, +) -> tuple[float, ...]: + """Fit per-class weights `w` so that `argmax(p * w)` maximises `metric`. + + Returns an all-ones no-op when the rarest class is too small to tune on, or when + no weighting strictly beats plain argmax. + """ + if metric not in DECISION_METRICS: + raise ValueError( + f"decision metric must be one of {', '.join(sorted(DECISION_METRICS))}" + ) + values = _validated_probabilities(probabilities) + n_classes = values.shape[1] + encoded_targets = _validated_targets(targets, len(values), n_classes) + no_op = (1.0,) * n_classes + + counts = np.bincount(encoded_targets, minlength=n_classes) + if int(counts.min()) < _MIN_CLASS_COUNT: + return no_op + + baseline_score = _metric_score( + metric, + encoded_targets, + _decision_labels(values, np.ones(n_classes, dtype=np.float32)), + ) + if n_classes == 2: + weights, score = _tune_binary(values, encoded_targets, metric) + else: + weights, score = _tune_multiclass(values, encoded_targets, metric, n_classes) + if score <= baseline_score: + return no_op + return tuple(float(weight) for weight in weights) + + +def serialize_decision_rule( + serialized: SerializedModelRepr, + weights: tuple[float, ...], +) -> SerializedModelRepr: + weight_values = np.asarray(weights, dtype=np.float32) + if weight_values.ndim != 1 or weight_values.size < 2: + raise ValueError("A decision rule needs one weight per class") + if not np.isfinite(weight_values).all() or (weight_values < 0).any(): + raise ValueError("Decision weights must be finite and non-negative") + + model = deepcopy(serialized.get_model()) + if len(model.graph.output) < 2: + raise ValueError("A decision rule graph must expose labels and probabilities") + label_output = model.graph.output[0] + probability_output = model.graph.output[-1] + if ( + not probability_output.type.HasField("tensor_type") + or probability_output.type.tensor_type.elem_type != onnx.TensorProto.FLOAT + ): + raise ValueError("Classifier probabilities must be a float tensor") + if ( + not label_output.type.HasField("tensor_type") + or label_output.type.tensor_type.elem_type != onnx.TensorProto.INT64 + ): + raise ValueError("Classifier labels must be an int64 tensor") + dimensions = probability_output.type.tensor_type.shape.dim + if ( + len(dimensions) == 2 + and dimensions[1].dim_value + and dimensions[1].dim_value != weight_values.size + ): + raise ValueError("The decision rule needs one weight per probability column") + + weights_name = "falcon_decision_weights" + scores_name = "falcon_decision_scores" + label_name = "falcon_decision_label" + model.graph.initializer.append( + helper.make_tensor( + weights_name, + onnx.TensorProto.FLOAT, + [1, int(weight_values.size)], + weight_values.tolist(), + ) + ) + model.graph.node.extend( + [ + helper.make_node( + "Mul", + [probability_output.name, weights_name], + [scores_name], + name="falcon_decision/apply_weights", + ), + helper.make_node( + "ArgMax", + [scores_name], + [label_name], + axis=1, + keepdims=0, + name="falcon_decision/argmax", + ), + ] + ) + model.graph.output[0].CopyFrom( + helper.make_tensor_value_info(label_name, onnx.TensorProto.INT64, [None]) + ) + if not any(opset.domain in {"", "ai.onnx"} for opset in model.opset_import): + model.opset_import.append(helper.make_opsetid("", ONNX_OPSET_VERSION)) + + return SerializedModelRepr( + model, + serialized.get_n_inputs(), + serialized.get_n_outputs(), + serialized.get_initial_types().copy(), + [shape.copy() for shape in serialized.get_initial_shapes()], + serialized.get_type(), + ) + + +__all__ = ["fit_decision_weights", "serialize_decision_rule"] diff --git a/falcon/tabular/evaluation.py b/falcon/tabular/evaluation.py new file mode 100644 index 0000000..4270d3a --- /dev/null +++ b/falcon/tabular/evaluation.py @@ -0,0 +1,77 @@ +from typing import Any + +import numpy as np +from numpy import typing as npt +from sklearn import metrics + + +def _scale_accuracy(accuracy: float, n_classes: int) -> float: + if accuracy < 0.0 or accuracy > 1.0: + raise ValueError("Accuracy score should be in range [0,1]") + if accuracy in {0.0, 1.0} or n_classes < 3: + return accuracy + + random_performance = 1 / n_classes + if accuracy <= random_performance: + return accuracy * 0.5 / random_performance + return accuracy * 0.5 / (1 - random_performance) + ( + 0.5 - 0.5 * random_performance / (1 - random_performance) + ) + + +def classification_metrics( + y: npt.NDArray[Any], + predictions: npt.NDArray[Any], +) -> dict[str, int | float]: + labels = np.asarray(y).astype(np.str_) + predicted_labels = np.asarray(predictions).astype(np.str_) + report = metrics.classification_report( + labels, + predicted_labels, + output_dict=True, + zero_division=0, + ) + macro_average = report["macro avg"] + weighted_average = report["weighted avg"] + n_classes = int(np.unique(labels).size) + balanced_accuracy = float(metrics.balanced_accuracy_score(labels, predicted_labels)) + result: dict[str, int | float] = { + "N_SAMPLES": len(labels), + "N_CLASSES": n_classes, + "ACC": float(metrics.accuracy_score(labels, predicted_labels)), + "BACC": balanced_accuracy, + "PRECISION": float(macro_average["precision"]), + "RECALL": float(macro_average["recall"]), + "F1": float(macro_average["f1-score"]), + "B_PRECISION": float(weighted_average["precision"]), + "B_RECALL": float(weighted_average["recall"]), + "B_F1": float(weighted_average["f1-score"]), + "SCORE": balanced_accuracy, + } + result["SC_SCORE"] = _scale_accuracy(balanced_accuracy, n_classes) + return result + + +def regression_metrics( + y: npt.NDArray[Any], + predictions: npt.NDArray[Any], +) -> dict[str, int | float]: + targets = np.asarray(y, dtype=np.float64).reshape(-1) + predicted_targets = np.asarray(predictions, dtype=np.float64).reshape(-1) + differences = targets - predicted_targets + r2 = float(metrics.r2_score(targets, predicted_targets)) + rmse = float(np.sqrt(np.mean(np.square(differences)))) + score = max(r2, 0.0) + return { + "N_SAMPLES": len(targets), + "R2": r2, + "RMSE": rmse, + "MSE": float(np.mean(np.square(differences))), + "MAE": float(np.mean(np.abs(differences))), + "RMSLE": float(np.log(rmse + 1e-7)), + "SCORE": score, + "SC_SCORE": (score + 1) / 2, + } + + +__all__ = ["classification_metrics", "regression_metrics"] diff --git a/falcon/tabular/hpo.py b/falcon/tabular/hpo.py new file mode 100644 index 0000000..4023667 --- /dev/null +++ b/falcon/tabular/hpo.py @@ -0,0 +1,429 @@ +from __future__ import annotations + +from importlib import import_module +from typing import Any + +import numpy as np +from numpy import typing as npt + +from falcon.config import ClassWeight +from falcon.constants import TABULAR_CLASSIFICATION_TASK, TABULAR_REGRESSION_TASK +from falcon.tabular.candidates import ( + CandidateTrainer, + EstimatorSpec, + score_oof_predictions, +) +from falcon.tabular.splitting import out_of_fold_indices +from falcon.utils import logger + +_EARLY_STOPPING_FAMILIES = { + "catboost", + "hist_gradient_boosting", + "lightgbm", + "xgboost", +} +_SUPPORTED_FAMILIES = { + "catboost", + "extra_trees", + "hist_gradient_boosting", + "lightgbm", + "linear", + "random_forest", + "xgboost", +} + + +def _load_optuna() -> Any: + try: + return import_module("optuna") + except ImportError as error: + raise ImportError( + "HPO candidate sources require the hpo extra; " + "install it with `pip install falcon-ml[hpo]`" + ) from error + + +def _base_parameters(family: str, task: str) -> dict[str, object]: + classification = task == TABULAR_CLASSIFICATION_TASK + if family == "hist_gradient_boosting": + return { + "learning_rate": 0.08, + "l2_regularization": 0.1, + "max_iter": 200, + "max_leaf_nodes": 31, + "min_samples_leaf": 20, + } + if family in {"extra_trees", "random_forest"}: + return { + "criterion": "gini" if classification else "squared_error", + "max_features": 0.75, + "min_samples_leaf": 1, + "n_estimators": 300, + } + if family == "linear": + return {"C": 1.0, "max_iter": 1_000} if classification else {"alpha": 1.0} + if family == "lightgbm": + return { + "colsample_bytree": 1.0, + "learning_rate": 0.1, + "min_child_samples": 20, + "n_estimators": 300, + "num_leaves": 31, + } + if family == "xgboost": + return { + "colsample_bytree": 1.0, + "learning_rate": 0.3, + "max_depth": 6, + "min_child_weight": 1.0, + "n_estimators": 300, + } + if family == "catboost": + return { + "depth": 6, + "iterations": 300, + "l2_leaf_reg": 3.0, + "learning_rate": 0.1, + } + raise ValueError( + f"Unknown HPO family `{family}`. Available families: " + f"{', '.join(sorted(_SUPPORTED_FAMILIES))}." + ) + + +def _suggest_parameters(trial: Any, family: str, task: str) -> dict[str, object]: + parameters = _base_parameters(family, task) + if family == "hist_gradient_boosting": + parameters.update( + { + "learning_rate": trial.suggest_float( + "learning_rate", 0.03, 0.2, log=True + ), + "l2_regularization": trial.suggest_categorical( + "l2_regularization", [0.0, 0.1, 1.0] + ), + "max_iter": trial.suggest_int("max_iter", 100, 300, step=100), + "max_leaf_nodes": trial.suggest_categorical( + "max_leaf_nodes", [15, 31, 63] + ), + "min_samples_leaf": trial.suggest_int( + "min_samples_leaf", 10, 40, step=10 + ), + } + ) + elif family in {"extra_trees", "random_forest"}: + parameters.update( + { + "max_features": trial.suggest_categorical( + "max_features", [0.5, 0.75, 1.0] + ), + "min_samples_leaf": trial.suggest_int("min_samples_leaf", 1, 4), + "n_estimators": trial.suggest_int("n_estimators", 100, 300, step=100), + } + ) + elif family == "linear": + name = "C" if task == TABULAR_CLASSIFICATION_TASK else "alpha" + parameters[name] = trial.suggest_float(name, 1e-3, 100.0, log=True) + elif family == "lightgbm": + parameters.update( + { + "colsample_bytree": trial.suggest_categorical( + "colsample_bytree", [0.75, 1.0] + ), + "learning_rate": trial.suggest_float( + "learning_rate", 0.01, 0.2, log=True + ), + "min_child_samples": trial.suggest_categorical( + "min_child_samples", [10, 20, 40] + ), + "n_estimators": trial.suggest_int("n_estimators", 100, 500, step=100), + "num_leaves": trial.suggest_categorical("num_leaves", [15, 31, 63]), + } + ) + elif family == "xgboost": + parameters.update( + { + "colsample_bytree": trial.suggest_categorical( + "colsample_bytree", [0.75, 1.0] + ), + "learning_rate": trial.suggest_float( + "learning_rate", 0.01, 0.3, log=True + ), + "max_depth": trial.suggest_int("max_depth", 3, 10), + "min_child_weight": trial.suggest_float( + "min_child_weight", 0.5, 5.0, log=True + ), + "n_estimators": trial.suggest_int("n_estimators", 100, 500, step=100), + } + ) + elif family == "catboost": + parameters.update( + { + "depth": trial.suggest_int("depth", 4, 10), + "iterations": trial.suggest_int("iterations", 100, 500, step=100), + "l2_leaf_reg": trial.suggest_float("l2_leaf_reg", 1.0, 10.0, log=True), + "learning_rate": trial.suggest_float( + "learning_rate", 0.01, 0.2, log=True + ), + } + ) + return parameters + + +def _trial_zero_parameters(family: str, task: str) -> dict[str, object]: + parameters = _base_parameters(family, task) + if family in {"extra_trees", "random_forest"}: + parameters.pop("criterion") + elif family == "linear" and task == TABULAR_CLASSIFICATION_TASK: + parameters.pop("max_iter") + return parameters + + +def _estimator_spec( + family: str, + parameters: dict[str, object], + trial_number: int, +) -> EstimatorSpec: + early_stopping_rounds = 20 if family in _EARLY_STOPPING_FAMILIES else None + return EstimatorSpec( + name=f"hpo_{family}_trial_{trial_number}", + family=family, + parameters=parameters, + early_stopping_rounds=early_stopping_rounds, + ) + + +def _trial_objective( + optuna: Any, + task: str, + family: str, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + groups: npt.NDArray[Any] | None, + splits: list[tuple[npt.NDArray[np.int64], npt.NDArray[np.int64]]], + random_state: int, + class_weight: ClassWeight, + prior_correct: bool, +) -> Any: + n_classes = int(np.unique(y).size) if task == TABULAR_CLASSIFICATION_TASK else None + trainer = CandidateTrainer( + task, + random_state=random_state, + class_weight=class_weight, + ) + baseline = _estimator_spec(family, _base_parameters(family, task), 0) + available_gbdt = trainer._available_gbdt((baseline,), n_classes) + trainer._assert_sample_weight_support((baseline,), n_classes, available_gbdt) + + def objective(trial: Any) -> float: + spec = _estimator_spec( + family, + _suggest_parameters(trial, family, task), + trial.number, + ) + output_shape: tuple[int, ...] = ( + (len(X), n_classes) if n_classes is not None else (len(X),) + ) + predictions = np.full(output_shape, np.nan, dtype=np.float32) + completed_indices: list[npt.NDArray[np.int64]] = [] + + for fold_index, (train_indices, eval_indices) in enumerate(splits): + fold_seed = random_state + fold_index + model = trainer._model( + spec, + n_classes, + available_gbdt, + random_state=fold_seed, + ) + fold_groups = None if groups is None else groups[train_indices] + ( + train_X, + train_y, + sample_weight, + validation_data, + early_stopping_rounds, + ) = trainer._fit_inputs( + spec, + X[train_indices], + y[train_indices], + fold_groups, + random_state=fold_seed, + ) + model.fit( + train_X, + train_y, + sample_weight=sample_weight, + validation_data=validation_data, + early_stopping_rounds=early_stopping_rounds, + ) + expected_shape: tuple[int, ...] + if task == TABULAR_CLASSIFICATION_TASK: + if n_classes is None: + raise RuntimeError("Classification class count is unavailable") + predict_proba = getattr(model, "predict_proba", None) + if not callable(predict_proba): + raise TypeError( + "Classification HPO candidates must expose predict_proba" + ) + fold_predictions = np.asarray( + predict_proba(X[eval_indices]), + dtype=np.float32, + ) + expected_shape = (len(eval_indices), n_classes) + else: + fold_predictions = np.asarray( + model.predict(X[eval_indices]), + dtype=np.float32, + ).reshape(-1) + expected_shape = (len(eval_indices),) + if fold_predictions.shape != expected_shape: + raise ValueError( + f"HPO candidate {spec.name} produced predictions with shape " + f"{fold_predictions.shape}; expected {expected_shape}" + ) + predictions[eval_indices] = fold_predictions + completed_indices.append(eval_indices) + evaluated = np.sort(np.concatenate(completed_indices)) + intermediate_score = score_oof_predictions( + predictions[evaluated], + y[evaluated], + task, + prior_correct=prior_correct, + ) + trial.report(intermediate_score, step=fold_index) + if trial.should_prune(): + raise optuna.TrialPruned() + + evaluation_indices = np.sort(np.concatenate(completed_indices)) + return score_oof_predictions( + predictions[evaluation_indices], + y[evaluation_indices], + task, + prior_correct=prior_correct, + ) + + return objective + + +def _best_distinct_trials(study: Any, top_n: int) -> list[Any]: + complete_trials = [ + trial + for trial in study.trials + if trial.value is not None and trial.state.name == "COMPLETE" + ] + complete_trials.sort(key=lambda trial: (-float(trial.value), trial.number)) + selected: list[Any] = [] + seen: set[tuple[tuple[str, str], ...]] = set() + for trial in complete_trials: + signature = tuple( + sorted((name, repr(value)) for name, value in trial.params.items()) + ) + if signature in seen: + continue + seen.add(signature) + selected.append(trial) + if len(selected) == top_n: + break + return selected + + +def generate_hpo_candidates( + task: str, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + *, + groups: npt.ArrayLike | None, + family: str, + n_trials: int, + top_n: int, + n_splits: int, + time_limit: float | None, + time_budget_fraction: float, + random_state: int, + class_weight: ClassWeight = "none", + prior_correct: bool = True, +) -> tuple[EstimatorSpec, ...]: + if task not in {TABULAR_CLASSIFICATION_TASK, TABULAR_REGRESSION_TASK}: + raise ValueError(f"Unknown task `{task}`") + if family not in _SUPPORTED_FAMILIES: + _base_parameters(family, task) + optuna = _load_optuna() + feature_values = np.asarray(X) + target_values = np.asarray(y) + if target_values.ndim == 2 and target_values.shape[1] == 1: + target_values = target_values[:, 0] + if ( + feature_values.ndim != 2 + or target_values.ndim != 1 + or len(feature_values) != len(target_values) + ): + raise ValueError("HPO features and targets must contain matching rows") + if task == TABULAR_CLASSIFICATION_TASK and not np.issubdtype( + target_values.dtype, np.integer + ): + raise ValueError("Classification targets must be integer encoded") + group_values = None if groups is None else np.asarray(groups) + if group_values is not None and group_values.ndim == 2: + if group_values.shape[1] == 1: + group_values = group_values[:, 0] + if group_values is not None and ( + group_values.ndim != 1 or len(group_values) != len(feature_values) + ): + raise ValueError("Groups must contain one value per HPO feature row") + + splits = out_of_fold_indices( + feature_values, + target_values, + task, + group_values, + n_splits=n_splits, + random_state=random_state, + ) + study = optuna.create_study( + direction="maximize", + sampler=optuna.samplers.TPESampler(seed=random_state), + pruner=optuna.pruners.MedianPruner( + n_startup_trials=5, + n_warmup_steps=1, + ), + ) + study.enqueue_trial(_trial_zero_parameters(family, task)) + timeout = None if time_limit is None else time_limit * time_budget_fraction + study.optimize( + _trial_objective( + optuna, + task, + family, + feature_values, + target_values, + group_values, + splits, + random_state, + class_weight, + prior_correct, + ), + n_trials=n_trials, + timeout=timeout, + show_progress_bar=False, + ) + + selected_trials = _best_distinct_trials(study, top_n) + if not selected_trials: + raise RuntimeError(f"HPO produced no successful `{family}` trials") + logger.info( + "HPO completed %d trial(s) for %s and emitted %d candidate(s).", + len(study.trials), + family, + len(selected_trials), + ) + return tuple( + _estimator_spec( + family, + {**_base_parameters(family, task), **trial.params}, + trial.number, + ) + for trial in selected_trials + ) + + +__all__ = ["generate_hpo_candidates"] diff --git a/falcon/tabular/ingestion.py b/falcon/tabular/ingestion.py new file mode 100644 index 0000000..176614a --- /dev/null +++ b/falcon/tabular/ingestion.py @@ -0,0 +1,272 @@ +from __future__ import annotations + +from typing import Any, TypeAlias, cast + +import numpy as np +import pandas as pd +from numpy import typing as npt + +from falcon import types as ft +from falcon.constants import TABULAR_CLASSIFICATION_TASK, TABULAR_REGRESSION_TASK +from falcon.type_guessing import determine_column_types +from falcon.types import DatasetSchema, TargetKind +from falcon.utils import logger + +TabularData: TypeAlias = str | npt.NDArray[Any] | pd.DataFrame | tuple[Any, Any] + + +def read_data(path: str) -> pd.DataFrame: + if path.endswith(".csv"): + return pd.read_csv(path) + if path.endswith(".parquet"): + return pd.read_parquet(path) + raise ValueError("Only `.csv` and `.parquet` files are supported") + + +def _target_kind(task: str) -> TargetKind: + if task == TABULAR_CLASSIFICATION_TASK: + return "classification" + if task == TABULAR_REGRESSION_TASK: + return "regression" + raise ValueError(f"Unknown tabular task `{task}`") + + +def _validate_features(features: ft.ColumnsList | None) -> None: + if features is not None and len(features) < 1: + raise ValueError("Features List cannot be empty") + + +def _dataframe_parts( + data: pd.DataFrame, + features: ft.ColumnsList | None, + target: str | int | None, +) -> tuple[pd.DataFrame, pd.Series[Any], tuple[str, ...], str]: + _validate_features(features) + if features is not None and target is None: + raise ValueError( + "Either both target and features should be provided or neither of them." + ) + if data.shape[1] < 2 and features is None: + raise ValueError("Tabular data must contain at least one feature and a target") + + if target is None: + target_data: pd.Series[Any] | pd.DataFrame = data.iloc[:, -1] + target_name = str(data.columns[-1]) + elif isinstance(target, str): + target_data = data.loc[:, target] + target_name = target + else: + target_data = data.iloc[:, target] + target_name = str(data.columns[target]) + if isinstance(target_data, pd.DataFrame): + if target_data.shape[1] != 1: + raise ValueError("The target should contain only one column.") + target_data = target_data.iloc[:, 0] + + if features is None: + if target is None: + feature_data = data.iloc[:, :-1] + elif isinstance(target, str): + feature_data = data.loc[:, data.columns != target] + else: + target_position = target % data.shape[1] + positions = [ + position + for position in range(data.shape[1]) + if position != target_position + ] + feature_data = data.iloc[:, positions] + elif all(isinstance(feature, str) for feature in features): + feature_data = data.loc[:, cast(list[str], features)] + elif all(isinstance(feature, (int, np.integer)) for feature in features): + feature_data = data.iloc[:, cast(list[int], features)] + else: + raise ValueError("Features must contain only column names or only indices") + + column_names = tuple(str(column) for column in feature_data.columns) + return feature_data, target_data, column_names, target_name + + +def _array_parts( + data: npt.NDArray[Any], + features: ft.ColumnsList | None, + target: str | int | None, +) -> tuple[npt.NDArray[Any], npt.NDArray[Any], tuple[str, ...], str]: + _validate_features(features) + if data.ndim != 2: + raise ValueError("Tabular arrays must be two-dimensional") + if data.shape[1] < 2 and features is None: + raise ValueError("Tabular data must contain at least one feature and a target") + if (target is None or features is None) and not ( + target is None and features is None + ): + raise ValueError( + "Either both target and features should be provided or neither of them." + ) + if features is not None and not all( + isinstance(feature, (int, np.integer)) for feature in features + ): + raise ValueError("Expected list of integers as features, found strings") + if isinstance(target, str): + raise ValueError("Expected integer as target, found string") + + if features is None: + feature_indices = list(range(data.shape[1] - 1)) + target_index = data.shape[1] - 1 + else: + feature_indices = [int(feature) for feature in features] + if target is None: + raise RuntimeError("Target validation did not run") + target_index = int(target) + + X = data[:, feature_indices] + y = data[:, target_index] + column_names = tuple(f"feature_{index}" for index in feature_indices) + return X, y, column_names, "target" + + +def _tuple_parts( + data: tuple[Any, Any], + features: ft.ColumnsList | None, + target: str | int | None, +) -> tuple[npt.NDArray[Any], npt.NDArray[Any], tuple[str, ...], str]: + if len(data) != 2: + raise ValueError( + "When passing data as tuple, it should contain exactly 2 elements: `X` and `y`." + ) + if features is not None or target is not None: + logger.warning( + "When data is passed as tuple of (X, y) all columns are used regardless the values of `features` or `target` arguments." + ) + + raw_X, raw_y = data + if isinstance(raw_X, pd.DataFrame): + column_names = tuple(str(column) for column in raw_X.columns) + X = raw_X.to_numpy(dtype=np.object_) + else: + X = np.asarray(raw_X, dtype=np.object_) + if X.ndim != 2: + raise ValueError("Feature arrays must be two-dimensional") + column_names = tuple(f"feature_{index}" for index in range(X.shape[1])) + + if isinstance(raw_y, pd.DataFrame): + if raw_y.shape[1] != 1: + raise ValueError("The target should contain only one column.") + target_name = str(raw_y.columns[0]) + y = raw_y.to_numpy(dtype=np.object_) + elif isinstance(raw_y, pd.Series): + target_name = str(raw_y.name) if raw_y.name is not None else "target" + y = raw_y.to_numpy(dtype=np.object_) + else: + target_name = "target" + y = np.asarray(raw_y, dtype=np.object_) + return X, y, column_names, target_name + + +def _validate_shapes( + X: npt.NDArray[Any], y: npt.NDArray[Any], column_names: tuple[str, ...] +) -> tuple[npt.NDArray[np.object_], npt.NDArray[np.object_]]: + X = np.asarray(X, dtype=np.object_) + y = np.asarray(y, dtype=np.object_) + if X.ndim != 2: + raise ValueError("Feature arrays must be two-dimensional") + if X.shape[1] == 0: + raise ValueError("Tabular data must contain at least one feature") + if len(column_names) != X.shape[1]: + raise ValueError("Feature names do not match the feature array") + if y.ndim == 2: + if y.shape[1] != 1: + raise ValueError("The target should contain only one column.") + y = y[:, 0] + elif y.ndim != 1: + raise ValueError("The target should contain only one column.") + if X.shape[0] != y.shape[0]: + raise ValueError("Features and target must contain the same number of rows") + if X.shape[0] == 0: + raise ValueError("Tabular data must contain at least one row") + return X, y + + +def _drop_missing_targets( + X: npt.NDArray[np.object_], + y: npt.NDArray[np.object_], + row_indices: npt.NDArray[np.int64], +) -> tuple[ + npt.NDArray[np.object_], + npt.NDArray[np.object_], + npt.NDArray[np.int64], +]: + missing_target = np.asarray(pd.isna(y), dtype=np.bool_) + missing_count = int(missing_target.sum()) + if missing_count == 1: + logger.info("Dropped 1 row with a missing target value.") + elif missing_count > 1: + logger.info("Dropped %d rows with missing target values.", missing_count) + if missing_count: + keep = ~missing_target + X = X[keep] + y = y[keep] + row_indices = row_indices[keep] + if X.shape[0] == 0: + raise ValueError("No rows remain after dropping missing target values") + return X, y, row_indices + + +def ingest_data_with_row_selection( + data: TabularData, + task: str, + features: ft.ColumnsList | None = None, + target: str | int | None = None, +) -> tuple[ + npt.NDArray[np.object_], + npt.NDArray[np.object_], + DatasetSchema, + npt.NDArray[np.int64], + int, +]: + if isinstance(data, str): + data = read_data(data) + + if isinstance(data, tuple): + X, y, column_names, target_name = _tuple_parts(data, features, target) + elif isinstance(data, pd.DataFrame): + feature_data, target_data, column_names, target_name = _dataframe_parts( + data, features, target + ) + X = feature_data.to_numpy(dtype=np.object_) + y = target_data.to_numpy(dtype=np.object_) + elif isinstance(data, np.ndarray): + X, y, column_names, target_name = _array_parts(data, features, target) + else: + raise TypeError( + "Data must be a csv/parquet path, DataFrame, ndarray, or (X, y) tuple" + ) + + X, y = _validate_shapes(X, y, column_names) + source_row_count = X.shape[0] + row_indices: npt.NDArray[np.int64] = np.arange(source_row_count, dtype=np.int64) + X, y, row_indices = _drop_missing_targets(X, y, row_indices) + column_types = tuple(determine_column_types(X)) + schema = DatasetSchema( + column_names=column_names, + column_types=column_types, + target_name=target_name, + target_kind=_target_kind(task), + dimensions=cast(tuple[int, int], X.shape), + ) + return X, y, schema, row_indices, source_row_count + + +def ingest_data( + data: TabularData, + task: str, + features: ft.ColumnsList | None = None, + target: str | int | None = None, +) -> tuple[npt.NDArray[np.object_], npt.NDArray[np.object_], DatasetSchema]: + X, y, schema, _, _ = ingest_data_with_row_selection( + data, + task=task, + features=features, + target=target, + ) + return X, y, schema diff --git a/falcon/tabular/learners/__init__.py b/falcon/tabular/learners/__init__.py deleted file mode 100644 index 5f29cb8..0000000 --- a/falcon/tabular/learners/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from falcon.tabular.learners.super_learner import SuperLearner -from falcon.tabular.learners.optuna_learner import OptunaLearner -from falcon.tabular.learners.plain_learner import PlainLearner \ No newline at end of file diff --git a/falcon/tabular/learners/optuna_learner.py b/falcon/tabular/learners/optuna_learner.py deleted file mode 100644 index 1e77627..0000000 --- a/falcon/tabular/learners/optuna_learner.py +++ /dev/null @@ -1,244 +0,0 @@ -from falcon.abstract import Learner -from typing import Type, Optional, Any, Union, Dict, Callable, Tuple -from falcon.types import Float32Array, Int64Array -from numpy import typing as npt -from falcon.serialization import SerializedModelRepr -from falcon.abstract import Model, ONNXConvertible -from falcon.abstract.optuna import OptunaMixin -from falcon.tabular.models.hist_gbt import ( - HistGradientBoostingClassifier, - HistGradientBoostingRegressor, -) -from sklearn.model_selection import train_test_split -from sklearn.metrics import mean_squared_error, balanced_accuracy_score -import optuna -from imblearn.over_sampling import RandomOverSampler -from tqdm import tqdm - - -class OptunaLearner(Learner, ONNXConvertible): - """ - OptunaLerner select the best hyperparameters for the given model using the Optuna Framework. - """ - - def __init__( - self, - task: str, - model_class: Optional[Type] = None, - n_trials: Optional[int] = None, - dataset_size: Optional[Tuple[int, ...]] = None, - **kwargs: Any, - ) -> None: - """ - - Parameters - ---------- - task : str - 'tabular_classification' or 'tabular_regression' - model_class : Optional[Type], optional - the class of the model to train, by default None; - if None, HistGradientBoosting - n_trials : Optional[int], optional - number of optimization trials, minimum 20, by default None; - if None, the number of trials is chosen dynamically based on the dataset size - dataset_size : Optional[Tuple[int]], optional - the size of the dataset, by default None; - - """ - self.task = task - self.dataset_size = dataset_size - self.model_class: Type[Any] - if model_class is None: - if task == "tabular_classification": - self.model_class = HistGradientBoostingClassifier - elif task == "tabular_regression": - self.model_class = HistGradientBoostingRegressor - else: - ValueError("Not supported task") - else: - self.model_class = model_class - - if not issubclass(self.model_class, Model) or not issubclass(self.model_class, OptunaMixin): # type: ignore - raise ValueError( - "Model class should be a subclass of falcon.abstract.Model" - ) - if not issubclass(self.model_class, ONNXConvertible): # type: ignore - raise ValueError("OptunaLearner only supports ONNXConvertible models") - - if n_trials is not None and n_trials < 5: - print("n_trials should be >= 20, setting n_trials = 20") - n_trials = 20 - - self.n_trials = n_trials - - def _make_objective_func( - self, search_space: Union[Dict, Callable], X: npt.NDArray, y: npt.NDArray - ) -> Callable: - stratify = y if self.task == "tabular_classification" else None - X_train, X_val, y_train, y_val = train_test_split( - X, y, random_state=42, stratify=stratify - ) - - if self.task == "tabular_regression": - loss = mean_squared_error - elif self.task == "tabular_classification": - loss = lambda y, y_h: -balanced_accuracy_score(y, y_h) - X_train, y_train = RandomOverSampler().fit_resample(X_train, y_train) - progress_bar = tqdm(total=self.n_trials) - if isinstance(search_space, Dict): - search_space_dict: Dict = search_space - - def objective(trial) -> float: # type: ignore - params = {} - for hp_n, hp_v in search_space_dict.items(): - if hp_v["type"] == "int": - params[hp_n] = trial.suggest_int(name=hp_n, **hp_v["kwargs"]) - if hp_v["type"] == "float": - params[hp_n] = trial.suggest_float(name=hp_n, **hp_v["kwargs"]) - if hp_v["type"] == "categorical": - params[hp_n] = trial.suggest_categorical( - name=hp_n, **hp_v["kwargs"] - ) - - model = self.model_class(**params) - model.fit(X_train, y_train) - y_pred = model.predict(X_val) - loss_ = loss(y_val, y_pred) - progress_bar.update(1) - return loss_ - - else: - search_space_fn: Callable = search_space - - def objective(trial) -> float: # type: ignore - res = search_space_fn(trial, X_train, X_val, y_train, y_val) - if res["loss"] is None: - pred = res["predictions"] - loss_ = loss(y_val, pred) - else: - loss_ = res["loss"] - progress_bar.update(1) - return loss_ - - self.progress_bar = progress_bar - return objective - - def _set_n_trials(self, X: npt.NDArray, y: npt.NDArray) -> None: - if self.n_trials is not None: - return - - if self.dataset_size is None: - self.dataset_size = X.shape - volume = self.dataset_size[0] * self.dataset_size[1] - - min_threshold = 80_000 # 5_000 samples with 16 features - mid_threshold = 4_000_000 # 125_000 samples with 32 featrues / 250_000 samples with 16 features - large_threshold = 16_000_000 # 1_000_000 samples with 16 features - - if volume < min_threshold: - self.n_trials = 1000 - elif volume < mid_threshold: - self.n_trials = 500 - elif volume < large_threshold: - self.n_trials = 200 - else: - self.n_trials = 100 - - def fit(self, X: Float32Array, y: Float32Array, *args: Any, **kwargs: Any) -> None: - """ - Fits the model by choosing the best hyperparameters and training the final model using them. - For classification tasks, the dataset will be balanced by upsampling the minority class(es). - - Parameters - ---------- - X : Float32Array - features - y : Float32Array - targets - """ - self._set_n_trials(X, y) - search_space = self.model_class.get_search_space(X, y) - self.progress_bar = None - objective = self._make_objective_func(search_space, X, y) - - optuna.logging.set_verbosity(optuna.logging.ERROR) - study = optuna.create_study( - direction="minimize", sampler=optuna.samplers.TPESampler(seed=42) - ) - study.optimize(objective, n_trials=self.n_trials) - if self.progress_bar: - self.progress_bar.close() - best_params = study.best_params - self.best_params_ = best_params - - if self.task == "tabular_classification": - X, y = RandomOverSampler().fit_resample(X, y) - model = self.model_class(**best_params) - model.fit(X, y) - self.model = model - - def predict( - self, X: Float32Array, *args: Any, **kwargs: Any - ) -> Union[Float32Array, Int64Array]: - return self.model.predict(X) - - def get_input_type(self) -> Type: - """ - Returns - ------- - Type - Float32Array - """ - return Float32Array - - def get_output_type(self) -> Type: - """ - Returns - ------- - Type - Float32Array for regression, Int64Array for classification - """ - return Float32Array if self.task == "tabular_regression" else Int64Array - - def forward( - self, X: Float32Array, *args: Any, **kwargs: Any - ) -> Union[Float32Array, Int64Array]: - """ - Equivalent to `.predict(X)` - - Parameters - ---------- - X : Float32Array - features - - Returns - ------- - Union[Float32Array, Int64Array] - predictions - """ - return self.model.predict(X) - - def fit_pipe( - self, X: Float32Array, y: Float32Array, *args: Any, **kwargs: Any - ) -> None: - """ - Equivalent to `.fit(X, y)` - - Parameters - ---------- - X : Float32Array - features - y : Float32Array - targets - """ - self.fit(X, y) - - def to_onnx(self) -> SerializedModelRepr: - """ - Serializes the underlying model to onnx by calling its `.to_onnx()` method. - - Returns - ------- - SerializedModelRepr - """ - return self.model.to_onnx() diff --git a/falcon/tabular/learners/plain_learner.py b/falcon/tabular/learners/plain_learner.py deleted file mode 100644 index 39a3bc1..0000000 --- a/falcon/tabular/learners/plain_learner.py +++ /dev/null @@ -1,110 +0,0 @@ -from falcon.abstract import Learner -from falcon.abstract import Model, ONNXConvertible -from typing import Type, Optional, Any, Dict, Union -from falcon.tabular.models.hist_gbt import HistGradientBoostingClassifier, HistGradientBoostingRegressor -from falcon.types import Float32Array, Int64Array -from imblearn.over_sampling import RandomOverSampler -from falcon.serialization import SerializedModelRepr - -class PlainLearner(Learner, ONNXConvertible): - """ - PlainLearner trains a model using provided or default hyperparameters. - """ - def __init__(self, task: str, model_class: Optional[Type] = None, hyperparameters: Optional[Dict] = None, **kwargs: Any) -> None: - """ - Parameters - ---------- - task : str - 'tabular_classification' or 'tabular_regression' - model_class : Optional[Type], optional - the class of the model to train, by default None; - if None, HistGradientBoosting is used - hyperparameters: Dict, optional - the dictionary of hyperparameters for model training - """ - self.task = task - self.model_class: Type[Any] - self.hyperparameters = hyperparameters if hyperparameters else {} - if model_class is None: - if task == "tabular_classification": - self.model_class = HistGradientBoostingClassifier - elif task == "tabular_regression": - self.model_class = HistGradientBoostingRegressor - else: - ValueError("Not supported task") - else: - self.model_class = model_class - if not issubclass(self.model_class, Model): - raise ValueError('Model class should be a subclass of falcon.abstract.Model') - if not issubclass(self.model_class, ONNXConvertible): - raise ValueError('PlainLearner only supports ONNXConvertible models') - - def get_input_type(self) -> Type: - """ - Returns - ------- - Type - Float32Array - """ - return Float32Array - - def get_output_type(self) -> Type: - """ - Returns - ------- - Type - Float32Array for regression, Int64Array for classification - """ - return Float32Array if self.task == "tabular_regression" else Int64Array - - def fit(self, X: Float32Array, y: Float32Array, *args: Any, **kwargs: Any) -> None: - """ - Fits the model and trains the final model using them. - For classification tasks, the dataset will be balanced by upsampling the minority class(es). - - Parameters - ---------- - X : Float32Array - features - y : Float32Array - targets - """ - - if self.task == 'tabular_classification': - X, y = RandomOverSampler().fit_resample( - X, y - ) - model = self.model_class(**self.hyperparameters) - model.fit(X,y) - self.model = model - - def predict(self, X: Float32Array, *args: Any, **kwargs: Any) -> Union[Float32Array, Int64Array]: - return self.model.predict(X) - - # didn't work without forward method - - - def fit_pipe(self, X: Float32Array, y: Float32Array, *args: Any, **kwargs: Any) -> None: - """ - Equivalent to `.fit(X, y)` - - Parameters - ---------- - X : Float32Array - features - y : Float32Array - targets - """ - self.fit(X, y) - - def to_onnx(self) -> SerializedModelRepr: - """ - Serializes the underlying model to onnx by calling its `.to_onnx()` method. - - Returns - ------- - SerializedModelRepr - """ - return self.model.to_onnx() - - \ No newline at end of file diff --git a/falcon/tabular/learners/super_learner.py b/falcon/tabular/learners/super_learner.py deleted file mode 100644 index 560fc24..0000000 --- a/falcon/tabular/learners/super_learner.py +++ /dev/null @@ -1,1209 +0,0 @@ -from typing import Callable -from sklearn.ensemble import ( - AdaBoostRegressor, - GradientBoostingClassifier, - GradientBoostingRegressor, -) -from sklearn.linear_model import ElasticNet, LinearRegression -from sklearn.base import BaseEstimator as SklearnBaseEstimator -from sklearn.svm import NuSVR, SVR -from falcon.abstract import Learner -from falcon.abstract.onnx_convertible import ONNXConvertible -from falcon.types import Float32Array, Int64Array -from typing import Dict, List, Tuple, Callable, Optional, List, Type, Any, Union - -from imblearn.over_sampling import RandomOverSampler -from sklearn.discriminant_analysis import ( - LinearDiscriminantAnalysis, - # QuadraticDiscriminantAnalysis, # DO NOT USE -) -from sklearn.ensemble import ( - AdaBoostClassifier, - AdaBoostRegressor, - BaggingClassifier, - BaggingRegressor, - ExtraTreesClassifier, - ExtraTreesRegressor, - HistGradientBoostingClassifier, - HistGradientBoostingRegressor, - GradientBoostingClassifier, - GradientBoostingRegressor, - RandomForestClassifier, - RandomForestRegressor, -) -from sklearn.linear_model import ElasticNet, LinearRegression, LogisticRegression -from sklearn.naive_bayes import GaussianNB -from sklearn.svm import SVC, SVR, NuSVC, NuSVR -from sklearn.tree import DecisionTreeClassifier, DecisionTreeRegressor - -from sklearn.model_selection import train_test_split -from sklearn.metrics import r2_score, balanced_accuracy_score - -from falcon.tabular.models import StackingRegressor, StackingClassifier -from falcon.utils import print_ -import pandas as pd - -import numpy as np -from numpy import typing as npt -from falcon.serialization import SerializedModelRepr - -_SKLEARN_VERBOSE = 0 # for debugging only - -_default_estimators: Dict = { - "tabular_regression": { - "mini": [ - ("LinearRegression", LinearRegression, {}), - ("ElasticNet", ElasticNet, {}), - ("SVR", SVR, {}), - ("NuSVR", NuSVR, {}), - ("DecisionTreeRegressor", DecisionTreeRegressor, {}), - ( - "HistGradientBoostingRegressor", - HistGradientBoostingRegressor, - {"min_samples_leaf": 2}, - ), - ("GradientBoostingRegressor_100", GradientBoostingRegressor, {}), - ("AdaBoostRegressor_50", AdaBoostRegressor, {}), - ( - "BaggingRegressor_10", - BaggingRegressor, - { - "base_estimator": DecisionTreeRegressor(min_samples_split=2), - "n_jobs": -1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "RandomForestRegressor_100", - RandomForestRegressor, - {"min_samples_split": 2, "n_jobs": -1, "verbose": _SKLEARN_VERBOSE}, - ), - ( - "ExtraTreesRegressor_100", - ExtraTreesRegressor, - {"min_samples_split": 2, "n_jobs": -1, "verbose": _SKLEARN_VERBOSE}, - ), - ("SVR_linear", SVR, {"C": 0.025, "kernel": "linear"}), - ( - "GradientBoostingRegressor_10", - GradientBoostingRegressor, - {"n_estimators": 10}, - ), - ( - "GradientBoostingRegressor_25", - GradientBoostingRegressor, - {"n_estimators": 25}, - ), - ( - "GradientBoostingRegressor_50", - GradientBoostingRegressor, - {"n_estimators": 50}, - ), - ( - "GradientBoostingRegressor_200", - GradientBoostingRegressor, - {"n_estimators": 200}, - ), - ("AdaBoostRegressor_10", AdaBoostRegressor, {"n_estimators": 10}), - ("AdaBoostRegressor_25", AdaBoostRegressor, {"n_estimators": 25}), - ("AdaBoostRegressor_100", AdaBoostRegressor, {"n_estimators": 100}), - ( - "BaggingRegressor_25", - BaggingRegressor, - { - "n_estimators": 25, - "base_estimator": DecisionTreeRegressor(min_samples_split=2), - "n_jobs": -1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "BaggingRegressor_50", - BaggingRegressor, - { - "n_estimators": 50, - "base_estimator": DecisionTreeRegressor(min_samples_split=2), - "n_jobs": -1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "BaggingRegressor_100", - BaggingRegressor, - { - "n_estimators": 100, - "base_estimator": DecisionTreeRegressor(min_samples_split=2), - "n_jobs": -1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "RandomForestRegressor_10", - RandomForestRegressor, - { - "n_estimators": 10, - "min_samples_split": 2, - "n_jobs": -1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "RandomForestRegressor_25", - RandomForestRegressor, - { - "n_estimators": 25, - "min_samples_split": 2, - "n_jobs": -1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "RandomForestRegressor_50", - RandomForestRegressor, - { - "n_estimators": 50, - "min_samples_split": 2, - "n_jobs": -1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "RandomForestRegressor_200", - RandomForestRegressor, - { - "n_estimators": 200, - "min_samples_split": 2, - "n_jobs": -1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "ExtraTreesRegressor_10", - ExtraTreesRegressor, - { - "n_estimators": 10, - "min_samples_split": 2, - "n_jobs": -1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "ExtraTreesRegressor_25", - ExtraTreesRegressor, - { - "n_estimators": 25, - "min_samples_split": 2, - "n_jobs": -1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "ExtraTreesRegressor_50", - ExtraTreesRegressor, - { - "n_estimators": 50, - "min_samples_split": 2, - "n_jobs": -1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "ExtraTreesRegressor_200", - ExtraTreesRegressor, - { - "n_estimators": 200, - "min_samples_split": 2, - "n_jobs": -1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ], - "mid": [ - ("ElasticNet", ElasticNet, {}), - ( - "DecisionTreeRegressor", - DecisionTreeRegressor, - {"min_samples_split": 0.001}, - ), - ( - "HistGradientBoostingRegressor_150", - HistGradientBoostingRegressor, - {"max_iter": 150}, - ), - ("HistGradientBoostingRegressor", HistGradientBoostingRegressor, {}), - ( - "HistGradientBoostingRegressor_50", - HistGradientBoostingRegressor, - {"max_iter": 50}, - ), - ( - "GradientBoostingRegressor_100", - GradientBoostingRegressor, - {"min_samples_split": 0.003}, - ), - ("AdaBoostRegressor_50", AdaBoostRegressor, {}), - ( - "BaggingRegressor_10", - BaggingRegressor, - { - "base_estimator": DecisionTreeRegressor(min_samples_split=0.001), - "n_jobs": 4, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "RandomForestRegressor_100", - RandomForestRegressor, - {"min_samples_split": 0.003, "n_jobs": 4, "verbose": _SKLEARN_VERBOSE}, - ), - ( - "ExtraTreesRegressor_100", - ExtraTreesRegressor, - {"min_samples_split": 0.003, "n_jobs": 4, "verbose": _SKLEARN_VERBOSE}, - ), - ( - "GradientBoostingRegressor_10", - GradientBoostingRegressor, - {"n_estimators": 10, "min_samples_split": 0.003}, - ), - ( - "GradientBoostingRegressor_25", - GradientBoostingRegressor, - {"n_estimators": 25, "min_samples_split": 0.003}, - ), - ( - "GradientBoostingRegressor_50", - GradientBoostingRegressor, - {"n_estimators": 50, "min_samples_split": 0.003}, - ), - ("AdaBoostRegressor_25", AdaBoostRegressor, {"n_estimators": 25}), - ("AdaBoostRegressor_100", AdaBoostRegressor, {"n_estimators": 100}), - ( - "BaggingRegressor_25", - BaggingRegressor, - { - "n_estimators": 25, - "base_estimator": DecisionTreeRegressor(min_samples_split=0.003), - "n_jobs": 4, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "BaggingRegressor_50", - BaggingRegressor, - { - "n_estimators": 50, - "base_estimator": DecisionTreeRegressor(min_samples_split=0.003), - "n_jobs": 4, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "BaggingRegressor_100", - BaggingRegressor, - { - "n_estimators": 100, - "base_estimator": DecisionTreeRegressor(min_samples_split=0.003), - "n_jobs": 4, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "RandomForestRegressor_10", - RandomForestRegressor, - { - "n_estimators": 10, - "min_samples_split": 0.003, - "n_jobs": 4, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "RandomForestRegressor_25", - RandomForestRegressor, - { - "n_estimators": 25, - "min_samples_split": 0.003, - "n_jobs": 4, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "RandomForestRegressor_50", - RandomForestRegressor, - { - "n_estimators": 50, - "min_samples_split": 0.003, - "n_jobs": 4, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "ExtraTreesRegressor_10", - ExtraTreesRegressor, - { - "n_estimators": 10, - "min_samples_split": 0.003, - "n_jobs": 4, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "ExtraTreesRegressor_25", - ExtraTreesRegressor, - { - "n_estimators": 25, - "min_samples_split": 0.003, - "n_jobs": 4, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "ExtraTreesRegressor_50", - ExtraTreesRegressor, - { - "n_estimators": 50, - "min_samples_split": 0.003, - "n_jobs": 4, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ], - "large": [ - ("ElasticNet", ElasticNet, {}), - ("HistGradientBoostingRegressor", HistGradientBoostingRegressor, {}), - ( - "HistGradientBoostingRegressor_200", - HistGradientBoostingRegressor, - {"max_iter": 200}, - ), - ( - "BaggingRegressor_10", - BaggingRegressor, - { - "base_estimator": DecisionTreeRegressor(min_samples_split=0.001), - "n_jobs": 1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "RandomForestRegressor_100", - RandomForestRegressor, - {"min_samples_split": 0.003, "n_jobs": 1, "verbose": _SKLEARN_VERBOSE}, - ), - ( - "ExtraTreesRegressor_100", - ExtraTreesRegressor, - {"min_samples_split": 0.003, "n_jobs": 2, "verbose": _SKLEARN_VERBOSE}, - ), - ( - "GradientBoostingRegressor_25", - GradientBoostingRegressor, - {"n_estimators": 25}, - ), - ("AdaBoostRegressor_10", AdaBoostRegressor, {"n_estimators": 10}), - ("AdaBoostRegressor_25", AdaBoostRegressor, {"n_estimators": 25}), - ( - "BaggingRegressor_25", - BaggingRegressor, - { - "n_estimators": 25, - "base_estimator": DecisionTreeRegressor(min_samples_split=0.001), - "n_jobs": 2, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "RandomForestRegressor_10", - RandomForestRegressor, - { - "n_estimators": 10, - "min_samples_split": 0.003, - "n_jobs": 1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "RandomForestRegressor_25", - RandomForestRegressor, - { - "n_estimators": 25, - "min_samples_split": 0.003, - "n_jobs": 1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "RandomForestRegressor_50", - RandomForestRegressor, - { - "n_estimators": 50, - "min_samples_split": 0.003, - "n_jobs": 1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "ExtraTreesRegressor_10", - ExtraTreesRegressor, - { - "n_estimators": 10, - "min_samples_split": 0.003, - "n_jobs": 1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "ExtraTreesRegressor_25", - ExtraTreesRegressor, - { - "n_estimators": 25, - "min_samples_split": 0.003, - "n_jobs": 1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "ExtraTreesRegressor_50", - ExtraTreesRegressor, - { - "n_estimators": 50, - "min_samples_split": 0.003, - "n_jobs": 1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ], - "x-large": [ - ("HistGradientBoostingRegressor_100", HistGradientBoostingRegressor, {}), - ( - "HistGradientBoostingRegressor_200", - HistGradientBoostingRegressor, - {"max_iter": 200}, - ), - ( - "HistGradientBoostingRegressor_200", - HistGradientBoostingRegressor, - {"max_iter": 50}, - ), - ( - "RandomForestRegressor_100", - RandomForestRegressor, - {"min_samples_split": 0.003, "n_jobs": 1, "verbose": _SKLEARN_VERBOSE}, - ), - ], - }, - "tabular_classification": { - "mini": [ - ("LogisticRegression", LogisticRegression, {}), - ("DecisionTreeClassifier", DecisionTreeClassifier, {}), - ("SVC", SVC, {}), - ("NuSVC", NuSVC, {}), - ("GaussianNB", GaussianNB, {}), - ("AdaBoostClassifier_50", AdaBoostClassifier, {}), - ("GradientBoostingClassifier_100", GradientBoostingClassifier, {}), - ( - "HistGradientBoostingClassifier", - HistGradientBoostingClassifier, - {"min_samples_leaf": 2}, - ), - ( - "RandomForestClassifier_100", - RandomForestClassifier, - {"min_samples_split": 0.003, "n_jobs": -1, "verbose": _SKLEARN_VERBOSE}, - ), - ( - "BaggingClassifier_10", - BaggingClassifier, - { - "base_estimator": DecisionTreeClassifier(min_samples_split=0.001), - "n_jobs": -1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "ExtraTreesClassifier_100", - ExtraTreesClassifier, - {"min_samples_split": 0.003, "n_jobs": -1, "verbose": _SKLEARN_VERBOSE}, - ), - ("SVC_linear", SVC, {"C": 0.025, "kernel": "linear"}), - ("LinearDiscriminantAnalysis", LinearDiscriminantAnalysis, {}), - # ("QuadraticDiscriminantAnalysis", QuadraticDiscriminantAnalysis, {}), # this breaks onnx - ("AdaBoostClassifier_10", AdaBoostClassifier, {"n_estimators": 10}), - ("AdaBoostClassifier_25", AdaBoostClassifier, {"n_estimators": 25}), - ("AdaBoostClassifier_100", AdaBoostClassifier, {"n_estimators": 100}), - ( - "GradientBoostingClassifier_10", - GradientBoostingClassifier, - {"n_estimators": 10}, - ), - ( - "GradientBoostingClassifier_25", - GradientBoostingClassifier, - {"n_estimators": 25}, - ), - ( - "GradientBoostingClassifier_50", - GradientBoostingClassifier, - {"n_estimators": 50}, - ), - ( - "GradientBoostingClassifier_200", - GradientBoostingClassifier, - {"n_estimators": 200}, - ), - ( - "RandomForestClassifier_10", - RandomForestClassifier, - { - "n_estimators": 10, - "min_samples_split": 2, - "n_jobs": -1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "RandomForestClassifier_25", - RandomForestClassifier, - { - "n_estimators": 25, - "min_samples_split": 2, - "n_jobs": -1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "RandomForestClassifier_50", - RandomForestClassifier, - { - "n_estimators": 50, - "min_samples_split": 2, - "n_jobs": -1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "RandomForestClassifier_200", - RandomForestClassifier, - { - "n_estimators": 200, - "min_samples_split": 2, - "n_jobs": -1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "BaggingClassifier_25", - BaggingClassifier, - { - "n_estimators": 25, - "base_estimator": DecisionTreeClassifier(min_samples_split=0.001), - "n_jobs": -1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "BaggingClassifier_50", - BaggingClassifier, - { - "n_estimators": 50, - "base_estimator": DecisionTreeClassifier(min_samples_split=0.001), - "n_jobs": -1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "BaggingClassifier_100", - BaggingClassifier, - { - "n_estimators": 100, - "base_estimator": DecisionTreeClassifier(min_samples_split=0.001), - "n_jobs": -1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "ExtraTreesClassifier_10", - ExtraTreesClassifier, - { - "n_estimators": 10, - "min_samples_split": 2, - "n_jobs": -1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "ExtraTreesClassifier_25", - ExtraTreesClassifier, - { - "n_estimators": 25, - "min_samples_split": 2, - "n_jobs": -1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "ExtraTreesClassifier_50", - ExtraTreesClassifier, - { - "n_estimators": 50, - "min_samples_split": 2, - "n_jobs": -1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "ExtraTreesClassifier_200", - ExtraTreesClassifier, - { - "n_estimators": 200, - "min_samples_split": 2, - "n_jobs": -1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ], - "mid": [ - ("LogisticRegression", LogisticRegression, {"max_iter": 150}), - ( - "DecisionTreeClassifier", - DecisionTreeClassifier, - {"min_samples_split": 0.001}, - ), - ("GaussianNB", GaussianNB, {}), - ("LinearDiscriminantAnalysis", LinearDiscriminantAnalysis, {}), - ("AdaBoostClassifier_50", AdaBoostClassifier, {}), - ( - "GradientBoostingClassifier_100", - GradientBoostingClassifier, - {"min_samples_split": 0.003}, - ), - ( - "HistGradientBoostingClassifier_50", - HistGradientBoostingClassifier, - {"max_iter": 50}, - ), - ("HistGradientBoostingClassifier", HistGradientBoostingClassifier, {}), - ( - "HistGradientBoostingClassifier_150", - HistGradientBoostingClassifier, - {"max_iter": 150}, - ), - ( - "RandomForestClassifier_100", - RandomForestClassifier, - {"min_samples_split": 0.003, "n_jobs": 4, "verbose": _SKLEARN_VERBOSE}, - ), - ( - "BaggingClassifier_10", - BaggingClassifier, - { - "base_estimator": DecisionTreeClassifier(min_samples_split=0.001), - "n_jobs": 4, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "ExtraTreesClassifier_100", - ExtraTreesClassifier, - {"min_samples_split": 0.003, "n_jobs": 4, "verbose": _SKLEARN_VERBOSE}, - ), - ("AdaBoostClassifier_25", AdaBoostClassifier, {"n_estimators": 25}), - ("AdaBoostClassifier_100", AdaBoostClassifier, {"n_estimators": 100}), - ( - "GradientBoostingClassifier_10", - GradientBoostingClassifier, - {"n_estimators": 10, "min_samples_split": 0.003}, - ), - ( - "GradientBoostingClassifier_25", - GradientBoostingClassifier, - {"n_estimators": 25, "min_samples_split": 0.003}, - ), - ( - "GradientBoostingClassifier_50", - GradientBoostingClassifier, - {"n_estimators": 50, "min_samples_split": 0.003}, - ), - ( - "RandomForestClassifier_10", - RandomForestClassifier, - { - "n_estimators": 10, - "min_samples_split": 0.003, - "n_jobs": 4, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "RandomForestClassifier_25", - RandomForestClassifier, - { - "n_estimators": 25, - "min_samples_split": 0.003, - "n_jobs": 4, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "RandomForestClassifier_50", - RandomForestClassifier, - { - "n_estimators": 50, - "min_samples_split": 0.003, - "n_jobs": 4, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "BaggingClassifier_25", - BaggingClassifier, - { - "n_estimators": 25, - "base_estimator": DecisionTreeClassifier(min_samples_split=0.001), - "n_jobs": 4, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "BaggingClassifier_50", - BaggingClassifier, - { - "n_estimators": 50, - "base_estimator": DecisionTreeClassifier(min_samples_split=0.001), - "n_jobs": 4, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "BaggingClassifier_100", - BaggingClassifier, - { - "n_estimators": 100, - "base_estimator": DecisionTreeClassifier(min_samples_split=0.001), - "n_jobs": 4, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "ExtraTreesClassifier_10", - ExtraTreesClassifier, - { - "n_estimators": 10, - "min_samples_split": 0.003, - "n_jobs": 4, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "ExtraTreesClassifier_25", - ExtraTreesClassifier, - { - "n_estimators": 25, - "min_samples_split": 0.003, - "n_jobs": 4, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "ExtraTreesClassifier_50", - ExtraTreesClassifier, - { - "n_estimators": 50, - "min_samples_split": 0.003, - "n_jobs": 4, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ], - "large": [ - ("LogisticRegression", LogisticRegression, {"max_iter": 250}), - ("GaussianNB", GaussianNB, {}), - ("LinearDiscriminantAnalysis", LinearDiscriminantAnalysis, {}), - ("HistGradientBoostingClassifier", HistGradientBoostingClassifier, {}), - ( - "HistGradientBoostingClassifier_200", - HistGradientBoostingClassifier, - {"max_iter": 200}, - ), - ( - "HistGradientBoostingClassifier_50", - HistGradientBoostingClassifier, - {"max_iter": 50}, - ), - ( - "RandomForestClassifier_100", - RandomForestClassifier, - {"min_samples_split": 0.003, "n_jobs": 1, "verbose": _SKLEARN_VERBOSE}, - ), - ( - "BaggingClassifier_10", - BaggingClassifier, - { - "base_estimator": DecisionTreeClassifier(min_samples_split=0.001), - "n_jobs": 1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "ExtraTreesClassifier_100", - ExtraTreesClassifier, - {"min_samples_split": 0.003, "n_jobs": 1, "verbose": _SKLEARN_VERBOSE}, - ), - ("AdaBoostClassifier_200", AdaBoostClassifier, {"n_estimators": 200}), - ( - "RandomForestClassifier_10", - RandomForestClassifier, - { - "n_estimators": 10, - "min_samples_split": 0.003, - "n_jobs": 1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "RandomForestClassifier_25", - RandomForestClassifier, - { - "n_estimators": 25, - "min_samples_split": 0.003, - "n_jobs": 1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "RandomForestClassifier_50", - RandomForestClassifier, - { - "n_estimators": 50, - "min_samples_split": 0.003, - "n_jobs": 1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "BaggingClassifier_25", - BaggingClassifier, - { - "n_estimators": 25, - "base_estimator": DecisionTreeClassifier(min_samples_split=0.001), - "n_jobs": 1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "ExtraTreesClassifier_10", - ExtraTreesClassifier, - { - "n_estimators": 10, - "min_samples_split": 0.003, - "n_jobs": 1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "ExtraTreesClassifier_25", - ExtraTreesClassifier, - { - "n_estimators": 25, - "min_samples_split": 0.003, - "n_jobs": 1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ( - "ExtraTreesClassifier_50", - ExtraTreesClassifier, - { - "n_estimators": 50, - "min_samples_split": 0.003, - "n_jobs": 1, - "verbose": _SKLEARN_VERBOSE, - }, - ), - ], - "x-large": [ - ("HistGradientBoostingClassifier_100", HistGradientBoostingClassifier, {}), - ( - "HistGradientBoostingClassifier_200", - HistGradientBoostingClassifier, - {"max_iter": 200}, - ), - ( - "HistGradientBoostingClassifier_50", - HistGradientBoostingClassifier, - {"max_iter": 50}, - ), - ( - "RandomForestClassifier_100", - RandomForestClassifier, - {"min_samples_split": 0.003, "n_jobs": 1, "verbose": _SKLEARN_VERBOSE}, - ), - ], - }, -} - - -class SuperLearner(Learner, ONNXConvertible): - """ - Tabular learner which employs StackingModel for construction of meta estimator. - """ - - def __init__( - self, - task: str, - base_estimators: Optional[List[Tuple[str, Callable, Dict]]] = None, - base_score_threshold: Optional[float] = None, - cv: Any = None, - filter_estimators: Optional[bool] = None, - dataset_size: Optional[Tuple[int, ...]] = None, - **kwargs: Any, - ) -> None: - """ - Constructs a meta model which is trained on cross-validated predictions of base estimators. - - Parameters - ---------- - task : str - `tabular_classification` or `tabular_regression` - base_estimators : Optional[List[Tuple[str, Callable, Dict]]], optional - list of base estimators, by default None - base_score_threshold : Optional[float], optional - threshold for filtering of the estimators, by default None - cv : Any, optional - number of CV folds or CV custom object, by default None - filter_estimators : Optional[bool], optional - when True, the perfomance of the estimators pre-estimated on the subset of training, estimators with the performance below the threshold will not be used for meta model construction, by default None - dataset_size : Optional[Tuple[int]], optional - size of the dataset, by default None - """ - - if task not in ["tabular_classification", "tabular_regression"]: - raise ValueError( - f"Invalid task type. Expected `tabular_classification` or `tabular_regression`, found `{task}`." - ) - - self.base_estimators = base_estimators - self.dataset_size: Optional[Tuple[int, ...]] = dataset_size - self.task = task - self.base_score_threshold = base_score_threshold - self.cv = cv - self.filter_estimators = filter_estimators - - def _split( - self, X: npt.NDArray, y: npt.NDArray - ) -> Tuple[Float32Array, Float32Array, Float32Array, Float32Array]: - X_train: Float32Array - y_train: Float32Array - X_val: Float32Array - y_val: Float32Array - - if self.task == "tabular_classification": - X_train, X_val, y_train, y_val = train_test_split(X, y, stratify=y) - X_train_upsampled: Float32Array - y_train_upsampled: Float32Array - X_train_upsampled, y_train_upsampled = RandomOverSampler().fit_resample( - X_train, y_train - ) - return X_train_upsampled, X_val, y_train_upsampled, y_val - X_train, X_val, y_train, y_val = train_test_split(X, y, stratify=None) - return X_train, X_val, y_train, y_val - - def _calculate_base_score(self, y_hat: Float32Array, y: Float32Array) -> float: - if self.task == "tabular_classification": - return balanced_accuracy_score(y, y_hat) - else: - return (r2_score(y, y_hat) + 1) / 2 - - def _set_size_optimized_config(self, X: Float32Array) -> None: - if self.dataset_size is None: - self.dataset_size = X.shape - volume = self.dataset_size[0] * self.dataset_size[1] - - min_threshold = 80_000 # 5_000 samples with 16 features - mid_threshold = 4_000_000 # 125_000 samples with 32 featrues / 250_000 samples with 16 features - large_threshold = 16_000_000 # 1_000_000 samples with 16 features - - if volume < min_threshold: - print_("Setting up learner config [small dataset]") - cv = 10 - base_estimators = _default_estimators[self.task]["mini"] - filter_estimators = True - elif volume < mid_threshold: - print_("Setting up learner config [mid dataset]") - cv = 5 - base_estimators = _default_estimators[self.task]["mid"] - filter_estimators = True - elif volume < large_threshold: - print_("Setting up learner config [large dataset]") - base_estimators = _default_estimators[self.task]["large"] - cv = 3 - filter_estimators = False - else: - print_("Setting up learner config [x-large dataset]") - base_estimators = _default_estimators[self.task]["x-large"] - cv = 3 - filter_estimators = False - - if self.cv is None: - self.cv = cv - if self.base_estimators is None: - self.base_estimators = base_estimators - if self.filter_estimators is None: - self.filter_estimators = filter_estimators - - def _preselect( - self, X: Float32Array, y: Float32Array - ) -> List[ - Tuple[str, Callable] - ]: # select estimators to be used in the main training loop - if self.base_estimators is None: - raise ValueError("expected base_estimators to be a list, found None") - selected_estimators: List[Tuple[str, Callable]] = [] - if not self.filter_estimators: - print( - "\t -> Skipping filtering of base classifiers => all estimators will be used for final model" - ) - selected_estimators = [ - (estimator[0], estimator[1](**estimator[2])) - for estimator in self.base_estimators - ] - return selected_estimators - print_(f"\t -> Filtering base classifiers:") - if self.base_score_threshold is None: - if self.task == "tabular_classification": - n_classes = len(np.unique(y, return_counts=False)) - baseline = 1 / n_classes - self.base_score_threshold = 1.1 * baseline - print_( - f"\t Using {self.base_score_threshold} as baseline score for {n_classes} classes classification task" - ) - else: - self.base_score_threshold = 0.55 - X_train: Float32Array - y_train: Float32Array - X_val: Float32Array - y_val: Float32Array - X_train, X_val, y_train, y_val = self._split(X, y) - for estimator in self.base_estimators: - - est: SklearnBaseEstimator = estimator[1](**estimator[2]) - est.fit(X_train, y_train) - y_hat: Float32Array = est.predict(X_val) - base_score: float = self._calculate_base_score(y_hat, y_val) - print_(f"\t\t-> {estimator[0]} base score: {base_score}") - if base_score >= self.base_score_threshold: - selected_estimators.append((estimator[0], estimator[1](**estimator[2]))) - if len(selected_estimators) < 3: # using all estimators - for estimator in self.base_estimators: - selected_estimators = [] - selected_estimators.append((estimator[0], estimator[1](**estimator[2]))) - return selected_estimators - - def fit(self, X: Float32Array, y: Float32Array, *args: Any, **kwargs: Any) -> None: - """ - Fits the model. The hyperparameters that were not passed to the `__init__` will be automatically determined based on the size of the training set. - For classification tasks, the dataset will be balanced by upsampling the minority class(es). - - Parameters - ---------- - X : Float32Array - features - y : Float32Array - targets - """ - print_("Fitting stacked model... ") - self._set_size_optimized_config(X) - estimators: List[Tuple[str, Callable]] = self._preselect(X, y) - print_(f"\t -> Fitting the final estimator") - stacked_estimator: Union[StackingClassifier, StackingRegressor] - if self.task == "tabular_classification": - stacked_estimator = StackingClassifier( - estimators=estimators, final_estimator=LogisticRegression(), cv=self.cv - ) - else: - stacked_estimator = StackingRegressor( - estimators=estimators, final_estimator=LinearRegression(), cv=self.cv - ) - - stacked_estimator.fit(X, y) - self.model = stacked_estimator - - def predict( - self, X: Float32Array, *args: Any, **kwargs: Any - ) -> Union[Float32Array, Int64Array]: - """ - Makes a prediction for given X. - - Parameters - ---------- - X : Float32Array - features - - Returns - ------- - Union[Float32Array, Int64Array] - predictions - """ - return self.model.predict(X) - - def get_input_type(self) -> Type: - """ - Returns - ------- - Type - Float32Array - """ - return Float32Array - - def get_output_type(self) -> Type: - """ - Returns - ------- - Type - Float32Array for regression, Int64Array for classification - """ - return Float32Array if self.task == "tabular_regression" else Int64Array - - def forward( - self, X: Float32Array, *args: Any, **kwargs: Any - ) -> Union[Float32Array, Int64Array]: - """ - Equivalen to `.predict(X)` - - Parameters - ---------- - X : Float32Array - features - - Returns - ------- - Union[Float32Array, Int64Array] - predictions - """ - return self.model.predict(X) - - def fit_pipe( - self, X: Float32Array, y: Float32Array, *args: Any, **kwargs: Any - ) -> None: - """ - Equivalent to `.fit(X, y)` - - Parameters - ---------- - X : Float32Array - features - y : Float32Array - targets - """ - self.fit(X, y) - - def to_onnx(self) -> SerializedModelRepr: - """ - Serializes the underlying model to onnx by calling its `.to_onnx()` method. - - Returns - ------- - SerializedModelRepr - """ - return self.model.to_onnx() diff --git a/falcon/tabular/models/__init__.py b/falcon/tabular/models/__init__.py index 40e28de..c9c2ef6 100644 --- a/falcon/tabular/models/__init__.py +++ b/falcon/tabular/models/__init__.py @@ -1,2 +1 @@ -from falcon.tabular.models.stacking import StackingClassifier, StackingRegressor -from falcon.tabular.models.hist_gbt import HistGradientBoostingClassifier, HistGradientBoostingRegressor +__all__: list[str] = [] diff --git a/falcon/tabular/models/gbdt.py b/falcon/tabular/models/gbdt.py new file mode 100644 index 0000000..5f7e6d2 --- /dev/null +++ b/falcon/tabular/models/gbdt.py @@ -0,0 +1,397 @@ +from __future__ import annotations + +from dataclasses import dataclass +from importlib import import_module +from pathlib import Path +from tempfile import TemporaryDirectory +from typing import Any, Protocol + +import numpy as np +import onnx +from numpy import typing as npt +from onnx import TensorProto, helper + +from falcon.config import ONNX_OPSET_VERSION +from falcon.constants import TABULAR_CLASSIFICATION_TASK, TABULAR_REGRESSION_TASK +from falcon.serialization import SerializedModelRepr +from falcon.utils import logger + +_ONNXMLTOOLS_TARGET_OPSET = 15 + + +class GBDTModel(Protocol): + def fit( + self, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + *, + sample_weight: npt.NDArray[np.float64] | None = None, + validation_data: tuple[npt.NDArray[Any], npt.NDArray[Any]] | None = None, + early_stopping_rounds: int | None = None, + ) -> None: ... + + def predict(self, X: npt.NDArray[Any]) -> npt.NDArray[Any]: ... + + def serialize(self) -> SerializedModelRepr: ... + + +class GBDTClassifierModel(GBDTModel, Protocol): + def predict_proba(self, X: npt.NDArray[Any]) -> npt.NDArray[Any]: ... + + +class _OnnxmltoolsGBDT: + def __init__( + self, + estimator: Any, + converter_name: str, + prediction_dtype: npt.DTypeLike, + ) -> None: + self.estimator = estimator + self._converter_name = converter_name + self._prediction_dtype = prediction_dtype + self._shape: list[int | None] | None = None + + def fit( + self, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + *, + sample_weight: npt.NDArray[np.float64] | None = None, + validation_data: tuple[npt.NDArray[Any], npt.NDArray[Any]] | None = None, + early_stopping_rounds: int | None = None, + ) -> None: + self._shape = [None, *X.shape[1:]] + fit_options: dict[str, Any] = {} + if sample_weight is not None: + fit_options["sample_weight"] = sample_weight + if validation_data is not None: + if early_stopping_rounds is None: + raise ValueError( + "early_stopping_rounds is required with validation_data" + ) + fit_options["eval_set"] = [validation_data] + if self._converter_name == "convert_lightgbm": + fit_options["callbacks"] = [ + import_module("lightgbm").early_stopping( + early_stopping_rounds, + verbose=False, + ) + ] + else: + self.estimator.set_params(early_stopping_rounds=early_stopping_rounds) + fit_options["verbose"] = False + elif early_stopping_rounds is not None: + raise ValueError("validation_data is required for early stopping") + self.estimator.fit(X, y, **fit_options) + + def predict(self, X: npt.NDArray[Any]) -> npt.NDArray[Any]: + return np.asarray(self.estimator.predict(X), dtype=self._prediction_dtype) + + def serialize(self) -> SerializedModelRepr: + if self._shape is None: + raise RuntimeError("The model must be fitted before it can be serialized") + + converter = getattr(import_module("onnxmltools"), self._converter_name) + float_tensor_type = import_module( + "onnxmltools.convert.common.data_types" + ).FloatTensorType + conversion_options: dict[str, object] = { + "initial_types": [("model_input", float_tensor_type(self._shape))], + # onnxmltools currently rejects newer core opsets even though these + # converters emit only older, compatible operators. + "target_opset": _ONNXMLTOOLS_TARGET_OPSET, + } + if self._converter_name == "convert_lightgbm": + conversion_options["zipmap"] = False + model = converter(self.estimator, **conversion_options) + return SerializedModelRepr( + model, + len(model.graph.input), + len(model.graph.output), + ["FLOAT32"], + [self._shape], + ) + + +class _OnnxmltoolsClassifier(_OnnxmltoolsGBDT): + def predict_proba(self, X: npt.NDArray[Any]) -> npt.NDArray[Any]: + return np.asarray(self.estimator.predict_proba(X), dtype=np.float32) + + def serialize(self) -> SerializedModelRepr: + serialized = super().serialize() + label_shape = serialized.get_model().graph.output[0].type.tensor_type.shape + if label_shape.dim and label_shape.dim[0].dim_value == 1: + label_shape.dim[0].ClearField("dim_value") + return serialized + + +class LightGBMClassifier(_OnnxmltoolsClassifier): + def __init__(self, random_state: int = 42, **parameters: Any) -> None: + defaults: dict[str, Any] = { + "n_jobs": 1, + "random_state": random_state, + "verbosity": -1, + } + defaults.update(parameters) + estimator = import_module("lightgbm").LGBMClassifier(**defaults) + super().__init__(estimator, "convert_lightgbm", np.int64) + + +class LightGBMRegressor(_OnnxmltoolsGBDT): + def __init__(self, random_state: int = 42, **parameters: Any) -> None: + defaults: dict[str, Any] = { + "n_jobs": 1, + "random_state": random_state, + "verbosity": -1, + } + defaults.update(parameters) + estimator = import_module("lightgbm").LGBMRegressor(**defaults) + super().__init__(estimator, "convert_lightgbm", np.float32) + + +class XGBoostClassifier(_OnnxmltoolsClassifier): + def __init__(self, random_state: int = 42, **parameters: Any) -> None: + defaults: dict[str, Any] = { + "n_jobs": 1, + "random_state": random_state, + "verbosity": 0, + } + defaults.update(parameters) + estimator = import_module("xgboost").XGBClassifier(**defaults) + super().__init__(estimator, "convert_xgboost", np.int64) + + +class XGBoostRegressor(_OnnxmltoolsGBDT): + def __init__(self, random_state: int = 42, **parameters: Any) -> None: + defaults: dict[str, Any] = { + "n_jobs": 1, + "random_state": random_state, + "verbosity": 0, + } + defaults.update(parameters) + estimator = import_module("xgboost").XGBRegressor(**defaults) + super().__init__(estimator, "convert_xgboost", np.float32) + + +class _CatBoostGBDT: + def __init__(self, estimator: Any, prediction_dtype: npt.DTypeLike) -> None: + self.estimator = estimator + self._prediction_dtype = prediction_dtype + self._shape: list[int | None] | None = None + + def fit( + self, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + *, + sample_weight: npt.NDArray[np.float64] | None = None, + validation_data: tuple[npt.NDArray[Any], npt.NDArray[Any]] | None = None, + early_stopping_rounds: int | None = None, + ) -> None: + self._shape = [None, *X.shape[1:]] + fit_options: dict[str, Any] = {} + if sample_weight is not None: + fit_options["sample_weight"] = sample_weight + if validation_data is not None: + if early_stopping_rounds is None: + raise ValueError( + "early_stopping_rounds is required with validation_data" + ) + fit_options.update( + { + "early_stopping_rounds": early_stopping_rounds, + "eval_set": validation_data, + "use_best_model": True, + } + ) + elif early_stopping_rounds is not None: + raise ValueError("validation_data is required for early stopping") + self.estimator.fit(X, y, **fit_options) + + def predict(self, X: npt.NDArray[Any]) -> npt.NDArray[Any]: + return np.asarray( + self.estimator.predict(X), dtype=self._prediction_dtype + ).reshape(-1) + + def serialize(self) -> SerializedModelRepr: + if self._shape is None: + raise RuntimeError("The model must be fitted before it can be serialized") + + with TemporaryDirectory(prefix="falcon-catboost-") as directory: + model_path = Path(directory) / "model.onnx" + self.estimator.save_model(str(model_path), format="onnx") + model = onnx.load(model_path) + return SerializedModelRepr( + model, + len(model.graph.input), + len(model.graph.output), + ["FLOAT32"], + [self._shape], + ) + + +class CatBoostClassifier(_CatBoostGBDT): + def __init__(self, random_state: int = 42, **parameters: Any) -> None: + defaults: dict[str, Any] = { + "allow_writing_files": False, + "random_seed": random_state, + "thread_count": 1, + "verbose": False, + } + defaults.update(parameters) + estimator = import_module("catboost").CatBoostClassifier(**defaults) + super().__init__(estimator, np.int64) + self._class_count: int | None = None + + def fit( + self, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + *, + sample_weight: npt.NDArray[np.float64] | None = None, + validation_data: tuple[npt.NDArray[Any], npt.NDArray[Any]] | None = None, + early_stopping_rounds: int | None = None, + ) -> None: + self._class_count = len(np.unique(y)) + if self._class_count > 2: + raise ValueError( + "CatBoost multiclass models are disabled because ONNX export parity " + "is not established" + ) + super().fit( + X, + y, + sample_weight=sample_weight, + validation_data=validation_data, + early_stopping_rounds=early_stopping_rounds, + ) + + def predict(self, X: npt.NDArray[Any]) -> npt.NDArray[Any]: + return np.argmax(self.predict_proba(X), axis=1).astype(np.int64) + + def predict_proba(self, X: npt.NDArray[Any]) -> npt.NDArray[Any]: + return np.asarray(self.estimator.predict_proba(X), dtype=np.float32) + + def serialize(self) -> SerializedModelRepr: + serialized = super().serialize() + model = serialized.get_model() + zipmap = next( + (node for node in model.graph.node if node.op_type == "ZipMap"), None + ) + if zipmap is None or self._class_count is None: + raise RuntimeError( + "CatBoost did not export the expected probability output" + ) + + probability_name = zipmap.input[0] + model.graph.node.remove(zipmap) + label_name = "falcon_argmax_label" + model.graph.node.append( + helper.make_node( + "ArgMax", + [probability_name], + [label_name], + axis=1, + keepdims=0, + name="falcon_catboost_argmax", + ) + ) + del model.graph.output[:] + model.graph.output.extend( + [ + helper.make_tensor_value_info(label_name, TensorProto.INT64, [None]), + helper.make_tensor_value_info( + probability_name, + TensorProto.FLOAT, + [None, self._class_count], + ), + ] + ) + if not any(opset.domain in {"", "ai.onnx"} for opset in model.opset_import): + model.opset_import.append(helper.make_operatorsetid("", ONNX_OPSET_VERSION)) + return serialized + + +class CatBoostRegressor(_CatBoostGBDT): + def __init__(self, random_state: int = 42, **parameters: Any) -> None: + defaults: dict[str, Any] = { + "allow_writing_files": False, + "random_seed": random_state, + "thread_count": 1, + "verbose": False, + } + defaults.update(parameters) + estimator = import_module("catboost").CatBoostRegressor(**defaults) + super().__init__(estimator, np.float32) + + +@dataclass(frozen=True) +class GBDTModelFamily: + classifier: type[GBDTClassifierModel] + regressor: type[GBDTModel] + + +def _module_is_available(module_name: str) -> bool: + try: + import_module(module_name) + except ImportError: + return False + return True + + +def _discover_gbdt_families() -> dict[str, GBDTModelFamily]: + families: dict[str, GBDTModelFamily] = {} + if _module_is_available("onnxmltools") and _module_is_available("lightgbm"): + families["lightgbm"] = GBDTModelFamily( + LightGBMClassifier, + LightGBMRegressor, + ) + if _module_is_available("onnxmltools") and _module_is_available("xgboost"): + families["xgboost"] = GBDTModelFamily( + XGBoostClassifier, + XGBoostRegressor, + ) + if _module_is_available("catboost"): + families["catboost"] = GBDTModelFamily( + CatBoostClassifier, + CatBoostRegressor, + ) + return families + + +_GBDT_FAMILIES = _discover_gbdt_families() + + +def get_gbdt_model_classes( + task: str, + *, + n_classes: int | None = None, +) -> dict[str, type[GBDTModel]]: + if task == TABULAR_CLASSIFICATION_TASK: + classes: dict[str, type[GBDTModel]] = { + name: family.classifier for name, family in _GBDT_FAMILIES.items() + } + if n_classes is not None and n_classes > 2 and "catboost" in classes: + del classes["catboost"] + logger.info( + "CatBoost is excluded from multiclass classification because ONNX " + "export parity is not established." + ) + return classes + if task == TABULAR_REGRESSION_TASK: + return {name: family.regressor for name, family in _GBDT_FAMILIES.items()} + raise ValueError(f"Unknown task `{task}`") + + +__all__ = [ + "CatBoostClassifier", + "CatBoostRegressor", + "GBDTClassifierModel", + "GBDTModel", + "GBDTModelFamily", + "LightGBMClassifier", + "LightGBMRegressor", + "XGBoostClassifier", + "XGBoostRegressor", + "get_gbdt_model_classes", +] diff --git a/falcon/tabular/models/hist_gbt.py b/falcon/tabular/models/hist_gbt.py deleted file mode 100644 index aaf2f56..0000000 --- a/falcon/tabular/models/hist_gbt.py +++ /dev/null @@ -1,148 +0,0 @@ -from sklearn.ensemble import ( - HistGradientBoostingClassifier as SklearnHistGradientBoostingClassifier, - HistGradientBoostingRegressor as SklearnHistGradientBoostingRegressor, -) -from typing import Callable, Dict, Any, Union -from falcon.abstract import Model, ONNXConvertible -from skl2onnx import convert_sklearn -from skl2onnx.common.data_types import TensorType, FloatTensorType -from falcon.types import Float32Array, Int64Array -from falcon.serialization import SerializedModelRepr -from falcon.config import ONNX_OPSET_VERSION, ML_ONNX_OPSET_VERSION -from falcon.abstract.optuna import OptunaMixin -from numpy import typing as npt - -class _BaseHistGradientBoosting(Model, ONNXConvertible, OptunaMixin): - def __init__(self, estimator: Union[SklearnHistGradientBoostingClassifier, SklearnHistGradientBoostingRegressor], **kwargs: Any): - self.estimator = estimator - - def fit(self, X: Float32Array, y: Float32Array, *args: Any, **kwargs: Any) -> None: - """ - Fits the model - - Parameters - ---------- - X : Float32Array - Features - y : Float32Array - targets - """ - self._shape = [None, *X.shape[1:]] - self.estimator.fit(X, y) - - def to_onnx(self) -> SerializedModelRepr: - """ - Serializes the model to onnx. - - Returns - ------- - SerializedModelRepr - """ - initial_type = [("model_input", FloatTensorType(self._shape))] - options = self._get_onnx_options() - onnx_model = convert_sklearn( - self.estimator, - initial_types=initial_type, - target_opset={'': ONNX_OPSET_VERSION, 'ai.onnx.ml': ML_ONNX_OPSET_VERSION}, - options=options, - ) - n_inputs = len(onnx_model.graph.input) - n_outputs = len(onnx_model.graph.output) - - return SerializedModelRepr( - onnx_model, - n_inputs, - n_outputs, - ["FLOAT32"], - [self._shape] - ) - - def _get_onnx_options(self) -> Dict: - return {} - - def predict(self, X: npt.NDArray, *args: Any, **kwargs: Any) -> npt.NDArray: - return self.estimator.predict(X) - - @classmethod - def get_search_space(cls, X: npt.NDArray, y: npt.NDArray) -> Union[Callable, Dict]: - return { - "max_iter" : { - "type": "int", - "kwargs": { - "low": 50, - "high": 500 - } - }, - "min_samples_leaf" : { - "type": "int", - "kwargs": { - "low": 5, - "high": 20, - "step": 5 - } - }, - "learning_rate" : { - "type": "float", - "kwargs": { - "low": 0.001, - "high": 1., - "log": True - } - }, - "l2_regularization": { - "type": "float", - "kwargs": { - "low": 1e-7, - "high": 0.01, - "log": True - } - } - } - -class HistGradientBoostingRegressor(_BaseHistGradientBoosting): - """ - Wrapper around `sklearn.ensemble.HistGradientBoostingRegressor`. - """ - def __init__(self, max_iter: int = 100, min_samples_leaf: int = 20, learning_rate: float = 0.1, l2_regularization: float = 0., random_seed: int = 42, **kwargs: Any): - """ - - Parameters - ---------- - max_iter : int, optional - number of decision trees, by default 100 - min_samples_leaf : int, optional - minimum number of samples per leaf, by default 20 - learning_rate : float, optional - learning rate, by default 0.1 - l2_regularization : float, optional - L2 regularization parameter, by default 0.0 - random_seed : int, optional - by default 42 - """ - estimator = SklearnHistGradientBoostingRegressor(max_iter = max_iter, learning_rate=learning_rate, l2_regularization=l2_regularization, min_samples_leaf=min_samples_leaf, random_state=random_seed) - super().__init__(estimator=estimator) - - - -class HistGradientBoostingClassifier(_BaseHistGradientBoosting): - """ - Wrapper around `sklearn.ensemble.HistGradientBoostingClassifier`. - """ - def __init__(self, max_iter: int = 100, min_samples_leaf: int = 20, learning_rate: float = 0.1, l2_regularization: float = 0., random_seed: int = 42, **kwargs: Any): - """ - - Parameters - ---------- - max_iter : int, optional - number of decision trees, by default 100 - min_samples_leaf : int, optional - minimum number of samples per leaf, by default 20 - learning_rate : float, optional - learning rate, by default 0.1 - l2_regularization : float, optional - L2 regularization parameter, by default 0.0 - random_seed : int, optional - by default 42 - """ - estimator = SklearnHistGradientBoostingClassifier(max_iter = max_iter, learning_rate=learning_rate, l2_regularization=l2_regularization, min_samples_leaf=min_samples_leaf, random_state=random_seed) - super().__init__(estimator=estimator) \ No newline at end of file diff --git a/falcon/tabular/models/sklearn_model.py b/falcon/tabular/models/sklearn_model.py new file mode 100644 index 0000000..9dc7754 --- /dev/null +++ b/falcon/tabular/models/sklearn_model.py @@ -0,0 +1,144 @@ +from typing import Any + +import numpy as np +from numpy import typing as npt +from skl2onnx import convert_sklearn +from skl2onnx.common.data_types import FloatTensorType +from sklearn.ensemble import ( + HistGradientBoostingClassifier, + HistGradientBoostingRegressor, +) +from sklearn.metrics import log_loss, mean_squared_error + +from falcon.config import ML_ONNX_OPSET_VERSION, ONNX_OPSET_VERSION +from falcon.constants import TABULAR_CLASSIFICATION_TASK, TABULAR_REGRESSION_TASK +from falcon.serialization import SerializedModelRepr + + +class SklearnModel: + def __init__(self, estimator: Any, task: str) -> None: + if task not in {TABULAR_CLASSIFICATION_TASK, TABULAR_REGRESSION_TASK}: + raise ValueError(f"Unknown task `{task}`") + self.estimator = estimator + self.task = task + self._shape: list[int | None] | None = None + + def _fit_estimator( + self, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + sample_weight: npt.NDArray[np.float64] | None, + ) -> None: + if sample_weight is None: + self.estimator.fit(X, y) + else: + self.estimator.fit(X, y, sample_weight=sample_weight) + + def fit( + self, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + *, + sample_weight: npt.NDArray[np.float64] | None = None, + validation_data: tuple[npt.NDArray[Any], npt.NDArray[Any]] | None = None, + early_stopping_rounds: int | None = None, + ) -> None: + self._shape = [None, *X.shape[1:]] + if validation_data is None and early_stopping_rounds is None: + self._fit_estimator(X, y, sample_weight) + return + if validation_data is None or early_stopping_rounds is None: + raise ValueError( + "validation_data and early_stopping_rounds must be provided together" + ) + if not isinstance( + self.estimator, + (HistGradientBoostingClassifier, HistGradientBoostingRegressor), + ): + raise ValueError( + "This sklearn estimator does not support external early stopping" + ) + self._fit_hist_gradient_boosting( + X, + y, + sample_weight, + validation_data, + early_stopping_rounds, + ) + + def _fit_hist_gradient_boosting( + self, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + sample_weight: npt.NDArray[np.float64] | None, + validation_data: tuple[npt.NDArray[Any], npt.NDArray[Any]], + early_stopping_rounds: int, + ) -> None: + validation_X, validation_y = validation_data + max_iter = int(self.estimator.max_iter) + self.estimator.set_params(warm_start=True) + best_iteration = 1 + best_loss = float("inf") + iterations_without_improvement = 0 + trained_iterations = 0 + for iteration in range(1, max_iter + 1): + self.estimator.set_params(max_iter=iteration) + self._fit_estimator(X, y, sample_weight) + trained_iterations = iteration + if self.task == TABULAR_CLASSIFICATION_TASK: + loss = log_loss( + validation_y, + self.estimator.predict_proba(validation_X), + labels=self.estimator.classes_, + ) + else: + loss = mean_squared_error( + validation_y, + self.estimator.predict(validation_X), + ) + if loss < best_loss - float(self.estimator.tol): + best_loss = loss + best_iteration = iteration + iterations_without_improvement = 0 + else: + iterations_without_improvement += 1 + if iterations_without_improvement >= early_stopping_rounds: + break + + if best_iteration < trained_iterations: + self.estimator.set_params(max_iter=best_iteration, warm_start=False) + self._fit_estimator(X, y, sample_weight) + else: + self.estimator.set_params(warm_start=False) + + def predict(self, X: npt.NDArray[Any]) -> npt.NDArray[Any]: + dtype = np.int64 if self.task == TABULAR_CLASSIFICATION_TASK else np.float32 + return np.asarray(self.estimator.predict(X), dtype=dtype).reshape(-1) + + def predict_proba(self, X: npt.NDArray[Any]) -> npt.NDArray[np.float32]: + if self.task != TABULAR_CLASSIFICATION_TASK: + raise RuntimeError("Regression models do not expose probabilities") + return np.asarray(self.estimator.predict_proba(X), dtype=np.float32) + + def serialize(self) -> SerializedModelRepr: + if self._shape is None: + raise RuntimeError("The model must be fitted before it can be serialized") + options: dict[int, dict[str, bool]] = {} + if self.task == TABULAR_CLASSIFICATION_TASK: + options[id(self.estimator)] = {"zipmap": False} + model = convert_sklearn( + self.estimator, + initial_types=[("model_input", FloatTensorType(self._shape))], + target_opset={"": ONNX_OPSET_VERSION, "ai.onnx.ml": ML_ONNX_OPSET_VERSION}, + options=options, + ) + return SerializedModelRepr( + model, + len(model.graph.input), + len(model.graph.output), + ["FLOAT32"], + [self._shape], + ) + + +__all__ = ["SklearnModel"] diff --git a/falcon/tabular/models/stacking.py b/falcon/tabular/models/stacking.py deleted file mode 100644 index 2df9329..0000000 --- a/falcon/tabular/models/stacking.py +++ /dev/null @@ -1,227 +0,0 @@ -from sklearn.ensemble import ( - StackingRegressor as SklearnStackingRegressor, - StackingClassifier as SklearnStackingClassifier, -) -from falcon.abstract.onnx_convertible import ONNXConvertible -from falcon.addons.sklearn import ( - BalancedStackingClassifier as SklearnBalancedStackingClassifier, -) -from falcon.abstract import Model -from falcon.types import Float32Array -from sklearn.base import BaseEstimator -from typing import Any, List, Optional, Union, Dict, Tuple, Callable, Type -from numpy import typing as npt -import numpy as np -from skl2onnx import convert_sklearn -from skl2onnx.common.data_types import TensorType, FloatTensorType -from falcon.config import ONNX_OPSET_VERSION, ML_ONNX_OPSET_VERSION -from falcon.serialization import SerializedModelRepr - - -class _StackingBase(Model, ONNXConvertible): - def __init__( - self, - cls: Callable, - estimators: List[Tuple[str, BaseEstimator]], - final_estimator: BaseEstimator, - cv: Any = None, - n_jobs: int = 1, - passthrough: bool = False, - verbose: int = 0, - **kwargs: Any, - ) -> None: - self.estimator: BaseEstimator = cls( - estimators=estimators, - final_estimator=final_estimator, - cv=cv, - n_jobs=n_jobs, - passthrough=passthrough, - **kwargs, - ) - - self._ret_type: Type = ( - np.float32 - if isinstance(self.estimator, SklearnStackingRegressor) - else np.int64 - ) - - def fit(self, X: Float32Array, y: Float32Array, *args: Any, **kwargs: Any) -> None: - """ - Fits the model - - Parameters - ---------- - X : Float32Array - Features - y : Float32Array - targets - """ - self._shape = [None, *X.shape[1:]] - self.estimator.fit(X, y) - - def predict(self, X: Float32Array, *args: Any, **kwargs: Any) -> Float32Array: - """ - Predicts the target for the given input - - Parameters - ---------- - X : Float32Array - featrues - - Returns - ------- - Float32Array - predictions - """ - prediction: npt.NDArray = self.estimator.predict(X) - prediction = prediction.astype(dtype=self._ret_type) - return prediction - - def to_onnx(self) -> SerializedModelRepr: - """ - Serializes the model to onnx. - - Returns - ------- - SerializedModelRepr - """ - initial_type = [("model_input", FloatTensorType(self._shape))] - options = self._get_onnx_options() - onnx_model = convert_sklearn( - self.estimator, - initial_types=initial_type, - target_opset={'': ONNX_OPSET_VERSION, 'ai.onnx.ml': ML_ONNX_OPSET_VERSION}, - # final_types=self._get_onnx_final_types(), - options=options, - ) - n_inputs = len(onnx_model.graph.input) - n_outputs = len(onnx_model.graph.output) - - return SerializedModelRepr( - onnx_model, - n_inputs, - n_outputs, - ["FLOAT32"], - [self._shape], - ) - - # def _get_onnx_final_types(self) -> List[Tuple[str, TensorType]]: - # return [("stacking_output", FloatTensorType([None, 1]))] - - def _get_onnx_options(self) -> Dict: - return {} - - -class StackingClassifier(_StackingBase): - """ - Small wrapper around `sklearn.ensemble.StackingClassifier`. - """ - - def __init__( - self, - estimators: List[Tuple[str, BaseEstimator]], - final_estimator: BaseEstimator, - balanced: bool = True, - cv: Any = 5, - n_jobs: int = 1, - passthrough: bool = False, - verbose: int = 0, - stack_method: Any = "auto", - **kwargs: Any, - ) -> None: - """ - Small wrapper around `sklearn.ensemble.StackingClassifier`. - For more detailed description please refer to sklarn documentation. - - - Parameters - ---------- - estimators : List[Tuple[str, BaseEstimator]] - base estimators - final_estimator : BaseEstimator - meta estimator, by default LogisticRegression - balanced : bool, optional - if True, the classes are balanced by performing random oversampling, by default True - cv : Any, optional - number of CV folds, or custom CV object, by default 5 - n_jobs : int, optional - number of parallel jobs, by default -1 - passthrough : bool, optional - when True the meta estimator is trained on original data in addition to the predictions of base estimators, by default False - verbose : int, optional - verbosity level of underlying sklearn estimator, by default 0 - stack_method : Any, optional - methods called for each base estimator, by default "auto" - """ - cls: Callable - if balanced: - cls = SklearnBalancedStackingClassifier - else: - cls = SklearnStackingClassifier - super().__init__( - cls=cls, - estimators=estimators, - final_estimator=final_estimator, - cv=cv, - n_jobs=n_jobs, - passthrough=passthrough, - verbose=verbose, - stack_method=stack_method, - **kwargs, - ) - - # def _get_onnx_final_types( - # self, - # ) -> List[Tuple[str, TensorType]]: - # return [ - # ("stacking_labels", FloatTensorType([None])), - # ("stacking_probs", FloatTensorType([None, None])), - # ] - - def _get_onnx_options(self) -> Dict: - return {id(self.estimator): {"zipmap": False}} - - -class StackingRegressor(_StackingBase): - """ - Small wrapper around `sklearn.ensemble.StackingRegressor`. - """ - def __init__( - self, - estimators: List[Tuple[str, BaseEstimator]], - final_estimator: BaseEstimator, - cv: Any = 5, - n_jobs: int = 1, - passthrough: bool = False, - verbose: int = 0, - **kwargs: Any, - ) -> None: - """ - Small wrapper around `sklearn.ensemble.StackingRegressor`. - For more detailed description please refer to sklarn documentation. - - Parameters - ---------- - estimators : List[Tuple[str, BaseEstimator]] - base estimators - final_estimator : BaseEstimator - meta estimator - cv : Any, optional - number of CV folds, or custom CV object, by default 5 - n_jobs : int, optional - number of parallel jobs, by default -1 - passthrough : bool, optional - when True the meta estimator is trained on original data in addition to the predictions of base estimators, by default False - verbose : int, optional - verbosity level of underlying sklearn estimator, by default 0 - """ - super().__init__( - cls=SklearnStackingRegressor, - estimators=estimators, - final_estimator=final_estimator, - cv=cv, - n_jobs=n_jobs, - passthrough=passthrough, - verbose=verbose, - **kwargs, - ) diff --git a/falcon/tabular/pipelines/__init__.py b/falcon/tabular/pipelines/__init__.py index 4e960bb..0e10f71 100644 --- a/falcon/tabular/pipelines/__init__.py +++ b/falcon/tabular/pipelines/__init__.py @@ -1 +1,3 @@ from falcon.tabular.pipelines.simple_tabular_pipeline import SimpleTabularPipeline + +__all__ = ["SimpleTabularPipeline"] diff --git a/falcon/tabular/pipelines/simple_tabular_pipeline.py b/falcon/tabular/pipelines/simple_tabular_pipeline.py index 1c7e090..dbf524e 100644 --- a/falcon/tabular/pipelines/simple_tabular_pipeline.py +++ b/falcon/tabular/pipelines/simple_tabular_pipeline.py @@ -1,15 +1,13 @@ +from typing import Any + from numpy import typing as npt -import numpy.typing as npt -from typing import List, Any, Optional, Dict, Type, Tuple -from falcon.abstract import Pipeline, PipelineElement -from falcon.abstract.learner import Learner -from falcon.abstract.onnx_convertible import ONNXConvertible + +from falcon.abstract.task_pipeline import Pipeline, PipelineStep from falcon.tabular.processors.label_decoder import LabelDecoder -from falcon.tabular.processors.scaler_and_encoder import ScalerAndEncoder from falcon.tabular.processors.multi_modal_encoder import MultiModalEncoder -from falcon.tabular.learners.super_learner import SuperLearner -from falcon.types import ColumnTypes -from falcon.utils import print_ +from falcon.tabular.processors.scaler_and_encoder import ScalerAndEncoder +from falcon.types import DatasetSchema +from falcon.utils import logger class SimpleTabularPipeline(Pipeline): @@ -20,96 +18,55 @@ class SimpleTabularPipeline(Pipeline): def __init__( self, task: str, - dataset_size: Tuple[int], - mask: List[ColumnTypes], - learner: Type[Learner] = SuperLearner, - learner_kwargs: Optional[Dict] = None, + dataset_size: tuple[int, ...], + learner: type[Any], + schema: DatasetSchema | None = None, + learner_kwargs: dict[str, Any] | None = None, preprocessor: str = "MultiModalEncoder", + impute_missing: bool = True, **kwargs: Any, - ): - """ - Default tabular pipeline. On a high level it simply chains a preprocessor and model learner (by default `SuperLearner`). - For classification tasks, the labels are also encoded as integers (while predictions are decoded back to strings). - Internally, all numerical features are scaled to 0 mean and 1 std. All categorical features are one-hot encoded (this approach might not be suitable for features with very high cardinality). - - Parameters - ---------- - task : str - `tabular_classification` or `tabular_regression` - mask : List[int] - list of ints where 1/2 indicates a low/high cardinality categorical feature and 0 indicates a numerical feature - learner : Learner, optional - learner class to be used, by default `SuperLearner` - learner_kwargs : Optional[Dict], optional - arguments to be passed to the learner, by default None - preprocessor: str - defines which preprocessor to use, can be one of {'MultiModalEncoder','ScalerAndEncoder'}, by default 'MultiModalEncoder' - """ - - super().__init__(task=task, dataset_size=dataset_size, mask = mask) + ) -> None: + super().__init__(task=task, dataset_size=dataset_size, schema=schema) self.preprocessor = preprocessor + self.impute_missing = impute_missing self.learner = learner self.learner_kwargs = learner_kwargs + self.labels_transformer: LabelDecoder | None = None def _reset(self) -> None: - self._pipeline = [] - encoder: PipelineElement + self.clear_steps() + encoder: PipelineStep if self.preprocessor == "MultiModalEncoder": - encoder = MultiModalEncoder(self.mask) + encoder = MultiModalEncoder(impute_missing=self.impute_missing) else: - encoder = ScalerAndEncoder(self.mask) + encoder = ScalerAndEncoder(impute_missing=self.impute_missing) - self.add_element(encoder) + self.add_step(encoder) if not self.learner_kwargs: learner_kwargs = {} else: learner_kwargs = self.learner_kwargs - learner_: PipelineElement = self.learner( + learner = self.learner( task=self.task, dataset_size=self.dataset_size, **learner_kwargs ) - self.add_element(learner_) + self.add_step(learner) - if self.task == "tabular_classification": - self.labels_transformer: LabelDecoder = LabelDecoder() - self.add_element(self.labels_transformer) - - def fit(self, X: npt.NDArray, y: npt.NDArray, *args: Any, **kwargs: Any) -> None: - """ - Fits the pipeline by consecutively calling `.fit_pipe()` method of each element in pipeline. - For tabular classification, `LabelDecoder` is applied to targets before actual training occurs. - - Parameters - ---------- - X : npt.NDArray - train featrues - y : npt.NDArray - train targets - """ - print_("Fitting the pipeline...") + def fit( + self, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + schema: DatasetSchema, + *, + groups: npt.ArrayLike | None = None, + ) -> None: + logger.info("Fitting the pipeline...") self._reset() if self.task == "tabular_classification": - self.labels_transformer.fit(y) - y = self.labels_transformer.transform(y, inverse=False) - for p in self._pipeline: - p.fit_pipe(X, y) - X = p.forward(X) - - def predict(self, X: npt.NDArray, *args: Any, **kwargs: Any) -> npt.NDArray: - """ - Predicts the label of passed data points. - - Parameters - ---------- - X : npt.NDArray - features - - Returns - ------- - npt.NDArray - predicted label - """ - for p in self._pipeline: - X = p.forward(X) - return X + self.labels_transformer = LabelDecoder() + self.labels_transformer.fit(X, y, schema, groups=groups) + y = self.labels_transformer.encode(y) + super().fit(X, y, schema, groups=groups) + if self.labels_transformer is not None: + self.add_step(self.labels_transformer) diff --git a/falcon/tabular/portfolio_ordering.py b/falcon/tabular/portfolio_ordering.py new file mode 100644 index 0000000..2e474d3 --- /dev/null +++ b/falcon/tabular/portfolio_ordering.py @@ -0,0 +1,324 @@ +from __future__ import annotations + +import math +from collections.abc import Sequence +from dataclasses import dataclass +from numbers import Integral, Real +from typing import TYPE_CHECKING + +import numpy as np +from numpy import typing as npt + +from falcon.constants import TABULAR_CLASSIFICATION_TASK, TABULAR_REGRESSION_TASK +from falcon.types import ColumnTypes, DatasetSchema + +if TYPE_CHECKING: + from falcon.tabular.candidates import EstimatorSpec + + +_MIN_ROWS = 100 +_MAX_ROWS = 400_000 +_MIN_FEATURES = 3 +_MAX_FEATURES = 10_936 + + +@dataclass(frozen=True) +class DatasetMetaFeatures: + n_rows: int + n_features: int + class_balance: float | None + categorical_fraction: float + text_fraction: float + + def __post_init__(self) -> None: + if ( + isinstance(self.n_rows, bool) + or not isinstance(self.n_rows, Integral) + or self.n_rows < 1 + ): + raise ValueError("n_rows must be positive") + if ( + isinstance(self.n_features, bool) + or not isinstance(self.n_features, Integral) + or self.n_features < 1 + ): + raise ValueError("n_features must be positive") + if self.class_balance is not None and not _is_fraction( + self.class_balance, include_zero=False + ): + raise ValueError("class_balance must be between zero and one") + if not _is_fraction(self.categorical_fraction): + raise ValueError("categorical_fraction must be between zero and one") + if not _is_fraction(self.text_fraction): + raise ValueError("text_fraction must be between zero and one") + if self.categorical_fraction + self.text_fraction > 1: + raise ValueError( + "categorical_fraction and text_fraction must not sum above one" + ) + + +@dataclass(frozen=True) +class _PerformanceProfile: + task: str + meta_features: DatasetMetaFeatures + mean_ranks: tuple[tuple[str, float], ...] + + +def _is_fraction(value: object, *, include_zero: bool = True) -> bool: + if isinstance(value, bool) or not isinstance(value, Real): + return False + numeric_value = float(value) + above_lower_bound = numeric_value >= 0 if include_zero else numeric_value > 0 + return math.isfinite(numeric_value) and above_lower_bound and numeric_value <= 1 + + +def _ranks(**values: float) -> tuple[tuple[str, float], ...]: + return tuple(values.items()) + + +# Mean per-dataset ranks reduced from the public TabRepo 2023-11-14 config results. +_PERFORMANCE_PROFILES = ( + _PerformanceProfile( + TABULAR_CLASSIFICATION_TASK, + DatasetMetaFeatures(1_031, 929, 0.1270, 0.0025, 0.0), + _ranks( + catboost_zeroshot_r177=3.167, + catboost_default=3.375, + lightgbm_default=4.292, + xgboost_zeroshot_r33=4.333, + linear_default=5.292, + xgboost_default=5.375, + lightgbm_zeroshot_large=6.000, + random_forest_zeroshot=6.521, + extra_trees_zeroshot=6.646, + ), + ), + _PerformanceProfile( + TABULAR_CLASSIFICATION_TASK, + DatasetMetaFeatures(42_580, 11, 0.0980, 0.2302, 0.0), + _ranks( + catboost_zeroshot_r177=3.162, + lightgbm_default=3.737, + xgboost_default=3.921, + catboost_default=3.973, + xgboost_zeroshot_r33=4.211, + lightgbm_zeroshot_large=4.447, + random_forest_zeroshot=6.459, + extra_trees_zeroshot=7.054, + linear_default=7.789, + ), + ), + _PerformanceProfile( + TABULAR_CLASSIFICATION_TASK, + DatasetMetaFeatures(7_540, 21, 0.1543, 0.9231, 0.0), + _ranks( + catboost_zeroshot_r177=2.667, + catboost_default=2.956, + lightgbm_default=4.267, + xgboost_zeroshot_r33=4.400, + xgboost_default=4.489, + lightgbm_zeroshot_large=4.911, + extra_trees_zeroshot=6.822, + random_forest_zeroshot=6.844, + linear_default=7.644, + ), + ), + _PerformanceProfile( + TABULAR_CLASSIFICATION_TASK, + DatasetMetaFeatures(6_660, 20, 0.1421, 0.0627, 0.0), + _ranks( + catboost_default=2.580, + catboost_zeroshot_r177=2.580, + lightgbm_default=3.909, + xgboost_default=4.398, + xgboost_zeroshot_r33=4.807, + lightgbm_zeroshot_large=5.557, + extra_trees_zeroshot=6.580, + linear_default=7.091, + random_forest_zeroshot=7.500, + ), + ), + _PerformanceProfile( + TABULAR_REGRESSION_TASK, + DatasetMetaFeatures(1_147, 126, None, 0.0034, 0.0), + _ranks( + catboost_default=3.250, + lightgbm_zeroshot_large=3.750, + catboost_zeroshot_r177=4.000, + xgboost_default=4.500, + lightgbm_default=4.750, + xgboost_zeroshot_r33=5.500, + random_forest_zeroshot=6.250, + extra_trees_zeroshot=6.250, + linear_default=6.750, + ), + ), + _PerformanceProfile( + TABULAR_REGRESSION_TASK, + DatasetMetaFeatures(34_525, 18, None, 0.1727, 0.0), + _ranks( + catboost_zeroshot_r177=2.125, + lightgbm_default=2.875, + lightgbm_zeroshot_large=2.875, + catboost_default=3.000, + xgboost_default=6.000, + xgboost_zeroshot_r33=6.143, + extra_trees_zeroshot=6.250, + random_forest_zeroshot=6.625, + linear_default=8.750, + ), + ), + _PerformanceProfile( + TABULAR_REGRESSION_TASK, + DatasetMetaFeatures(9_622, 13, None, 0.4000, 0.0), + _ranks( + catboost_zeroshot_r177=2.545, + catboost_default=3.636, + lightgbm_default=3.909, + lightgbm_zeroshot_large=4.000, + xgboost_zeroshot_r33=4.700, + xgboost_default=5.364, + extra_trees_zeroshot=5.909, + random_forest_zeroshot=6.727, + linear_default=7.818, + ), + ), + _PerformanceProfile( + TABULAR_REGRESSION_TASK, + DatasetMetaFeatures(3_759, 9, None, 0.0, 0.0), + _ranks( + lightgbm_default=3.000, + xgboost_default=3.333, + xgboost_zeroshot_r33=3.667, + catboost_zeroshot_r177=4.000, + extra_trees_zeroshot=4.667, + lightgbm_zeroshot_large=5.333, + random_forest_zeroshot=6.000, + catboost_default=6.000, + linear_default=9.000, + ), + ), +) + + +def extract_dataset_meta_features( + y: npt.ArrayLike, + schema: DatasetSchema, +) -> DatasetMetaFeatures: + target_values = np.asarray(y) + if target_values.ndim != 1: + raise ValueError("Target data must be one-dimensional") + if len(target_values) != schema.n_rows: + raise ValueError("Target data does not match the dataset schema") + + class_balance = None + if schema.target_kind == "classification": + _, class_counts = np.unique(target_values, return_counts=True) + class_balance = float(class_counts.min() / len(target_values)) + + categorical_types = {ColumnTypes.CAT_LOW_CARD, ColumnTypes.CAT_HIGH_CARD} + categorical_columns = sum( + column_type in categorical_types for column_type in schema.column_types + ) + text_columns = schema.column_types.count(ColumnTypes.TEXT_UTF8) + return DatasetMetaFeatures( + n_rows=schema.n_rows, + n_features=schema.n_features, + class_balance=class_balance, + categorical_fraction=categorical_columns / schema.n_features, + text_fraction=text_columns / schema.n_features, + ) + + +def _in_corpus_range(meta_features: DatasetMetaFeatures) -> bool: + return ( + _MIN_ROWS <= meta_features.n_rows <= _MAX_ROWS + and _MIN_FEATURES <= meta_features.n_features <= _MAX_FEATURES + ) + + +def _scaled_log(value: int, minimum: int, maximum: int) -> float: + return (math.log(value) - math.log(minimum)) / ( + math.log(maximum) - math.log(minimum) + ) + + +def _profile_distance( + left: DatasetMetaFeatures, + right: DatasetMetaFeatures, + task: str, +) -> float: + differences = [ + _scaled_log(left.n_rows, _MIN_ROWS, _MAX_ROWS) + - _scaled_log(right.n_rows, _MIN_ROWS, _MAX_ROWS), + _scaled_log(left.n_features, _MIN_FEATURES, _MAX_FEATURES) + - _scaled_log(right.n_features, _MIN_FEATURES, _MAX_FEATURES), + left.categorical_fraction - right.categorical_fraction, + left.text_fraction - right.text_fraction, + ] + if task == TABULAR_CLASSIFICATION_TASK: + if left.class_balance is None or right.class_balance is None: + raise ValueError("Classification ordering requires class_balance") + differences.append((left.class_balance - right.class_balance) * 2) + return sum(difference**2 for difference in differences) + + +def reorder_portfolio( + specs: Sequence[EstimatorSpec], + meta_features: DatasetMetaFeatures, + task: str, + *, + random_state: int, +) -> tuple[EstimatorSpec, ...]: + if task not in {TABULAR_CLASSIFICATION_TASK, TABULAR_REGRESSION_TASK}: + raise ValueError(f"Unknown task `{task}`") + if task == TABULAR_CLASSIFICATION_TASK and meta_features.class_balance is None: + raise ValueError("Classification ordering requires class_balance") + if ( + isinstance(random_state, bool) + or not isinstance(random_state, int) + or random_state < 0 + ): + raise ValueError("random_state must be a non-negative integer") + + static_order = tuple(specs) + if not static_order or not _in_corpus_range(meta_features): + return static_order + profiles = tuple( + profile for profile in _PERFORMANCE_PROFILES if profile.task == task + ) + nearest = min( + profiles, + key=lambda profile: _profile_distance( + meta_features, + profile.meta_features, + task, + ), + ) + ranks = dict(nearest.mean_ranks) + known_positions = [ + index for index, spec in enumerate(static_order) if spec.name in ranks + ] + if len(known_positions) < 2: + return static_order + + generator = np.random.default_rng(random_state) + tie_breakers = generator.random(len(static_order)) + ordered_known = sorted( + ((index, static_order[index]) for index in known_positions), + key=lambda item: ( + ranks[item[1].name], + tie_breakers[item[0]], + ), + ) + reordered = list(static_order) + for index, (_, spec) in zip(known_positions, ordered_known, strict=True): + reordered[index] = spec + return tuple(reordered) + + +__all__ = [ + "DatasetMetaFeatures", + "extract_dataset_meta_features", + "reorder_portfolio", +] diff --git a/falcon/tabular/processors/__init__.py b/falcon/tabular/processors/__init__.py index 147e89c..5dcf490 100644 --- a/falcon/tabular/processors/__init__.py +++ b/falcon/tabular/processors/__init__.py @@ -1,3 +1,5 @@ -from falcon.tabular.processors.scaler_and_encoder import ScalerAndEncoder from falcon.tabular.processors.label_decoder import LabelDecoder -from falcon.tabular.processors.multi_modal_encoder import MultiModalEncoder \ No newline at end of file +from falcon.tabular.processors.multi_modal_encoder import MultiModalEncoder +from falcon.tabular.processors.scaler_and_encoder import ScalerAndEncoder + +__all__ = ["LabelDecoder", "MultiModalEncoder", "ScalerAndEncoder"] diff --git a/falcon/tabular/processors/label_decoder.py b/falcon/tabular/processors/label_decoder.py index 20cf075..9c50a34 100644 --- a/falcon/tabular/processors/label_decoder.py +++ b/falcon/tabular/processors/label_decoder.py @@ -1,118 +1,45 @@ -from falcon.abstract import Processor, ONNXConvertible, PipelineElement -from typing import Any, Type, Union -from numpy.typing import NDArray +from typing import Any + import numpy as np +from numpy import typing as npt +from numpy.typing import NDArray +from onnx import TensorProto +from onnx import helper as h from sklearn.preprocessing import LabelEncoder -from falcon.types import Float32Array, Int64Array -from skl2onnx import convert_sklearn -from onnx import TensorProto, helper as h, OperatorSetIdProto -from skl2onnx.common.data_types import StringTensorType + from falcon.config import ML_ONNX_OPSET_VERSION -from numpy import typing as npt from falcon.serialization import SerializedModelRepr +from falcon.types import DatasetSchema, Int64Array -class LabelDecoder(Processor, ONNXConvertible): - """ - Label encoder/decoder to be used for encoding labels as integers and vice versa. - """ +class LabelDecoder: def __init__(self) -> None: - """ - does not take any arguments - """ self.le = LabelEncoder() - def fit_pipe(self, X: Any, y: Any, *args: Any, **kwargs: Any) -> None: # Do nothing - """ - Since label decoder should initially be fitted and applied before the main training phase of pipeline, this method does nothing. - - Parameters - ---------- - X : Any - dummy argument - y : Any - dummy argument - """ - return - - def fit(self, X: npt.NDArray, y: Any = None, *args: Any, **kwargs: Any) -> None: - """ - Fits the decoder. - - Parameters - ---------- - X : npt.NDArray - labels to be encoded as integers - y : Any, optional - dummy argument, by default None - """ - self.le.fit(X) + def fit( + self, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + schema: DatasetSchema, + *, + groups: npt.ArrayLike | None = None, + ) -> None: + self.le.fit(y) - def predict(self, X: npt.NDArray, inverse: bool = True, *args: Any, **kwargs: Any) -> npt.NDArray: - """ - Equivalent of `.transform()`. + def encode(self, labels: npt.NDArray[Any]) -> npt.NDArray[np.int64]: + return self.le.transform(labels) - Parameters - ---------- - X : npt.NDArray - labels - inverse : bool, optional - if True, encode strings as integers, else convert integers back to strings, by default True + def transform(self, X: npt.NDArray[Any]) -> npt.NDArray[Any]: + """Decodes integer predictions back to the fitted labels.""" + return self.le.inverse_transform(X.astype(np.int64)).astype(np.str_) - Returns - ------- - npt.NDArray - encoded/decoded labels - """ - return self.transform(X, inverse=inverse) - - def transform(self, X: npt.NDArray, inverse: bool = True, *args: Any, **kwargs: Any) -> npt.NDArray: - """ - Encodes/decodes the labels. - - Parameters - ---------- - X : npt.NDArray - labels - inverse : bool, optional - if True, encode strings as integers, else convert integers back to strings, by default True - - Returns - ------- - npt.NDArray - encoded/decoded labels - """ - if not inverse: - return self.le.transform(X) - else: - return self.le.inverse_transform(X.astype(np.int64)).astype(np.str_) - - def get_input_type(self) -> Type: - """ - Returns - ------- - Type - Int64Array - """ + def get_input_type(self) -> object: return Int64Array - def get_output_type(self) -> Type: - """ - Returns - ------- - Type - NDArray[str] - """ + def get_output_type(self) -> object: return NDArray[np.str_] - def to_onnx(self) -> SerializedModelRepr: - """ - Serializes the encoder to onnx. - - Returns - ------- - SerializedModelRepr - """ + def serialize(self) -> SerializedModelRepr: inputs = [h.make_tensor_value_info("encoded_labels", TensorProto.INT64, [None])] outputs = [ h.make_tensor_value_info("decoded_labels", TensorProto.STRING, [None]) @@ -123,28 +50,10 @@ def to_onnx(self) -> SerializedModelRepr: ["decoded_labels"], values_strings=[str(el) for el in self.le.classes_], keys_int64s=[int(i) for i in range(len(self.le.classes_))], - name=f"labels_decoder", + name="labels_decoder", domain="ai.onnx.ml", ) - graph = h.make_graph([node], f"decoder", inputs, outputs) + graph = h.make_graph([node], "decoder", inputs, outputs) op = h.make_operatorsetid("ai.onnx.ml", ML_ONNX_OPSET_VERSION) - model = h.make_model(graph, producer_name="falcon", opset_imports = [op]) + model = h.make_model(graph, producer_name="falcon", opset_imports=[op]) return SerializedModelRepr(model, 1, 1, ["INT64"], [[None]]) - - def forward( - self, X: npt.NDArray, *args: Any, **kwargs: Any - ) -> npt.NDArray: # Inside pipeline used as post-processor to decode labels back to strings - """ - Equivalent to `.transform(X, inverse=True)`. - - Parameters - ---------- - X : npt.NDArray - labels to decode - - Returns - ------- - npt.NDArray - labels decoded to strings - """ - return self.transform(X, inverse=True) diff --git a/falcon/tabular/processors/multi_modal_encoder.py b/falcon/tabular/processors/multi_modal_encoder.py index 8c3b68b..7ed6343 100644 --- a/falcon/tabular/processors/multi_modal_encoder.py +++ b/falcon/tabular/processors/multi_modal_encoder.py @@ -1,20 +1,29 @@ -import numpy as np +from typing import Any + from numpy import typing as npt -from falcon.types import Float32Array, ColumnTypes -from typing import List, Optional, Type, Any, Tuple, Union +from skl2onnx.sklapi import CastTransformer from sklearn.compose import ColumnTransformer -from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.pipeline import Pipeline as SKLPipeline from sklearn.preprocessing import MaxAbsScaler -from skl2onnx.sklapi import CastTransformer -from falcon.tabular.processors.scaler_and_encoder import ScalerAndEncoder -from falcon.addons.sklearn.preprocessing.date_tokenizer import DateTimeTokenizer + from falcon.addons.sklearn.decomposition.svd import ConditionalSVD +from falcon.addons.sklearn.preprocessing.date_tokenizer import DateTimeTokenizer +from falcon.addons.sklearn.preprocessing.text_vectorizer import ( + FalconTfidfVectorizer, +) +from falcon.tabular.processors.scaler_and_encoder import ScalerAndEncoder +from falcon.types import ColumnTypes, DatasetSchema + +DATE_COLUMN_TYPES = ( + ColumnTypes.DATE_YMD_ISO8601, + ColumnTypes.DATETIME_YMDHMS_ISO8601, +) class MultiModalEncoder(ScalerAndEncoder): """ - Applies different types of encodings on numerical, categorical, text and date/datetime features. + Extends `ScalerAndEncoder` with tokenization of date/datetime features and + tf-idf vectorization of text features. """ def _get_date_tokenizer(self, ct: ColumnTypes) -> SKLPipeline: @@ -26,47 +35,59 @@ def _get_date_tokenizer(self, ct: ColumnTypes) -> SKLPipeline: raise ValueError("Unknown column type encountered") return SKLPipeline( steps=[ - ("cast_str", CastTransformer(dtype=np.str_)), ("date_tokenizer", DateTimeTokenizer(format=f)), ("cast32", CastTransformer()), ("sc", MaxAbsScaler()), ] ) + def _reject_unsupported_date_columns(self, schema: DatasetSchema) -> None: + if self.impute_missing: + return + unsupported = [ + name + for name, column_type in zip( + schema.column_names, schema.column_types, strict=True + ) + if column_type in DATE_COLUMN_TYPES + ] + if unsupported: + raise ValueError( + "Date and datetime features are not supported while imputation is " + f"disabled: {', '.join(unsupported)}" + ) + def _get_text_tfidf(self) -> SKLPipeline: return SKLPipeline( steps=[ - ("cast_str", CastTransformer(dtype=np.str_)), + ("imputer", self._get_string_imputer("")), ( "tfidf_vectorizer", - TfidfVectorizer( - stop_words="english", - input="content", - analyzer="word", - max_features=1024, - token_pattern="[a-zA-Z0-9_]+", - ), + FalconTfidfVectorizer(), ), ("cast32", CastTransformer()), ("svd", ConditionalSVD(n_components=32)), ] ) - def fit(self, X: npt.NDArray, y: Any = None, *args: Any, **kwargs: Any) -> None: - """ - Fits the encoder. - - Parameters - ---------- - X : npt.NDArray - data to encode - _ : Any, optional - dummy argument to keep compatibility with pipeline training - """ + def fit( + self, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + schema: DatasetSchema, + *, + groups: npt.ArrayLike | None = None, + ) -> None: + if X.ndim != 2 or X.shape[1] != schema.n_features: + raise ValueError("Feature data does not match the dataset schema") + self._reject_unsupported_date_columns(schema) + self.column_types = schema.column_types transformers = [] - for i, v in enumerate(self.mask): + for i, v in enumerate(self.column_types): if v == ColumnTypes.CAT_LOW_CARD: method = self._get_ohe() + elif v == ColumnTypes.CAT_HIGH_CARD: + method = self._get_target_encoder(schema.target_kind) elif v == ColumnTypes.NUMERIC_REGULAR: method = self._get_numeric_scaler() elif v in [ @@ -78,11 +99,12 @@ def fit(self, X: npt.NDArray, y: Any = None, *args: Any, **kwargs: Any) -> None: method = self._get_text_tfidf() else: method = self._get_ordinal_encoder() - t: Tuple[str, Any, Union[int, List[int]]] + t: tuple[str, Any, int | list[int]] if v != ColumnTypes.TEXT_UTF8: t = (f"input {i}", method, [i]) else: t = (f"input {i}", method, i) transformers.append(t) self.ct = ColumnTransformer(transformers) - self.ct.fit(X) + self.ct.fit(X, y) + self._align_numeric_scalers_with_onnx() diff --git a/falcon/tabular/processors/scaler_and_encoder.py b/falcon/tabular/processors/scaler_and_encoder.py index e1de02c..4d8c4bc 100644 --- a/falcon/tabular/processors/scaler_and_encoder.py +++ b/falcon/tabular/processors/scaler_and_encoder.py @@ -1,46 +1,57 @@ +from typing import Any + import numpy as np from numpy import typing as npt -from falcon.types import Float32Array, ColumnTypes -from sklearn.base import BaseEstimator -from sklearn import __version__ as sklearn_version -from packaging import version -from sklearn.compose import ColumnTransformer -from sklearn.preprocessing import StandardScaler -from sklearn.preprocessing import OneHotEncoder -from falcon.abstract import Processor, ONNXConvertible from skl2onnx import convert_sklearn from skl2onnx.common.data_types import FloatTensorType, StringTensorType -from falcon.config import ONNX_OPSET_VERSION, ML_ONNX_OPSET_VERSION -from typing import List, Optional, Type, Any +from sklearn.base import BaseEstimator +from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline as SKLPipeline -from sklearn.preprocessing import MaxAbsScaler, OrdinalEncoder -from skl2onnx.sklapi import CastTransformer +from sklearn.preprocessing import ( + MaxAbsScaler, + OneHotEncoder, + OrdinalEncoder, + StandardScaler, +) + +from falcon.addons.sklearn.preprocessing.missing_values import ( + MissingStringImputer, + NumericCast, + NumericMedianImputer, + StringCast, +) +from falcon.addons.sklearn.preprocessing.target_encoder import FalconTargetEncoder +from falcon.config import ML_ONNX_OPSET_VERSION, ONNX_OPSET_VERSION from falcon.serialization import SerializedModelRepr +from falcon.types import ColumnTypes, DatasetSchema, Float32Array, TargetKind -class ScalerAndEncoder(Processor, ONNXConvertible): +class ScalerAndEncoder: """ - Applies OneHotEncoder/OrdinalEncoder on low/high cardinality categorical features and StandardScaler on numerical features. + One-hot encodes low cardinality categoricals, target encodes high cardinality ones + and standard scales numeric features. + + With `impute_missing` disabled, missing values are not filled in and the exported + graph contains no data dependent branching; numeric features must then be complete + at fit time and missing categories become ordinary categories. """ - def __init__(self, mask: List[ColumnTypes]) -> None: - """ - Parameters - ---------- - mask : List[ColumnTypes] - provides a type for each column at a given index - """ - self.mask = mask + def __init__(self, impute_missing: bool = True) -> None: + self.impute_missing = impute_missing + self.column_types: tuple[ColumnTypes, ...] = () + self.ct: ColumnTransformer + + def _get_string_imputer(self, fill_value: str) -> BaseEstimator: + if not self.impute_missing: + return StringCast() + return MissingStringImputer(fill_value=fill_value) def _get_ohe(self) -> BaseEstimator: - if version.parse(sklearn_version) < version.parse("1.2.0"): - not_sparse = {"sparse": False} - else: - not_sparse = {"sparse_output": False} + not_sparse = {"sparse_output": False} method = SKLPipeline( steps=[ - ("cast_str", CastTransformer(dtype=np.str_)), + ("imputer", self._get_string_imputer("__falcon_missing__")), ( "ohe", OneHotEncoder( @@ -52,18 +63,35 @@ def _get_ohe(self) -> BaseEstimator: return method def _get_numeric_scaler(self) -> BaseEstimator: + imputer = NumericMedianImputer() if self.impute_missing else NumericCast() return SKLPipeline( steps=[ - ("cast64", CastTransformer(dtype=np.float64)), + ("imputer", imputer), ("scaler", StandardScaler(with_mean=True, with_std=True)), - ("cast32", CastTransformer()), + ] + ) + + def _get_target_encoder(self, target_kind: TargetKind) -> BaseEstimator: + target_type = "continuous" if target_kind == "regression" else "auto" + return SKLPipeline( + steps=[ + ("imputer", self._get_string_imputer("__falcon_missing__")), + ( + "target_encoder", + FalconTargetEncoder( + target_type=target_type, + cv=5, + shuffle=True, + random_state=42, + ), + ), ] ) def _get_ordinal_encoder(self) -> BaseEstimator: return SKLPipeline( steps=[ - ("cast_str", CastTransformer(dtype=np.str_)), + ("imputer", self._get_string_imputer("__falcon_missing__")), ( "ord_enc", OrdinalEncoder( @@ -76,22 +104,34 @@ def _get_ordinal_encoder(self) -> BaseEstimator: ] ) - def fit(self, X: npt.NDArray, y: Any = None, *args: Any, **kwargs: Any) -> None: - """ - Fits the encoder. - - Parameters - ---------- - X : npt.NDArray - data to encode - _ : Any, optional - dummy argument to keep compatibility with pipeline training - """ + def _align_numeric_scalers_with_onnx(self) -> None: + for index, column_type in enumerate(self.column_types): + if column_type != ColumnTypes.NUMERIC_REGULAR: + continue + pipeline = self.ct.named_transformers_[f"input {index}"] + scaler = pipeline.named_steps["scaler"] + # Float32 statistics keep tree split decisions identical after export. + scaler.mean_ = scaler.mean_.astype(np.float32) + scaler.scale_ = scaler.scale_.astype(np.float32) + + def fit( + self, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + schema: DatasetSchema, + *, + groups: npt.ArrayLike | None = None, + ) -> None: + if X.ndim != 2 or X.shape[1] != schema.n_features: + raise ValueError("Feature data does not match the dataset schema") + self.column_types = schema.column_types transformers = [] - for i, v in enumerate(self.mask): + for i, v in enumerate(self.column_types): if v == ColumnTypes.CAT_LOW_CARD: method = self._get_ohe() + elif v == ColumnTypes.CAT_HIGH_CARD: + method = self._get_target_encoder(schema.target_kind) elif v == ColumnTypes.NUMERIC_REGULAR: method = self._get_numeric_scaler() else: @@ -100,74 +140,54 @@ def fit(self, X: npt.NDArray, y: Any = None, *args: Any, **kwargs: Any) -> None: transformers.append(t) self.ct = ColumnTransformer(transformers) - self.ct.fit(X) - - def predict(self, X: npt.NDArray, *args: Any, **kwargs: Any) -> npt.NDArray: - """ - Applies the encoder. - - Parameters - ---------- - X : npt.NDArray - input data - - Returns - ------- - npt.NDArray - encoded data - """ + self.ct.fit(X, y) + self._align_numeric_scalers_with_onnx() + + def fit_transform( + self, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + schema: DatasetSchema, + *, + groups: npt.ArrayLike | None = None, + ) -> Float32Array: + self.fit(X, y, schema, groups=groups) + transformed = np.asarray(self.ct.transform(X), dtype=np.float32) + for index, column_type in enumerate(self.column_types): + if column_type != ColumnTypes.CAT_HIGH_CARD: + continue + transformer_name = f"input {index}" + fitted_pipeline = self.ct.named_transformers_[transformer_name] + imputed = fitted_pipeline.named_steps["imputer"].transform(X[:, [index]]) + target_encoder = fitted_pipeline.named_steps["target_encoder"] + cross_fitted = target_encoder.cross_fit_transform( + imputed, + y, + X, + schema.target_kind, + groups=groups, + ) + transformed[:, self.ct.output_indices_[transformer_name]] = cross_fitted + return transformed + + def transform(self, X: npt.NDArray[Any]) -> npt.NDArray[Any]: return self.ct.transform(X).astype(dtype=np.float32) - def get_input_type(self) -> Type: - """ - Returns - ------- - Type - object - """ + def get_input_type(self) -> object: return npt.NDArray[np.object_] - def get_output_type(self) -> Type: - """ - Returns - ------- - Type - Float32Array - """ + def get_output_type(self) -> object: return Float32Array - def forward( - self, X: npt.NDArray[np.object_], *args: Any, **kwargs: Any - ) -> npt.NDArray: + def serialize(self) -> SerializedModelRepr: """ - Equivalent of `.predict()` or `.transform()`. - - - Parameters - ---------- - X : npt.NDArray[object] - data to process - - Returns - ------- - npt.NDArray - processed data - """ - return self.transform(X) - - def to_onnx(self) -> SerializedModelRepr: - """ - Serializes the encoder to onnx. - Each feature in the original dataset is mapped to its own input node (`float32` for numerical or `string` for categorical). - - Returns - ------- - SerializedModelRepr + Each feature of the original dataset becomes its own onnx input node, + `float32` for numeric features and `string` for everything else. """ initial_types = [] - initial_types_str: List[str] = [] - initial_shapes: List[List[Optional[int]]] = [] - for i, t in enumerate(self.mask): + initial_types_str: list[str] = [] + initial_shapes: list[list[int | None]] = [] + for i, t in enumerate(self.column_types): if t in [ColumnTypes.NUMERIC_REGULAR]: tensor = FloatTensorType([None, 1]) initial_types_str.append("FLOAT32") @@ -184,9 +204,11 @@ def to_onnx(self) -> SerializedModelRepr: "": ONNX_OPSET_VERSION, "ai.onnx.ml": ML_ONNX_OPSET_VERSION, }, - options={StandardScaler: {"div": "div_cast"}}, + options={ + StandardScaler: {"div": "div"}, + }, ), - len(self.mask), + len(self.column_types), 1, initial_types_str, initial_shapes, diff --git a/falcon/tabular/reporting.py b/falcon/tabular/reporting.py deleted file mode 100644 index 5207f7d..0000000 --- a/falcon/tabular/reporting.py +++ /dev/null @@ -1,145 +0,0 @@ -from numpy import typing as npt -from typing import Dict -import numpy as np -from sklearn import metrics - -def scale_acc(acc: float, n_classes: int) -> float: - if acc < 0. or acc > 1.: - raise ValueError('Accuracy score should be in range [0,1]') - elif acc == 0. or acc == 1. or n_classes < 3: - return acc - - random_performance = 1 / n_classes - - a_l = 0.5 / random_performance - - a_u = 0.5 / (1-random_performance) - b_u = 0.5 - (0.5/(1-random_performance))*random_performance - - if acc <= random_performance: - return acc * a_l - else: - return acc * a_u + b_u - -def print_classification_report(y: npt.NDArray, y_hat: npt.NDArray, silent: bool = False) -> Dict: - y = y.astype(np.str_) - classification_report = metrics.classification_report(y, y_hat, output_dict=True) - n_classes, n_samples = len(np.unique(y)), len(y) - metrics_ = { - 'N_SAMPLES': n_samples, - 'N_CLASSES': n_classes, - 'ACC': metrics.accuracy_score(y, y_hat), - 'BACC': metrics.balanced_accuracy_score(y, y_hat), - 'PRECISION': list(classification_report[list(classification_report.keys())[-2]].values())[0], - 'RECALL': list(classification_report[list(classification_report.keys())[-2]].values())[1], - 'F1': list(classification_report[list(classification_report.keys())[-2]].values())[2], - 'B_PRECISION': list(classification_report[list(classification_report.keys())[-1]].values())[0], - 'B_RECALL': list(classification_report[list(classification_report.keys())[-1]].values())[1], - 'B_F1': list(classification_report[list(classification_report.keys())[-1]].values())[2] - } - - metrics_['SCORE'] = metrics_['BACC'] - metrics_['SC_SCORE'] = scale_acc(metrics_['SCORE'], n_classes) - - if not silent: - confusion_matrix = metrics.confusion_matrix(y, y_hat) - print() - print("PERFORMANCE REPORT: CLASSIFICATION") - print() - print() - labels = [ - "Precision (% of correct predictions for this class):", - "Recall (% of samples of this class that are correctly predicted):", - "F1 Score (out of samples of this class, % of correct predictions):", - "Support (number of class samples):", - ] - print("CLASS-SPECIFIC METRICS") - print() - for k in list(classification_report.keys())[:-3]: - print(f"Label: {k}") - print() - for i, kk in enumerate(classification_report[k].keys()): - print(labels[i]) - print(classification_report[k][kk]) - print() - print() - print("AVERAGE METRICS") - print() - print("Confusion Matrix") - print("Note: high values in diagonal, low values elsewhere indicate good performance") - print(confusion_matrix) - print() - print("Accuracy") - print( - "Note: misleading for imbalanced datasets! Low accuracy on classes with a low number of samples is not reflected!" - ) - print(metrics_['ACC']) - print() - print("Balanced Accuracy") - print("Each class weighs the same even if it has a low number of samples") - print(metrics_['BACC']) - print() - # TODO - # print("Weighted ROC AUC score") - # print( - # "Area Under the Receiver Operating Characteristic Curve, balanced by number of samples per class. The closer to 1 the better the performance." - # ) - # print(metrics.roc_auc_score(np.eye(np.max(y) + 1)[y], np.eye(np.max(y_hat) + 1)[y_hat], multi_class="ovo")) - # print() - print("Precision") - print("Note: misleading for imbalanced datasets!") - print(metrics_['PRECISION']) - print() - print("Recall") - print("Note: misleading for imbalanced datasets!") - print(metrics_['RECALL']) - print() - print("F1 Score") - print("Note: misleading for imbalanced datasets!") - print(metrics_['F1']) - print() - print("Balanced Precision") - print(metrics_['B_PRECISION']) - print() - print("Balanced Recall") - print(metrics_['B_RECALL']) - print() - print("Balanced F1 Score") - print(metrics_['B_F1']) - return metrics_ - - -def print_regression_report(y: npt.NDArray, y_hat: npt.NDArray, silent: bool = False) -> Dict: - diff = y-y_hat - metrics_ = { - 'N_SAMPLES': len(y), - 'R2': metrics.r2_score(y, y_hat), - 'RMSE': np.sqrt(np.mean((diff) ** 2)), - 'MSE': np.mean((diff) ** 2), - 'MAE': np.mean(np.abs(diff)), - 'RMSLE': np.log(np.sqrt(np.mean((diff) ** 2)) + 1e-7) - } - metrics_['SCORE'] = metrics_['R2'] if metrics_['R2'] > 0.0 else 0.0 - metrics_['SC_SCORE'] = (metrics_['SCORE'] + 1) / 2 - if not silent: - print("PERFORMANCE REPORT: REGRESSION") - print() - print("R2") - print( - "The closer to 1 the better the performance. R2 of 0 is the score of a regressor that always predicts the average." - ) - print(metrics_['R2']) - print() - print("RMSE (Root Mean Squared Error)") - print(metrics_['RMSE']) - print() - print("MSE (Mean Sqaured Error)") - print(metrics_['MSE']) - print() - print("MAE (Mean Absolute Error)") - print(metrics_['MAE']) - print() - print("RMSLE (Root Mean Squared Log Error)") - print("Useful in case of skewed distribution of target values.") - print(metrics_['RMSLE']) - return metrics_ diff --git a/falcon/tabular/splitting.py b/falcon/tabular/splitting.py new file mode 100644 index 0000000..cdb17ec --- /dev/null +++ b/falcon/tabular/splitting.py @@ -0,0 +1,444 @@ +from __future__ import annotations + +from collections import defaultdict, deque +from collections.abc import Callable, Sequence +from typing import Any, Literal, TypeAlias, cast + +import numpy as np +import pandas as pd +from numpy import typing as npt +from sklearn.model_selection import ( + BaseCrossValidator, + GroupKFold, + GroupShuffleSplit, + StratifiedGroupKFold, +) + +from falcon.constants import TABULAR_CLASSIFICATION_TASK, TABULAR_REGRESSION_TASK + +GroupBy: TypeAlias = str | Sequence[str] | npt.ArrayLike +SplitIndices: TypeAlias = tuple[npt.NDArray[np.int64], npt.NDArray[np.int64]] +EvalStrategy: TypeAlias = Literal["cv", "holdout"] + +_DEFAULT_CV_SPLITS = 5 +_DEFAULT_TEST_SIZE = 0.25 +_HOLDOUT_ROW_THRESHOLD = 2_500 + + +def _factorize_rows(values: npt.NDArray[Any]) -> npt.NDArray[np.int64]: + if values.ndim == 1: + values = values.reshape(-1, 1) + try: + hashes = pd.util.hash_pandas_object( + pd.DataFrame(values), index=False + ).to_numpy() + except TypeError as error: + raise ValueError("Group values must be scalar and hashable") from error + groups, _ = pd.factorize(hashes, sort=False) + return np.asarray(groups, dtype=np.int64) + + +def _named_group_columns( + group_by: GroupBy, + column_names: tuple[str, ...], + n_rows: int, +) -> tuple[str, ...] | None: + if isinstance(group_by, str): + return (group_by,) + if not isinstance(group_by, (list, tuple)): + return None + if not group_by: + raise ValueError("group_by must not be empty") + if not all(isinstance(value, str) for value in group_by): + return None + + names = cast(Sequence[str], group_by) + if all(name in column_names for name in names) or len(names) != n_rows: + return tuple(names) + return None + + +def _column_indices( + requested_names: tuple[str, ...], column_names: tuple[str, ...] +) -> list[int]: + indices: list[int] = [] + for name in requested_names: + matching = [ + index + for index, column_name in enumerate(column_names) + if column_name == name + ] + if not matching: + raise ValueError(f"Unknown group_by feature column `{name}`") + if len(matching) > 1: + raise ValueError(f"group_by feature column `{name}` is ambiguous") + indices.append(matching[0]) + return indices + + +def _explicit_group_values( + group_by: GroupBy, + n_rows: int, + source_row_indices: npt.NDArray[np.int64] | None, + source_row_count: int | None, +) -> npt.NDArray[Any]: + values = np.asarray(group_by, dtype=np.object_) + if values.ndim == 2 and values.shape[1] == 1: + values = values[:, 0] + if values.ndim != 1: + raise ValueError("Explicit group_by values must be one-dimensional") + if values.shape[0] == n_rows: + return values + if ( + source_row_indices is not None + and source_row_count is not None + and values.shape[0] == source_row_count + ): + return values[source_row_indices] + raise ValueError( + "Explicit group_by values must contain one value per input data row" + ) + + +def resolve_groups( + X: npt.NDArray[Any], + column_names: tuple[str, ...], + group_by: GroupBy | None = None, + *, + source_row_indices: npt.NDArray[np.int64] | None = None, + source_row_count: int | None = None, +) -> npt.NDArray[np.int64]: + values = np.asarray(X) + if values.ndim != 2: + raise ValueError("Features must be two-dimensional when resolving groups") + if values.shape[1] != len(column_names): + raise ValueError("Feature names do not match the feature array") + + if group_by is None: + grouping_values = values + else: + requested_names = _named_group_columns(group_by, column_names, values.shape[0]) + if requested_names is None: + grouping_values = _explicit_group_values( + group_by, + values.shape[0], + source_row_indices, + source_row_count, + ) + else: + grouping_values = values[:, _column_indices(requested_names, column_names)] + return _factorize_rows(np.asarray(grouping_values)) + + +def resolve_evaluation_strategy(n_rows: int) -> EvalStrategy: + return "cv" if n_rows < _HOLDOUT_ROW_THRESHOLD else "holdout" + + +def _normalized_groups( + X: npt.NDArray[Any], groups: npt.ArrayLike | None +) -> npt.NDArray[np.int64]: + if groups is None: + column_names = tuple(f"feature_{index}" for index in range(X.shape[1])) + return resolve_groups(X, column_names) + group_values = np.asarray(groups, dtype=np.object_) + if group_values.ndim == 2 and group_values.shape[1] == 1: + group_values = group_values[:, 0] + if group_values.ndim != 1 or group_values.shape[0] != X.shape[0]: + raise ValueError("Groups must contain one value per feature row") + return _factorize_rows(group_values) + + +def _validate_split_inputs( + X: npt.NDArray[Any], y: npt.NDArray[Any], groups: npt.ArrayLike | None +) -> tuple[npt.NDArray[Any], npt.NDArray[Any], npt.NDArray[np.int64]]: + feature_values = np.asarray(X) + target_values = np.asarray(y) + if feature_values.ndim != 2: + raise ValueError("Features must be two-dimensional") + if target_values.ndim == 2 and target_values.shape[1] == 1: + target_values = target_values[:, 0] + if target_values.ndim != 1: + raise ValueError("Target values must be one-dimensional") + if feature_values.shape[0] != target_values.shape[0]: + raise ValueError("Features and target must contain the same number of rows") + normalized_groups = _normalized_groups(feature_values, groups) + if np.unique(normalized_groups).size < 2: + raise ValueError("At least two distinct groups are required to split the data") + return feature_values, target_values, normalized_groups + + +def _validate_task(task: str) -> None: + if task not in {TABULAR_CLASSIFICATION_TASK, TABULAR_REGRESSION_TASK}: + raise ValueError(f"Unknown tabular task `{task}`") + + +def _classification_split_count( + y: npt.NDArray[Any], + groups: npt.NDArray[np.int64], + requested_splits: int, +) -> int: + class_codes, classes = pd.factorize(pd.Series(y), sort=False) + if (class_codes < 0).any(): + raise ValueError("Classification targets must not contain missing values") + if len(classes) < 2: + raise ValueError("Classification splitting requires at least two classes") + groups_per_class = [ + np.unique(groups[class_codes == class_code]).size + for class_code in range(len(classes)) + ] + n_splits = min(requested_splits, np.unique(groups).size, *groups_per_class) + if n_splits < 2: + raise ValueError( + "Stratified group splitting requires each class to occur in at least two groups" + ) + return int(n_splits) + + +def _classification_codes(y: npt.NDArray[Any]) -> npt.NDArray[np.int64]: + class_codes, _ = pd.factorize(pd.Series(y), sort=False) + return np.asarray(class_codes, dtype=np.int64) + + +def _coerce_indices(values: Any, split_name: str) -> npt.NDArray[np.int64]: + indices = np.asarray(values) + if indices.ndim != 1 or not np.issubdtype(indices.dtype, np.integer): + raise ValueError( + f"{split_name} indices must be a one-dimensional integer array" + ) + return indices.astype(np.int64, copy=False) + + +def _validated_split( + split: tuple[Any, Any], + groups: npt.NDArray[np.int64], + split_name: str, +) -> SplitIndices: + train_indices = _coerce_indices(split[0], "Training") + eval_indices = _coerce_indices(split[1], "Evaluation") + if train_indices.size == 0 or eval_indices.size == 0: + raise ValueError(f"{split_name} must contain non-empty train and eval subsets") + n_rows = groups.shape[0] + if ( + (train_indices < 0).any() + or (eval_indices < 0).any() + or (train_indices >= n_rows).any() + or (eval_indices >= n_rows).any() + ): + raise ValueError(f"{split_name} contains an out-of-range row index") + if ( + np.unique(train_indices).size != train_indices.size + or np.unique(eval_indices).size != eval_indices.size + ): + raise ValueError(f"{split_name} contains duplicate row indices") + if np.intersect1d(train_indices, eval_indices).size: + raise ValueError(f"{split_name} places a row on both sides") + overlapping_groups = np.intersect1d(groups[train_indices], groups[eval_indices]) + if overlapping_groups.size: + raise ValueError( + f"{split_name} places group {overlapping_groups[0]} on both sides" + ) + return train_indices, eval_indices + + +def _holdout_balance_score( + y: npt.NDArray[Any], eval_indices: npt.NDArray[np.int64], test_size: float +) -> float: + class_codes, classes = pd.factorize(pd.Series(y), sort=False) + overall = np.bincount(class_codes, minlength=len(classes)) / len(class_codes) + evaluation = np.bincount(class_codes[eval_indices], minlength=len(classes)) / len( + eval_indices + ) + size_difference = abs(len(eval_indices) / len(y) - test_size) + return float(size_difference + np.abs(overall - evaluation).sum()) + + +def holdout_indices( + X: npt.NDArray[Any], + y: npt.NDArray[Any], + task: str, + groups: npt.ArrayLike | None = None, + *, + test_size: float = _DEFAULT_TEST_SIZE, + random_state: int = 42, +) -> SplitIndices: + _validate_task(task) + if not 0 < test_size < 1: + raise ValueError("test_size must be between 0 and 1") + feature_values, target_values, normalized_groups = _validate_split_inputs( + X, y, groups + ) + + if task == TABULAR_CLASSIFICATION_TASK: + requested_splits = max(2, round(1 / test_size)) + n_splits = _classification_split_count( + target_values, normalized_groups, requested_splits + ) + splitter = StratifiedGroupKFold(n_splits=n_splits) + split_targets = _classification_codes(target_values) + candidates = [ + _validated_split(split, normalized_groups, "Holdout split") + for split in splitter.split( + feature_values, split_targets, normalized_groups + ) + ] + return min( + candidates, + key=lambda split: _holdout_balance_score( + target_values, split[1], test_size + ), + ) + + splitter = GroupShuffleSplit( + n_splits=1, test_size=test_size, random_state=random_state + ) + split = next(splitter.split(feature_values, target_values, normalized_groups)) + return _validated_split(split, normalized_groups, "Holdout split") + + +def cross_validation_indices( + X: npt.NDArray[Any], + y: npt.NDArray[Any], + task: str, + groups: npt.ArrayLike | None = None, + *, + cv: BaseCrossValidator | None = None, + n_splits: int = _DEFAULT_CV_SPLITS, + random_state: int = 42, +) -> list[SplitIndices]: + _validate_task(task) + if n_splits < 2: + raise ValueError("n_splits must be at least 2") + feature_values, target_values, normalized_groups = _validate_split_inputs( + X, y, groups + ) + + if cv is None and task == TABULAR_CLASSIFICATION_TASK: + split_count = _classification_split_count( + target_values, normalized_groups, n_splits + ) + splitter: BaseCrossValidator = StratifiedGroupKFold(n_splits=split_count) + split_targets = _classification_codes(target_values) + elif cv is None: + split_count = min(n_splits, np.unique(normalized_groups).size) + splitter = GroupKFold(n_splits=split_count) + split_targets = target_values + else: + splitter = cv + split_targets = target_values + + splits = [ + _validated_split(split, normalized_groups, f"Cross-validation split {index}") + for index, split in enumerate( + splitter.split(feature_values, split_targets, normalized_groups), start=1 + ) + ] + if not splits: + raise ValueError("Cross-validator produced no splits") + return splits + + +def out_of_fold_indices( + X: npt.NDArray[Any], + y: npt.NDArray[Any], + task: str, + groups: npt.ArrayLike | None = None, + *, + n_splits: int = _DEFAULT_CV_SPLITS, + random_state: int = 42, +) -> list[SplitIndices]: + if resolve_evaluation_strategy(len(X)) == "holdout": + return [ + holdout_indices( + X, + y, + task, + groups, + random_state=random_state, + ) + ] + return cross_validation_indices( + X, + y, + task, + groups, + n_splits=n_splits, + random_state=random_state, + ) + + +def _row_hashes(X: npt.NDArray[Any], y: npt.NDArray[Any]) -> npt.NDArray[np.uint64]: + combined = np.column_stack((X, y.reshape(-1, 1))) + hashes = pd.util.hash_pandas_object(pd.DataFrame(combined), index=False).to_numpy( + dtype=np.uint64 + ) + return hashes + + +def _consume_row_indices( + hashes: npt.NDArray[np.uint64], + available: dict[int, deque[int]], + subset_name: str, +) -> npt.NDArray[np.int64]: + indices: list[int] = [] + for row_hash in hashes: + matches = available[int(row_hash)] + if not matches: + raise ValueError( + f"Callable splitter returned a {subset_name} row not present in the input" + ) + indices.append(matches.popleft()) + return np.asarray(indices, dtype=np.int64) + + +def _array_split_indices( + result: Sequence[Any], + X: npt.NDArray[Any], + y: npt.NDArray[Any], +) -> SplitIndices: + train_X = np.asarray(result[0]) + eval_X = np.asarray(result[1]) + train_y = np.asarray(result[2]).reshape(-1) + eval_y = np.asarray(result[3]).reshape(-1) + if ( + train_X.ndim != 2 + or eval_X.ndim != 2 + or train_X.shape[1] != X.shape[1] + or eval_X.shape[1] != X.shape[1] + or train_X.shape[0] != train_y.shape[0] + or eval_X.shape[0] != eval_y.shape[0] + ): + raise ValueError("Callable splitter returned invalid train/eval arrays") + + available: dict[int, deque[int]] = defaultdict(deque) + for index, row_hash in enumerate(_row_hashes(X, y)): + available[int(row_hash)].append(index) + train_indices = _consume_row_indices( + _row_hashes(train_X, train_y), available, "training" + ) + eval_indices = _consume_row_indices( + _row_hashes(eval_X, eval_y), available, "evaluation" + ) + return train_indices, eval_indices + + +def callable_holdout_indices( + splitter: Callable[..., Any], + X: npt.NDArray[Any], + y: npt.NDArray[Any], + groups: npt.ArrayLike | None = None, +) -> SplitIndices: + feature_values, target_values, normalized_groups = _validate_split_inputs( + X, y, groups + ) + result = splitter(feature_values, target_values, normalized_groups.copy()) + if not isinstance(result, (tuple, list)): + raise ValueError("Callable splitter must return a tuple or list") + if len(result) == 2: + split: tuple[Any, Any] = (result[0], result[1]) + elif len(result) == 4: + split = _array_split_indices(result, feature_values, target_values) + else: + raise ValueError( + "Callable splitter must return train/eval indices or four train/eval arrays" + ) + return _validated_split(split, normalized_groups, "Callable split") diff --git a/falcon/tabular/tabular_manager.py b/falcon/tabular/tabular_manager.py deleted file mode 100644 index 7569ae0..0000000 --- a/falcon/tabular/tabular_manager.py +++ /dev/null @@ -1,351 +0,0 @@ -from __future__ import annotations -from falcon.abstract import TaskManager, Pipeline -from falcon.tabular.pipelines.simple_tabular_pipeline import SimpleTabularPipeline -from falcon.tabular.utils import convert_to_np_obj -from .reporting import print_classification_report, print_regression_report -from falcon.tabular.utils import * -from falcon.type_guessing import determine_column_types -from falcon import types as ft -from falcon.types import ColumnTypes -from typing import Union, Optional, List, Tuple, Type, Dict, Any -from numpy import typing as npt -import pandas as pd -from falcon.utils import print_, set_verbosity_level -from sklearn.model_selection import train_test_split -import os -import pandas as pd - - -class TabularTaskManager(TaskManager): - """ - Default task manager for tabular data. - """ - - def __init__( - self, - task: str, - data: Union[str, npt.NDArray, pd.DataFrame, Tuple], - pipeline: Optional[Type[Pipeline]] = None, - pipeline_options: Optional[Dict] = None, - extra_pipeline_options: Optional[Dict] = None, - features: Optional[ft.ColumnsList] = None, - target: Optional[Union[str, int]] = None, - eval_strategy: Optional[Union[str, BaseCrossValidator, Callable]] = "auto", - **options: Any, - ) -> None: - """ - - Parameters - ---------- - task : str - `tabular_classification` or `tabular_regression` - data : Union[str, npt.NDArray, pd.DataFrame, Tuple] - path to data file or pandas dataframe or numpy array or tuple (X,y) - pipeline: Optional[Type[Pipeline]] - class to be used as pipeline, by default None. - If None, `SimpleTabularPipeline` will be used - pipeline_options : Optional[Dict], optional - arguments to be passed to the pipeline, by default None. - These options will overwrite the ones from `default_pipeline_options` attribute. - extra_pipeline_options : Optional[Dict], optional - arguments to be passed to the pipeline, by default None. - These options will be passed in addition to the ones from `default_pipeline_options` attribute. - This argument is ignored if `pipeline_options` is not None - features : Optional[ft.ColumnsList], optional - names or indices of columns to be used as features, by default None. - If None, all columns except the last one will be used. - If `target` argument is not None, features should be passed explicitly as well - target : Optional[Union[str, int]], optional - name or index of column to be used as target, by default None. - If None, the last column will be used as target. - If `features` argument is not None, target should be specified explicitly as well - eval_strategy : Optional[Union[str, BaseCrossValidator, Callable]], optional - evaluation strategy, can be one of {'auto', 'holdout' 'cv', BaseCrossValidator, Callable} by default 'auto'. - If 'auto', uses 5 fold CV for small datasets and holdout for large ones. - If 'holdout', uses holdout strategy with 25% of data for validation. - If 'cv', uses 5 fold CV. - If BaseCrossValidator, uses the specified cross-validator. - If Callable, uses the specified function to split data into train and validation sets. - If None, no evaluation will be performed. - """ - print_(f"\nInitializing a new TabularTaskManager for task `{task}`") - self._data: Tuple[npt.NDArray, npt.NDArray, List[ColumnTypes]] - # self._pipeline: Pipeline - super().__init__( - task=task, - data=data, - pipeline=pipeline, - pipeline_options=pipeline_options, - extra_pipeline_options=extra_pipeline_options, - features=features, - target=target, - ) - - self._eval_set: Optional[Tuple] = None - self._stored_cv_score: Optional[Dict] = None - self.eval_strategy = eval_strategy - if not self._validate_eval_strategy(): - raise ValueError( - f"Invalid value for `eval_strategy` argument: {self.eval_strategy}" - ) - - def _validate_eval_strategy(self) -> bool: - if self.eval_strategy is None: - return True - if self.eval_strategy in ("auto", "cv", "holdout"): - return True - if isinstance(self.eval_strategy, BaseCrossValidator): - return True - if callable(self.eval_strategy): - return True - return False - - def _infer_feature_names(self, data: Any) -> None: - print(type(data)) - if ( - isinstance(data, pd.DataFrame) - and self.features is None - and self.target is not None - ): - self.features: Optional[Union[List[str], List[int]]] = [c for c in data.columns if c != self.target] - - - def _prepare_data( - self, data: Union[str, npt.NDArray, pd.DataFrame, Tuple], training: bool = True - ) -> Tuple[npt.NDArray, npt.NDArray, List[ColumnTypes]]: - """ - Initial data preparation: - 1) optional: read data from the specified location; - 2) split into features and targets. By default it is assumed that the last column is the target; - 3) clean data; - 4) determine numerical and categorical features (create categorical mask). - - Parameters - ---------- - data : Union[str, npt.NDArray, pd.DataFrame, Tuple] - path to data file or pandas dataframe or numpy array or Tuple(X,y) - - Returns - ------- - Tuple[npt.NDArray, npt.NDArray, List[ColumnTypes]] - tuple of features, target and type mask for features - """ - if isinstance(data, str): - data = read_data(data) - self._infer_feature_names(data) - if isinstance(data, tuple): - if self.features is not None or self.target is not None: - print( - "When data is passed as tuple of (X, y) all columns are used regardless the values of `features` or `target` arguments." - ) - if len(data) != 2: - raise ValueError( - "When passing data as tuple, it should contain exactly 2 elements: `X` and `y`." - ) - X, y = data - if isinstance(X, pd.DataFrame): - self.feature_names_to_save = list(X.columns) - if len(y.shape) > 3 or len(X.shape) > 3: - raise ValueError("Invalid data shape.") - if len(y.shape) > 1 and y.shape[-1] != 1: - raise ValueError("The target should contain only one column.") - X, y = convert_to_np_obj(X), convert_to_np_obj(y) - else: - self._infer_feature_names(data) - X, y = split_features(data, features=self.features, target=self.target) - if self.features is None and isinstance(data, pd.DataFrame): - self.feature_names_to_save = list(data.columns[:-1]) - elif self.features is not None: - self.feature_names_to_save = self.features - X, y = clean_data_split(X, y) - self.dataset_size = X.shape - mask: List[ColumnTypes] - if training: - mask = determine_column_types(X) - else: - mask = [] - if len(y.shape) == 2: - y = y.ravel() - return X, y, mask - - @property - def default_pipeline(self) -> Type[Pipeline]: - """ - Default pipeline class. - """ - - return SimpleTabularPipeline - - @property - def default_pipeline_options(self) -> Dict: - """ - Default options for pipeline. - """ - options: Dict[str, Any] = {"mask": self._data[2]} - return options - - def _cross_validate(self) -> None: - cv = ( - self.eval_strategy - if isinstance(self.eval_strategy, BaseCrossValidator) - else None - ) - scores = tab_cv_score( - self._pipeline, self._data[0], self._data[1], self.task, cv=cv - ) - scores["N_SAMPLES"] = self.dataset_size[0] - self._stored_cv_score = scores - - def train(self, **kwargs: Any) -> TabularTaskManager: - """ - Invokes the training procedure of an underlying pipeline. - - Returns - ------- - TabularTaskManager - `self` - """ - print_("Beginning training") - if self.eval_strategy is not None: - eval_strategy = self.eval_strategy - split_fn = None - if eval_strategy == "auto": - if self.dataset_size[0] < 2500: - eval_strategy = "cv" - else: - eval_strategy = "holdout" - - if eval_strategy == "holdout": - split_fn = lambda X, y: train_test_split( - X, - y, - test_size=0.25, - stratify=y if self.task == "tabular_classification" else None, - ) - elif callable(eval_strategy) and not isinstance( - eval_strategy, BaseCrossValidator - ): - split_fn = eval_strategy - - if callable(split_fn): - if self._eval_set is None: - X_train, X_eval, y_train, y_eval = split_fn( - self._data[0], self._data[1] - ) - self._data = (X_train, y_train, self._data[2]) - self._eval_set = (X_eval, y_eval) - else: - print_("Evaluation set is already available.") - elif eval_strategy == "cv" or isinstance(eval_strategy, BaseCrossValidator): - print_("Starting cross validation") - self._cross_validate() - print_("Finished cross-validation") - - print_("Beginning the main training phase") - self._pipeline.fit(self._data[0], self._data[1]) - print_("Finished training") - return self - - def predict(self, data: Union[str, npt.NDArray, pd.DataFrame]) -> npt.NDArray: - """ - Performs prediction on new data. - - Parameters - ---------- - data : Union[str, npt.NDArray, pd.DataFrame] - path to data file or pandas dataframe or numpy array - - Returns - ------- - npt.NDArray - predictions - """ - if isinstance(data, str): - data = read_data(data) - if not isinstance(data, np.ndarray): - data = np.asarray(data, dtype=np.object_) - return self._pipeline.predict(data) - - def predict_stored_subset(self, subset: str = "train") -> npt.NDArray: - """ - Makes a prediction on a stored subset (`train` or `eval`). - - Parameters - ---------- - subset : str, optional - subset to predict on (train or eval), by default 'train' - - Returns - ------- - npt.NDArray - predicted values - """ - if subset == "train": - return self.predict(self._data[0]) - elif subset == "eval": - if self._eval_set is None: - raise RuntimeError("validation set is not available") - return self.predict(self._eval_set[0]) - else: - raise ValueError("subset should be either `train` or `eval`") - - def performance_summary( - self, test_data: Optional[Union[str, npt.NDArray, pd.DataFrame, Tuple]] = None - ) -> dict: - """ - Prints a performance summary of the model. - The summary always includes metrics calculated for the train set. - If the train/eval split was done during training, the summary includes metrics calculated on eval set. - If test set is provided as an argument, the performance includes metrics calculated on test set. - - Parameters - ---------- - test_data : Optional[Union[str, npt.NDArray, pd.DataFrame, Tuple]] - data to be used as test set, by default None - - Returns - ------- - dict - metrics for each subset - """ - metrics_ = {} - report_fn = ( - print_classification_report - if self.task == "tabular_classification" - else print_regression_report - ) - y_hat_train = self.predict_stored_subset("train") - metrics_["train"] = report_fn(self._data[1], y_hat_train, silent=True) - if self._eval_set is not None: - y_hat_eval = self.predict_stored_subset("eval") - metrics_["eval"] = report_fn(self._eval_set[1], y_hat_eval, silent=True) - if self._stored_cv_score is not None: - metrics_["eval_cv"] = self._stored_cv_score - if test_data is not None: - metrics_["test"] = self.evaluate(test_data, silent=True) - df = pd.DataFrame.from_dict(metrics_, orient="index") - print("\n", df, "\n") - return metrics_ - - def evaluate( - self, - test_data: Union[str, npt.NDArray, pd.DataFrame, Tuple], - silent: bool = False, - ) -> Dict: - """ - Perfoms and prints the evaluation report on the given dataset. - - Parameters - ---------- - test_data : Union[str, npt.NDArray, pd.DataFrame, Tuple] - dataset to be used for evaluation - silent: bool - controls whether the metrics are printed on screen, by default False - """ - print("The evaluation report will be provided here") - X, y, _ = self._prepare_data(test_data, training=False) - y_hat = self.predict(X) - if self.task == "tabular_classification": - return print_classification_report(y, y_hat, silent=silent) - else: - return print_regression_report(y, y_hat, silent=silent) diff --git a/falcon/tabular/training.py b/falcon/tabular/training.py new file mode 100644 index 0000000..d2e3b35 --- /dev/null +++ b/falcon/tabular/training.py @@ -0,0 +1,413 @@ +from __future__ import annotations + +from collections.abc import Sequence +from time import monotonic +from typing import Any + +import numpy as np +from numpy import typing as npt + +from falcon.config import RunConfig +from falcon.constants import TABULAR_CLASSIFICATION_TASK, TABULAR_REGRESSION_TASK +from falcon.serialization import SerializedModelRepr +from falcon.tabular.calibration import ( + fit_temperature, + serialize_temperature_scaling, + temperature_scale_probabilities, +) +from falcon.tabular.candidates import ( + CandidateModel, + CandidateTrainer, + EnsembleRun, + EstimatorSpec, + GreedyWeightedEnsemble, + OOFEnsembleTrainer, + score_oof_predictions, +) +from falcon.tabular.conformal import ( + fit_conformal_quantile, + serialize_conformal_interval, +) +from falcon.tabular.decision import fit_decision_weights, serialize_decision_rule +from falcon.types import DatasetSchema, Float32Array, Int64Array + +SplitIndices = tuple[npt.NDArray[np.int64], npt.NDArray[np.int64]] + + +class CandidateLearner: + def __init__( + self, + task: str, + dataset_size: tuple[int, ...], + config: RunConfig, + evaluation_splits: Sequence[SplitIndices] | None = None, + ) -> None: + self.task = task + self.dataset_size = dataset_size + self.config = config + self.evaluation_splits = ( + None if evaluation_splits is None else tuple(evaluation_splits) + ) + self.model: CandidateModel | GreedyWeightedEnsemble | None = None + self._ensemble_run: EnsembleRun | None = None + self._evaluation_run: EnsembleRun | None = None + self._leaderboard: list[dict[str, str | float]] = [] + self.temperature_: float | None = None + self.conformal_quantile_: float | None = None + self.decision_weights_: tuple[float, ...] | None = None + + def _decision_metric(self) -> str | None: + if self.task != TABULAR_CLASSIFICATION_TASK: + return None + return self.config.decision_metric + + def _candidate_specs( + self, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + groups: npt.ArrayLike | None, + schema: DatasetSchema, + ) -> tuple[tuple[EstimatorSpec, ...], float]: + n_classes = ( + int(np.unique(y).size) if self.task == TABULAR_CLASSIFICATION_TASK else None + ) + started_at = monotonic() + specs: list[EstimatorSpec] = [] + for source in self.config.candidate_sources: + remaining_time = self._remaining_time_limit(monotonic() - started_at) + specs.extend( + source.get_candidates( + self.task, + X=X, + y=y, + groups=groups, + n_classes=n_classes, + n_splits=self.config.oof_folds, + time_limit=remaining_time, + random_state=self.config.random_state, + schema=schema, + dataset_aware_ordering=self.config.dataset_aware_ordering, + config=self.config, + ) + ) + if not specs: + raise ValueError("Candidate sources produced no estimator specifications") + return tuple(specs), monotonic() - started_at + + def _remaining_time_limit(self, elapsed: float) -> float | None: + if self.config.time_limit is None: + return None + return max(float(np.finfo(float).eps), self.config.time_limit - elapsed) + + def _ensemble_trainer( + self, + *, + time_limit: float | None, + max_iterations: int | None = None, + ) -> OOFEnsembleTrainer: + return OOFEnsembleTrainer( + self.task, + max_iterations=( + self.config.ensemble_max_iterations + if max_iterations is None + else max_iterations + ), + plateau_enabled=self.config.plateau_enabled, + plateau_patience=self.config.plateau_patience, + plateau_tolerance=self.config.plateau_tolerance, + n_splits=self.config.oof_folds, + time_limit=time_limit, + random_state=self.config.random_state, + class_weight=self.config.class_weight, + prior_correct=self.config.prior_correct, + ) + + def _fit_ensemble( + self, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + specs: tuple[EstimatorSpec, ...], + groups: npt.ArrayLike | None, + time_limit: float | None, + ) -> None: + run = self._ensemble_trainer(time_limit=time_limit).fit( + X, + y, + specs=specs, + groups=groups, + splits=self.evaluation_splits, + ) + self._ensemble_run = run + self.model = run.ensemble + self._leaderboard = self._run_leaderboard(run) + + def _run_leaderboard(self, run: EnsembleRun) -> list[dict[str, str | float]]: + return [ + { + "candidate": candidate.spec.name, + "family": candidate.spec.family, + "score": candidate.oof_score, + "fit_time": candidate.fit_time, + "weight": weight, + } + for candidate, weight in zip( + run.candidates, run.ensemble.weights, strict=True + ) + ] + + def _fit_best_candidate( + self, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + specs: tuple[EstimatorSpec, ...], + groups: npt.ArrayLike | None, + time_limit: float | None, + ) -> None: + winner_spec = specs[0] + refit_time_limit = time_limit + needs_oof = ( + len(specs) > 1 + or self.evaluation_splits is not None + or self.config.calibrate + or self.config.conformal_alpha is not None + or self._decision_metric() is not None + ) + if needs_oof: + # max_iterations=1 keeps greedy selection at its seed, so the ensemble + # weights are 1.0 on the best-scoring candidate and 0.0 elsewhere. + selection_run = self._ensemble_trainer( + time_limit=time_limit, + max_iterations=1, + ).fit(X, y, specs=specs, groups=groups, splits=self.evaluation_splits) + self._evaluation_run = selection_run + winner_spec = selection_run.candidates[ + int(np.argmax(selection_run.ensemble.weights)) + ].spec + if time_limit is not None: + refit_time_limit = max( + float(np.finfo(float).eps), + time_limit - selection_run.elapsed_time, + ) + + refit_run = CandidateTrainer( + self.task, + time_limit=refit_time_limit, + random_state=self.config.random_state, + class_weight=self.config.class_weight, + ).fit(X, y, specs=(winner_spec,), groups=groups) + winner = refit_run.candidates[0] + self.model = winner.model + if self._evaluation_run is None: + if self.task == TABULAR_CLASSIFICATION_TASK: + predict_proba = getattr(winner.model, "predict_proba", None) + if not callable(predict_proba): + raise TypeError( + "Classification candidate models must expose predict_proba" + ) + predictions = predict_proba(X) + else: + predictions = winner.model.predict(X) + self._leaderboard = [ + { + "candidate": winner.spec.name, + "family": winner.spec.family, + "score": score_oof_predictions( + predictions, + y, + self.task, + prior_correct=self.config.prior_correct, + ), + "fit_time": winner.fit_time, + "weight": 1.0, + } + ] + else: + self._leaderboard = self._run_leaderboard(self._evaluation_run) + + def fit( + self, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + schema: DatasetSchema, + *, + groups: npt.ArrayLike | None = None, + ) -> None: + if self.config.calibrate and self.task != TABULAR_CLASSIFICATION_TASK: + raise ValueError( + "Probability calibration is only available for classification" + ) + if ( + self.config.conformal_alpha is not None + and self.task != TABULAR_REGRESSION_TASK + ): + raise ValueError( + "Conformal prediction intervals are only available for regression" + ) + if ( + self.config.decision_metric is not None + and self.task != TABULAR_CLASSIFICATION_TASK + and "decision_metric" in self.config._provided_fields + ): + raise ValueError( + "A tuned decision rule is only available for classification" + ) + specs, source_elapsed = self._candidate_specs(X, y, groups, schema) + training_time_limit = self._remaining_time_limit(source_elapsed) + if self.config.ensemble_enabled: + self._fit_ensemble(X, y, specs, groups, training_time_limit) + else: + self._fit_best_candidate(X, y, specs, groups, training_time_limit) + if self.config.calibrate: + oof_result = self._weighted_oof_predictions() + if oof_result is None: + raise RuntimeError("OOF probabilities are unavailable for calibration") + evaluation_indices, probabilities = oof_result + self.temperature_ = fit_temperature( + probabilities, + np.asarray(y)[evaluation_indices], + ) + decision_metric = self._decision_metric() + if decision_metric is not None: + oof_result = self._weighted_oof_predictions() + if oof_result is None: + raise RuntimeError( + "OOF probabilities are unavailable for the decision rule" + ) + evaluation_indices, probabilities = oof_result + self._assert_classes_are_encoded() + # The rule is tuned on calibrated probabilities because the graph applies + # it downstream of the temperature Softmax, and in multiclass a weighted + # argmax is not invariant to temperature. + if self.temperature_ is not None: + probabilities = temperature_scale_probabilities( + probabilities, + self.temperature_, + ) + weights = fit_decision_weights( + probabilities, + np.asarray(y)[evaluation_indices], + decision_metric, + ) + # An all-ones vector is the guard's no-op result. Keeping it as "no rule" + # avoids inert graph nodes and leaves prediction on the model's own argmax. + if any(weight != 1.0 for weight in weights): + self.decision_weights_ = weights + if self.config.conformal_alpha is not None: + oof_result = self._weighted_oof_predictions() + if oof_result is None: + raise RuntimeError( + "OOF predictions are unavailable for conformal intervals" + ) + evaluation_indices, predictions = oof_result + self.conformal_quantile_ = fit_conformal_quantile( + predictions, + np.asarray(y)[evaluation_indices], + self.config.conformal_alpha, + ) + + def _fitted_model(self) -> CandidateModel | GreedyWeightedEnsemble: + if self.model is None: + raise RuntimeError("The candidate learner has not been fitted") + return self.model + + def _assert_classes_are_encoded(self) -> None: + classes = getattr(self._fitted_model(), "classes", None) + if classes is None: + return + expected = np.arange(len(classes), dtype=np.int64) + if not np.array_equal(np.asarray(classes), expected): + raise RuntimeError( + "The tuned decision rule requires contiguous encoded class labels" + ) + + def transform(self, X: npt.NDArray[Any]) -> npt.NDArray[Any]: + if self.decision_weights_ is None: + return self._fitted_model().predict(X) + # predict_proba applies the temperature; predict does not, and a weighted + # argmax is not temperature-invariant, so the rule must read the scaled scores. + weights = np.asarray(self.decision_weights_, dtype=np.float32) + return np.argmax(self.predict_proba(X) * weights, axis=1).astype(np.int64) + + def predict_proba(self, X: npt.NDArray[Any]) -> npt.NDArray[np.float32]: + if self.task != TABULAR_CLASSIFICATION_TASK: + raise RuntimeError("Regression predictors do not expose probabilities") + predict_proba = getattr(self._fitted_model(), "predict_proba", None) + if not callable(predict_proba): + raise RuntimeError("The fitted classification model has no probabilities") + probabilities = np.asarray(predict_proba(X), dtype=np.float32) + if self.temperature_ is None: + return probabilities + return temperature_scale_probabilities(probabilities, self.temperature_) + + def _weighted_oof_predictions( + self, + ) -> tuple[npt.NDArray[np.int64], npt.NDArray[np.float32]] | None: + run = self._ensemble_run or self._evaluation_run + if run is None: + return None + weighted = np.asarray( + np.sum( + np.stack( + [ + candidate.oof_predictions * np.float32(weight) + for candidate, weight in zip( + run.candidates, run.ensemble.weights, strict=True + ) + ], + axis=0, + ), + axis=0, + dtype=np.float32, + ) + ) + return run.evaluation_indices.copy(), weighted + + def oof_predictions( + self, + ) -> tuple[npt.NDArray[np.int64], npt.NDArray[Any]] | None: + oof_result = self._weighted_oof_predictions() + if oof_result is None: + return None + evaluation_indices, weighted = oof_result + if self.task == TABULAR_CLASSIFICATION_TASK: + run = self._ensemble_run or self._evaluation_run + if run is None: + raise RuntimeError("Classification OOF run is unavailable") + if run.ensemble.classes is None: + raise RuntimeError("Classification classes are unavailable") + scores = weighted + if self.temperature_ is not None: + scores = temperature_scale_probabilities(scores, self.temperature_) + if self.decision_weights_ is not None: + scores = scores * np.asarray(self.decision_weights_, dtype=np.float32) + predictions: npt.NDArray[Any] = run.ensemble.classes[ + np.argmax(scores, axis=1) + ] + else: + predictions = weighted.reshape(-1) + return evaluation_indices, predictions + + def leaderboard_records(self) -> list[dict[str, str | float]]: + return [record.copy() for record in self._leaderboard] + + def serialize(self) -> SerializedModelRepr: + serialized = self._fitted_model().serialize() + if self.temperature_ is not None: + serialized = serialize_temperature_scaling(serialized, self.temperature_) + if self.decision_weights_ is not None: + serialized = serialize_decision_rule(serialized, self.decision_weights_) + if self.conformal_quantile_ is not None: + serialized = serialize_conformal_interval( + serialized, + self.conformal_quantile_, + ) + return serialized + + def get_input_type(self) -> object: + return Float32Array + + def get_output_type(self) -> object: + return Int64Array if self.task == TABULAR_CLASSIFICATION_TASK else Float32Array + + +__all__ = ["CandidateLearner", "SplitIndices"] diff --git a/falcon/tabular/utils.py b/falcon/tabular/utils.py deleted file mode 100644 index 45fda74..0000000 --- a/falcon/tabular/utils.py +++ /dev/null @@ -1,164 +0,0 @@ -from copy import deepcopy -import pandas as pd -from typing import Union, Tuple, Optional, List, Callable, Dict -import numpy as np -from numpy import isin, typing as npt -from falcon import types as ft -from falcon.abstract.task_pipeline import Pipeline -from falcon.types import ColumnTypes -from sklearn.model_selection import RepeatedStratifiedKFold, RepeatedKFold -from sklearn.metrics import balanced_accuracy_score, r2_score -from sklearn.model_selection._split import BaseCrossValidator -from falcon.tabular.reporting import print_classification_report, print_regression_report - -def read_data(path: str) -> pd.DataFrame: - if path.endswith(".csv"): - data = pd.read_csv(path) - elif path.endswith(".parquet"): - data = pd.read_parquet(path) - else: - raise ValueError("Only `.csv` and `.parquet` files are supported") - - return data - - -def clean_data( - data: Union[pd.DataFrame, npt.NDArray] -) -> Union[pd.DataFrame, npt.NDArray]: - if isinstance(data, pd.DataFrame): - return data.dropna() - else: - mask = pd.isnull(data) - keep = [] - for row in mask: - if True in row: - keep.append(False) - else: - keep.append(True) - data = data[keep, :] - return data - - -def clean_data_split(X: npt.NDArray, y: npt.NDArray) -> Tuple[npt.NDArray, npt.NDArray]: - mask_x = pd.isnull(X) - mask_y = pd.isnull(y) - keep = [] - for i in range(len(mask_x)): - if len(y.shape) == 1: - if True not in mask_x[i] and mask_y[i] == False: - keep.append(True) - else: - keep.append(False) - else: - if True not in mask_x[i] and True not in mask_y[i]: - keep.append(True) - else: - keep.append(False) - X = X[keep, :] - if len(y.shape) == 1: - y = y[keep] - else: - y = y[keep, :] - return X, y - - -def convert_to_np_obj( - data: Union[pd.DataFrame, npt.NDArray] -) -> npt.NDArray[np.object_]: - if isinstance(data, pd.DataFrame): - return data.to_numpy(dtype=np.object_) - else: - return data.astype(np.object_) - - -def split_features( - data: Union[pd.DataFrame, npt.NDArray], - features: Optional[ft.ColumnsList], - target: Optional[Union[str, int]], -) -> Tuple[npt.NDArray[np.object_], npt.NDArray[np.object_]]: - if features is not None and len(features) < 1: - ValueError("Features List cannot be empty") - if ( - isinstance(data, np.ndarray) - and features is not None - and isinstance(features[0], str) - ): - ValueError("Expected list of integers as features, found strings") - if isinstance(data, np.ndarray) and isinstance(target, str): - ValueError("Expected integer as target, found string") - - # TODO: provide a proper fix instead of a ValuError - if (target is None or features is None) and not ( - target is None and features is None - ): - raise ValueError( - "Either both target and features should be provided or neither of them." - ) - if isinstance(data, pd.DataFrame): - if features is None: - X = data.iloc[:, :-1] - elif isinstance(features[0], str): - X = data[features] - else: - X = data.iloc[:, features] - - if target is None: - y = data.iloc[:, -1] - elif isinstance(target, str): - y = data[[target]] - else: - y = data.iloc[:, target] - - X = X.to_numpy() - y = y.to_numpy() - - else: # Numpy - if features is None: - X = data[:, :-1] - else: - X = data[:, np.asarray(features, dtype=np.int64)] - - if target is None: - y = data[:, -1] - else: - y = data[:, np.asarray(target, dtype=np.int64)] - - return convert_to_np_obj(X), convert_to_np_obj(y) - - -def calculate_model_score(y: npt.NDArray, y_hat: npt.NDArray, task: str) -> float: - if task == "tabular_classification": - return balanced_accuracy_score(y.astype(np.str_), y_hat) - else: - score = r2_score(y, y_hat) - if score < 0: - score = 0 - score = (score + 1) / 2 - return score - - -def tab_cv_score( - pipeline: Pipeline, X: npt.NDArray, y: npt.NDArray, task: str, cv: Optional[BaseCrossValidator] = None, -) -> Dict[str, float]: - if cv is not None: - if not isinstance(cv, BaseCrossValidator): - raise ValueError("cv should be an instance of BaseCrossValidator") - kf = cv - elif task == "tabular_classification": - kf = RepeatedStratifiedKFold(n_splits=5, n_repeats=1) - y = y.astype(np.str_) - else: - kf = RepeatedKFold(n_splits=5, n_repeats=1) - scores = [] - for train_index, test_index in kf.split(X, y): - copied_pipeline = deepcopy(pipeline) - X_train, X_test = X[train_index], X[test_index] - y_train, y_test = y[train_index], y[test_index] - copied_pipeline.fit(X_train, y_train) - pred = copied_pipeline.predict(X_test) - report_fn = print_classification_report if task == 'tabular_classification' else print_regression_report - scores.append(report_fn(y_test, pred, silent = True)) - mean_scores = {} - for key in scores[0].keys(): - mean_scores[key] = np.mean([score[key] for score in scores]) - return mean_scores diff --git a/falcon/tabular/wrappers.py b/falcon/tabular/wrappers.py deleted file mode 100644 index 363c4a0..0000000 --- a/falcon/tabular/wrappers.py +++ /dev/null @@ -1,52 +0,0 @@ -from typing import Type, Any -import numpy as np -from numpy import typing as npt -from sklearn.base import ( - BaseEstimator as _BaseEstimator, - RegressorMixin as _RegressorMixin, -) -from skl2onnx import convert_sklearn -from skl2onnx.common.data_types import TensorType, FloatTensorType -from falcon.abstract.model import Model as _Model -from falcon.abstract.onnx_convertible import ONNXConvertible as _ONNXConvertible -from falcon.types import Float32Array, Int64Array -from falcon.serialization import SerializedModelRepr as _SerializedModelRepr -from falcon.config import ONNX_OPSET_VERSION, ML_ONNX_OPSET_VERSION - -class SklearnRegressorWrapper(_Model, _ONNXConvertible): - def __init__(self, regressor: Type[_RegressorMixin], **kwargs: Any): - if not issubclass(regressor, _RegressorMixin): - raise TypeError( - "Regressor must be a subclass of sklearn.base.RegressorMixin" - ) - self._regressor = regressor - self._kwargs = kwargs - - def fit(self, X: Any, y: Any, *args: Any, **kwargs: Any) -> None: - self._model = self._regressor(**self._kwargs) - self._model.fit(X, y) - self._shape = [None, *X.shape[1:]] - - def predict(self, X: npt.NDArray, *args: Any, **kwargs: Any) -> npt.NDArray: - return self._model.predict(X).astype(np.float32) - - def to_onnx(self) -> _SerializedModelRepr: - """ - Serializes the model to onnx. - - Returns - ------- - SerializedModelRepr - """ - initial_type = [("model_input", FloatTensorType(self._shape))] - onnx_model = convert_sklearn( - self._model, - initial_types=initial_type, - target_opset={"": ONNX_OPSET_VERSION, "ai.onnx.ml": ML_ONNX_OPSET_VERSION}, - ) - n_inputs = len(onnx_model.graph.input) - n_outputs = len(onnx_model.graph.output) - - return _SerializedModelRepr( - onnx_model, n_inputs, n_outputs, ["FLOAT32"], [self._shape] - ) \ No newline at end of file diff --git a/falcon/task_configurations.py b/falcon/task_configurations.py deleted file mode 100644 index 9a05ef2..0000000 --- a/falcon/task_configurations.py +++ /dev/null @@ -1,201 +0,0 @@ -from copy import deepcopy -from typing import Dict, List, Type -import os -from falcon.constants import ( - TABULAR_CLASSIFICATION_TASK as _TAB_CLF_TASK, - TABULAR_REGRESSION_TASK as _TAB_REGR_TASK, -) -from falcon.tabular.configurations import ( - TABULAR_CLASSIFICATION_CONFIGURATIONS as _TAB_CLF_CONF, - TABULAR_REGRESSION_CONFIGURATIONS as _TAB_REGR_CONF, -) -from falcon.abstract.task_manager import TaskManager as _TaskManager -from falcon.tabular.tabular_manager import TabularTaskManager as _TabularTaskManager - -_PREFIX = "falcon_ml_" - - -def _prevent_load() -> bool: - return bool(os.getenv("FALCON_PREVENT_EXTENSION_AUTO_LOAD", False)) - - -class TaskConfigurationRegistry: - - """ - Central registry holding pre-defined configurations for the tasks. - """ - - _CONFIGURATIONS: Dict[str, Dict] = {} - - @classmethod - def register_task(cls, task: str, task_manager: Type[_TaskManager]) -> None: - """ - Registers a new task. - - Parameters - ---------- - task : str - name of the task (e.g. `tabular_regression`) - task_manager : Type[TaskManager] - TaskManager responsible for handling the task - """ - if task not in cls._CONFIGURATIONS.keys(): - if not issubclass(task_manager, _TaskManager): - raise ValueError( - "Invalid task manager. Task manager should be a subclass of `falcon.base.manager.TaskManager`" - ) - cls._CONFIGURATIONS[task] = {"manager": task_manager, "configs": {}} - else: - print(f"Task {task} already exists and will not be registered again.") - - @classmethod - def get_registered_tasks(cls) -> List[str]: - """ - Returns the list of registered tasks. - - Returns - ------- - List[str] - list of registered tasks - """ - return list(cls._CONFIGURATIONS.keys()) - - @classmethod - def is_known_task(cls, task: str) -> bool: - """ - Parameters - ---------- - task : str - the name of the task - - Returns - ------- - bool - True if the task is registered, else False - """ - return task in cls._CONFIGURATIONS.keys() - - @classmethod - def register_configurations( - cls, task: str, config: Dict, silent: bool = False - ) -> None: - """ - Register configuration for the task. - - Parameters - ---------- - task : str - the name of the task - config : Dict - the name of the configuration, should follow the naming scheme `EXTENSION_NAME::config_name` - silent : bool, optional - prints config name on registration if True, by default False - """ - if not cls.is_known_task(task): - raise ValueError( - f"The task {task} does not exist. Please register it first using TaskConfigurationRegistry.register_task method." - ) - if not silent: - print(f"Registered {list(config.keys())} for task {task}") - cls._CONFIGURATIONS[task]["configs"].update(deepcopy(config)) - - @classmethod - def get_configuration( - cls, task: str, configuration_name: str, allow_extensions_discovery: bool = True - ) -> Dict: - """ - Parameters - ---------- - task : str - the name of the task - configuration_name : str - the name of the configuration - allow_extensions_discovery : bool, optional - if True falcon will try to import an extension module for a given config (config module is determined based on config name), by default True - - Returns - ------- - Dict - task configuration - """ - if not cls.is_known_task(task): - raise ValueError(f"Unknown task `{task}`") - elif configuration_name not in cls._CONFIGURATIONS[task]["configs"].keys(): - should_load = ( - allow_extensions_discovery - and not _prevent_load() - and "::" in configuration_name - ) - if should_load: - - extension_name = configuration_name.split("::")[0] - print( - f"Extension `{_PREFIX + extension_name.lower()}` does not seem to be loaded. Will try to load automatically." - ) - cls.load_extension(extension_name=extension_name) - return cls.get_configuration(task, configuration_name, False) - raise ValueError(f"Configuration `{configuration_name}` does not exist") - return deepcopy(cls._CONFIGURATIONS[task]["configs"][configuration_name]) - - @classmethod - def get_registered_config_names(cls, task: str) -> List[str]: - """ - Parameters - ---------- - task : str - the name of the task - - Returns - ------- - List[str] - a list of registered configuration names for a given task - """ - if not cls.is_known_task(task): - raise ValueError(f"Unknown task `{task}`") - return cls._CONFIGURATIONS[task]["configs"].keys() - - @classmethod - def get_task_manager(cls, task: str) -> Type[_TaskManager]: - """ - Parameters - ---------- - task : str - the name of the task - - Returns - ------- - Type[TaskManager] - TaskNanager class for the given task - """ - if not cls.is_known_task(task): - raise ValueError(f"Unknown task `{task}`") - return cls._CONFIGURATIONS[task]["manager"] - - @classmethod - def load_extension(cls, extension_name: str) -> None: - """ - Imports the extension module, module name should follow the naming scheme `falcon_ml_`. - - Parameters - ---------- - extension_name : str - the name of the extension - """ - extension_name = extension_name.lower() - print(f"Attempting to load {_PREFIX + extension_name}...") - try: - __import__(_PREFIX + extension_name).self_register() - except ModuleNotFoundError: - print( - f"Seems like the extension `{extension_name}` is not installed. Try installing it first using `pip install {_PREFIX+extension_name}`." - ) - - -TaskConfigurationRegistry.register_task(_TAB_CLF_TASK, _TabularTaskManager) -TaskConfigurationRegistry.register_task(_TAB_REGR_TASK, _TabularTaskManager) - -TaskConfigurationRegistry.register_configurations(_TAB_CLF_TASK, _TAB_CLF_CONF, silent = True) -TaskConfigurationRegistry.register_configurations(_TAB_REGR_TASK, _TAB_REGR_CONF, silent = True) - -# for backward compatibility -get_task_configuration = TaskConfigurationRegistry.get_configuration diff --git a/falcon/type_guessing.py b/falcon/type_guessing.py index db95f90..6b5c6cf 100644 --- a/falcon/type_guessing.py +++ b/falcon/type_guessing.py @@ -1,9 +1,11 @@ -from numpy import typing as npt +import re +from typing import Any + import numpy as np import pandas as pd -from typing import List, Optional, Any +from numpy import typing as npt + from falcon.types import ColumnTypes -import re NUM_CAT_THRESHOLD: int = 10 HIGH_CARD_THRESHOLD: int = 100 @@ -19,7 +21,7 @@ def _fullmatch(expr: str, x: Any) -> bool: return fm -def _determine_date_type(X: pd.DataFrame, column: int) -> Optional[ColumnTypes]: +def _determine_date_type(X: pd.DataFrame, column: int) -> ColumnTypes | None: if ( pd.to_datetime(X.iloc[:, column], format=r"%Y-%m-%d", errors="coerce") .notnull() @@ -29,32 +31,35 @@ def _determine_date_type(X: pd.DataFrame, column: int) -> Optional[ColumnTypes]: return None -def determine_column_types(data: npt.NDArray) -> List[ColumnTypes]: - mask: List[ColumnTypes] = [] +def determine_column_types(data: npt.NDArray[Any]) -> list[ColumnTypes]: + mask: list[ColumnTypes] = [] tmp_df: pd.DataFrame = pd.DataFrame(data).infer_objects() - # print(tmp_df.dtypes.apply(lambda x: x.name).to_dict()) for col in range(tmp_df.shape[-1]): + values = tmp_df.iloc[:, col].dropna() + if values.empty: + raise ValueError(f"Cannot infer type for all-missing column {col}") determined_type = None - if tmp_df.iloc[:, col].map(lambda x: isinstance(x, NP_NUMERIC_TYPES)).all(): - if len(tmp_df.iloc[:, col].unique().tolist()) > NUM_CAT_THRESHOLD: + if values.map(lambda x: isinstance(x, NP_NUMERIC_TYPES)).all(): + if values.nunique() > NUM_CAT_THRESHOLD: determined_type = ColumnTypes.NUMERIC_REGULAR else: determined_type = ColumnTypes.CAT_LOW_CARD if determined_type is None: - tmp_df[tmp_df.columns[col]] = tmp_df.iloc[:, col].astype(str) - if tmp_df.iloc[:, col].map(lambda x: _fullmatch(REGEX_MAYBE_DATE, x)).all(): - determined_type = _determine_date_type(tmp_df, col) - elif tmp_df.iloc[:, col].map(lambda x: _fullmatch(REGEX_UTC_LIKE, x)).all(): + string_values = values.astype(str) + if string_values.map(lambda x: _fullmatch(REGEX_MAYBE_DATE, x)).all(): + date_frame = string_values.to_frame() + determined_type = _determine_date_type(date_frame, 0) + elif string_values.map(lambda x: _fullmatch(REGEX_UTC_LIKE, x)).all(): determined_type = ColumnTypes.DATETIME_YMDHMS_ISO8601 elif ( - tmp_df.iloc[:, col] - .map(lambda x: len(re.findall(REGEX_UTF_TOKEN, x))) - .median() + string_values.map( + lambda x: len(re.findall(REGEX_UTF_TOKEN, x)) + ).median() > 5 ): determined_type = ColumnTypes.TEXT_UTF8 if determined_type is None: - if len(tmp_df.iloc[:, col].unique().tolist()) > HIGH_CARD_THRESHOLD: + if values.nunique() > HIGH_CARD_THRESHOLD: determined_type = ColumnTypes.CAT_HIGH_CARD else: determined_type = ColumnTypes.CAT_LOW_CARD diff --git a/falcon/types.py b/falcon/types.py index ed46c68..1b4a0c5 100644 --- a/falcon/types.py +++ b/falcon/types.py @@ -1,9 +1,11 @@ -from typing import Union, List, Tuple, Optional +from dataclasses import dataclass +from enum import Enum +from typing import Literal + import numpy as np from numpy import typing as npt -from enum import Enum -ColumnsList = Union[List[str], List[int]] +ColumnsList = list[str] | list[int] Float32Array = npt.NDArray[np.float32] Int64Array = npt.NDArray[np.int64] @@ -15,4 +17,38 @@ class ColumnTypes(Enum): TEXT_UTF8 = 3 DATE_YMD_ISO8601 = 100 # %Y-%m-%d i.e. '2023-02-21' DATETIME_YMDHMS_ISO8601 = 101 # %Y-%m-%dT%H:%M:%SZ i.e. '2023-02-21T17:24:22Z' OR %Y-%m-%d %H:%M:%S i.e. '2023-02-21 17:24:22' - \ No newline at end of file + + +TargetKind = Literal["classification", "regression"] + + +@dataclass(frozen=True) +class DatasetSchema: + column_names: tuple[str, ...] + column_types: tuple[ColumnTypes, ...] + target_name: str + target_kind: TargetKind + dimensions: tuple[int, int] + + @property + def n_rows(self) -> int: + return self.dimensions[0] + + @property + def n_features(self) -> int: + return self.dimensions[1] + + def to_dict(self) -> dict[str, object]: + return { + "columns": [ + {"name": name, "type": column_type.name} + for name, column_type in zip( + self.column_names, self.column_types, strict=True + ) + ], + "target": {"name": self.target_name, "kind": self.target_kind}, + "dimensions": { + "rows": self.n_rows, + "features": self.n_features, + }, + } diff --git a/falcon/utils.py b/falcon/utils.py index ddc6abe..f69500d 100644 --- a/falcon/utils.py +++ b/falcon/utils.py @@ -1,68 +1,27 @@ -import os -import sys -import warnings -from falcon.runtime import ONNXRuntime -from typing import List, Optional, Dict -from typing import List, Tuple, Optional -from numpy import typing as npt -import numpy as np -from typing import Any, Dict, Union +import logging +from typing import Any +logger = logging.getLogger("falcon") +if not any(isinstance(handler, logging.NullHandler) for handler in logger.handlers): + logger.addHandler(logging.NullHandler()) -def run_model(model_path: str, X: npt.NDArray) -> Union[List[npt.NDArray], np.ndarray]: - """ - Runs input data through the saved model. - - Parameters - ---------- - model_path : str - model path - X : npt.NDArray - model inputs - - Returns - ------- - Union[List[npt.NDArray], np.ndarray] - model predictions - """ - if model_path.endswith("onnx"): - return run_onnx(model_path, X, "final") - else: - raise ValueError("Invalid model path") - - -def run_onnx( - model: Union[bytes, str], X: npt.NDArray, outputs: str = "final" -) -> List[npt.NDArray]: - runtime = ONNXRuntime(model=model) - return runtime.run(X, outputs=outputs) +_VERBOSITY_LOG_LEVELS: dict[int, int] = { + 0: logging.WARNING, + 1: logging.INFO, +} def set_verbosity_level(level: int = 1) -> None: - if level not in {0, 1}: - level = 0 - os.environ["FALCON_VERBOSITY_LEVEL"] = str(level) - - -def print_(*args: Any) -> None: - verbosity_level = os.getenv("FALCON_VERBOSITY_LEVEL", "1") - if verbosity_level == "1": - for a in args: - print(a) - - -def disable_warnings() -> None: - if not sys.warnoptions: - warnings.simplefilter("ignore") - os.environ["PYTHONWARNINGS"] = "ignore" - return None + logger.setLevel(_VERBOSITY_LOG_LEVELS.get(level, logging.WARNING)) def set_eval_strategy( - eval_strategy: Any, manager_configuration_: Dict, test_data: Any = None + eval_strategy: Any, + manager_configuration_: dict[str, Any], + test_data: Any = None, ) -> None: - if "eval_strategy" in manager_configuration_.keys(): - if not eval_strategy == "dynamic": + if "eval_strategy" in manager_configuration_: + if eval_strategy != "dynamic": manager_configuration_["eval_strategy"] = eval_strategy else: if eval_strategy == "dynamic": diff --git a/mypy.ini b/mypy.ini index ac6efa9..1e80a69 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,7 +1,7 @@ [mypy] ignore_missing_imports = True disallow_untyped_defs = True - -[mypy-falcon.addons.*] -ignore_missing_imports = True -disallow_untyped_defs = False \ No newline at end of file +disallow_any_generics = True +check_untyped_defs = True +no_implicit_optional = True +warn_unused_ignores = True diff --git a/pyproject.toml b/pyproject.toml index 4ecba5e..99fc327 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,35 +5,77 @@ build-backend = "setuptools.build_meta" [project] dependencies = [ -"skl2onnx>=1.12.0", -"scikit-learn>=1.1.0", -"numpy>=1.18.0", -"onnx>=1.12.0", -"onnxruntime>=1.12.1", -"pandas>=1.0.0", -"imbalanced-learn>=0.8.1", -"pyarrow>=8.0.0", -"optuna>=3.0.0", -"packaging>=20.0.0", -"phonnx>=0.0.1", + "skl2onnx>=1.20.0,<1.21.0", + "scikit-learn>=1.5.0,<1.10.0", + "numpy>=1.23.0,<3.0.0", + "onnx>=1.16.0,<2.0.0", + "pandas>=2.0.0,<3.0.0", + "pyarrow>=12.0.0", + "scipy>=1.9.0", + # skl2onnx's tree converters put Python bools in the int64 attributes of + # TreeEnsemble nodes, which protobuf rejects from 7.34 onwards. Until that is + # fixed upstream, exporting any tree model needs the older protobuf. + "protobuf>=4.25.1,<7.34", ] + name = "falcon-ml" -version = "0.7.0" +version = "1.0.0" authors = [ - { name="Oleg Kostromin", email="kostromin97@gmail.com" }, - { name="Iryna Kondrashchenko", email="iryna230520@gmail.com" }, - { name="Marco Pasini", email="marco.pasini.98@gmail.com" }, + { name="Oleh Kostromin", email="oleh@dataforce.solutions" }, + { name="Iryna Kondrashchenko", email="iryna@dataforce.solutions" }, ] -description = "AutoML library for fast experementations." +description = "AutoML library for fast experimentation." readme = "README.md" license = {text = "MIT"} -requires-python = ">=3.9" +requires-python = ">=3.10" classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ] + +[project.optional-dependencies] +runtime = [ + "fnnx[core]>=0.0.3,<0.1.0", + "onnxruntime<1.24.0 ; python_full_version < '3.11'", + "onnxruntime>=1.18.1", +] +gbdt = [ + "catboost>=1.2.10", + "lightgbm>=4.7.0", + "onnxmltools>=1.16.0", + "xgboost>=3.2.0", +] +hpo = [ + "optuna>=3.0.0,<5.0.0", + "tqdm>=4.0.0", +] + + [tool.pytest.ini_options] pythonpath = [ "." -] \ No newline at end of file +] + +[tool.ruff] +line-length = 88 +target-version = "py310" + +[tool.ruff.lint] +select = ["ANN", "B", "E", "F", "I", "LOG", "UP"] +ignore = ["ANN401", "E501"] + +[tool.ruff.format] +docstring-code-format = true + +[tool.setuptools.packages.find] +include = ["falcon*"] +exclude = ["benchmarks*", "tests*"] + +[dependency-groups] +dev = [ + "mypy>=2.3.0", + "pytest>=9.1.1", + "ruff>=0.16.1", + "tomli>=2.0.0 ; python_full_version < '3.11'", +] diff --git a/scripts/run_matrix.py b/scripts/run_matrix.py new file mode 100644 index 0000000..c29f83b --- /dev/null +++ b/scripts/run_matrix.py @@ -0,0 +1,292 @@ +"""Run the test matrix defined in ci/matrix.json. + +CI runs one entry per job through this same script, so a local run and a CI run cannot +drift apart. Each entry gets its own virtualenv outside the tree, leaving the project's +own `.venv` alone. + + python scripts/run_matrix.py # every entry + python scripts/run_matrix.py latest-3.13 # one entry + python scripts/run_matrix.py --list +""" + +from __future__ import annotations + +import argparse +import json +import os +import subprocess +import sys +import tempfile +from dataclasses import dataclass, field +from pathlib import Path + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +MATRIX_FILE = REPOSITORY_ROOT / "ci" / "matrix.json" + +# `locked` installs the lock file verbatim, the way a CI job and `uv sync` do. The other +# two re-resolve, and go through `uv pip install` rather than `uv sync` for one reason: +# `uv sync --resolution ...` rewrites uv.lock in place, so running the floor entry would +# silently replace the project's lock with a lowest-direct one. +RESOLUTIONS = frozenset({"locked", "lowest-direct", "highest"}) + + +@dataclass(frozen=True) +class Entry: + name: str + python: str + resolution: str + extras: tuple[str, ...] = () + tests: tuple[str, ...] = () + allow_failure: bool = False + description: str = "" + + def __post_init__(self) -> None: + if self.resolution not in RESOLUTIONS: + raise SystemExit( + f"Entry `{self.name}` has unknown resolution `{self.resolution}`; " + f"expected one of {', '.join(sorted(RESOLUTIONS))}." + ) + + def install_commands(self, venv: Path) -> list[list[str]]: + if self.resolution == "locked": + command = ["uv", "sync", "--python", self.python, "--locked"] + for extra in self.extras: + command += ["--extra", extra] + return [command] + + target = ["--python", str(venv / "bin" / "python")] + specifier = f".[{','.join(self.extras)}]" if self.extras else "." + return [ + ["uv", "venv", str(venv), "--python", self.python], + [ + "uv", + "pip", + "install", + *target, + "--resolution", + self.resolution, + specifier, + ], + # The test tooling is installed separately and at its own newest version: + # the resolution mode is there to exercise falcon's declared bounds, and + # applying it to pytest just resolves an unusable twenty-year-old release. + [ + "uv", + "pip", + "install", + *target, + "pytest", + "mypy", + "ruff", + "tomli ; python_full_version < '3.11'", + ], + ] + + def quality_commands(self, venv: Path) -> list[tuple[str, list[str]]]: + """Lint and typecheck inside the entry's own environment. + + mypy resolves third-party stubs from what is installed, so its result depends + on the entry: numpy 2.2 types `np.arange` as strictly 1-D where 2.5 does not. + Running it per entry is what makes a green local run mean a green CI leg. + """ + runner = str(venv / "bin" / "python") + return [ + ("format", [runner, "-m", "ruff", "format", "--check", "falcon", "tests"]), + ("lint", [runner, "-m", "ruff", "check", "falcon", "tests"]), + ("mypy", [runner, "-m", "mypy", "falcon"]), + ] + + def pytest_command(self, venv: Path) -> list[str]: + runner = str(venv / "bin" / "python") + return [runner, "-m", "pytest", "-q", "-p", "no:cacheprovider", *self.tests] + + +@dataclass +class Result: + entry: Entry + stage: str = "ok" + returncode: int = 0 + versions: str = "" + failures: list[str] = field(default_factory=list) + + @property + def ok(self) -> bool: + return self.returncode == 0 + + +def load_entries() -> list[Entry]: + payload = json.loads(MATRIX_FILE.read_text(encoding="utf-8")) + return [ + Entry( + name=item["name"], + python=item["python"], + resolution=item["resolution"], + extras=tuple(item.get("extras", ())), + tests=tuple(item.get("tests", ())), + allow_failure=bool(item.get("allow_failure", False)), + description=item.get("description", ""), + ) + for item in payload["include"] + ] + + +def installed_versions(venv: Path) -> str: + probe = ( + "import sys, importlib.metadata as m\n" + "names=('scikit-learn','skl2onnx','onnx','onnxruntime','numpy','pandas','protobuf')\n" + "parts=['python ' + '.'.join(map(str, sys.version_info[:3]))]\n" + "for n in names:\n" + " try:\n" + " parts.append(n + ' ' + m.version(n))\n" + " except Exception:\n" + " pass\n" + "print(' | '.join(parts))\n" + ) + completed = subprocess.run( + [str(venv / "bin" / "python"), "-c", probe], + cwd=REPOSITORY_ROOT, + capture_output=True, + text=True, + ) + return completed.stdout.strip() or "(version probe failed)" + + +def run_entry(entry: Entry, *, workspace: Path, verbose: bool) -> Result: + venv = workspace / entry.name + environment = {**os.environ, "UV_PROJECT_ENVIRONMENT": str(venv)} + result = Result(entry) + + for command in entry.install_commands(venv): + install = subprocess.run( + command, + cwd=REPOSITORY_ROOT, + env=environment, + capture_output=not verbose, + text=True, + ) + if install.returncode != 0: + result.stage = "install" + result.returncode = install.returncode + if not verbose and install.stderr: + result.failures = install.stderr.strip().splitlines()[-8:] + return result + + result.versions = installed_versions(venv) + print(f" {result.versions}", flush=True) + + for stage, command in entry.quality_commands(venv): + check = subprocess.run( + command, + cwd=REPOSITORY_ROOT, + env=environment, + capture_output=not verbose, + text=True, + ) + if check.returncode != 0: + result.stage = stage + result.returncode = check.returncode + if not verbose: + output = f"{check.stdout}\n{check.stderr}" + result.failures = [ + line for line in output.strip().splitlines() if line + ][-12:] + return result + + tests = subprocess.run( + entry.pytest_command(venv), + cwd=REPOSITORY_ROOT, + env=environment, + capture_output=not verbose, + text=True, + ) + if tests.returncode != 0: + result.stage = "pytest" + result.returncode = tests.returncode + if not verbose: + output = f"{tests.stdout}\n{tests.stderr}" + result.failures = [ + line + for line in output.splitlines() + if line.startswith(("FAILED", "ERROR")) or " failed" in line + ][-12:] + return result + + +def report(results: list[Result]) -> int: + print("\n" + "=" * 78) + blocking = 0 + for result in results: + if result.ok: + status = "PASS" + elif result.entry.allow_failure: + status = "WARN" + else: + status = "FAIL" + blocking += 1 + print(f" {status:4} {result.entry.name:16} {result.versions}") + if not result.ok: + print(f" stage={result.stage} exit={result.returncode}") + for line in result.failures: + print(f" {line}") + print("=" * 78) + if blocking: + print(f"{blocking} blocking failure(s).") + else: + print("All blocking entries passed.") + return 1 if blocking else 0 + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("names", nargs="*", help="entries to run (default: all)") + parser.add_argument("--list", action="store_true", help="list entries and exit") + parser.add_argument( + "--verbose", action="store_true", help="stream uv and pytest output" + ) + parser.add_argument( + "--workspace", + type=Path, + help="where to build the virtualenvs (default: a temporary directory)", + ) + arguments = parser.parse_args() + + entries = load_entries() + if arguments.list: + for entry in entries: + marker = " (advisory)" if entry.allow_failure else "" + print(f"{entry.name:16} python {entry.python:5} {entry.resolution}{marker}") + if entry.description: + print(f" {entry.description}") + return 0 + + if arguments.names: + known = {entry.name: entry for entry in entries} + unknown = [name for name in arguments.names if name not in known] + if unknown: + raise SystemExit( + f"Unknown entries: {', '.join(unknown)}. Known: {', '.join(known)}." + ) + entries = [known[name] for name in arguments.names] + + workspace = arguments.workspace + temporary = None + if workspace is None: + temporary = tempfile.TemporaryDirectory(prefix="falcon_matrix_") + workspace = Path(temporary.name) + workspace.mkdir(parents=True, exist_ok=True) + + try: + results = [] + for index, entry in enumerate(entries, start=1): + print(f"\n[{index}/{len(entries)}] {entry.name}", flush=True) + results.append( + run_entry(entry, workspace=workspace, verbose=arguments.verbose) + ) + return report(results) + finally: + if temporary is not None: + temporary.cleanup() + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/addons/sklearn/test_conditional_svd.py b/tests/addons/sklearn/test_conditional_svd.py index 72cc9cc..076f1ff 100644 --- a/tests/addons/sklearn/test_conditional_svd.py +++ b/tests/addons/sklearn/test_conditional_svd.py @@ -1,44 +1,49 @@ -import numpy as np +import numpy as np from onnxruntime import InferenceSession from skl2onnx import to_onnx + from falcon.addons.sklearn.decomposition.svd import ConditionalSVD -def test_svd_id(): - X = np.random.uniform(size = (100, 24)) - svd = ConditionalSVD(n_components = 32) + +def test_svd_id() -> None: + X = np.random.uniform(size=(100, 24)) + svd = ConditionalSVD(n_components=32) y = svd.fit_transform(X) assert np.equal(X, y).all() - X = np.random.uniform(size = (100, 24)) - svd = ConditionalSVD(n_components = 32) + X = np.random.uniform(size=(100, 24)) + svd = ConditionalSVD(n_components=32) y = svd.fit(X).transform(X) assert np.equal(X, y).all() -def test_svd(): - X = np.random.uniform(size = (100, 64)) - svd = ConditionalSVD(n_components = 32) + +def test_svd() -> None: + X = np.random.uniform(size=(100, 64)) + svd = ConditionalSVD(n_components=32) y = svd.fit_transform(X) assert y.shape[-1] == 32 - X = np.random.uniform(size = (100, 64)) - svd = ConditionalSVD(n_components = 32) + X = np.random.uniform(size=(100, 64)) + svd = ConditionalSVD(n_components=32) y = svd.fit(X).transform(X) assert y.shape[-1] == 32 -def test_svd_onnx(): - X = np.random.uniform(size = (100, 64)) - svd = ConditionalSVD(n_components = 32) + +def test_svd_onnx() -> None: + X = np.random.uniform(size=(100, 64)) + svd = ConditionalSVD(n_components=32) expected = svd.fit(X).transform(X) onx = to_onnx(svd, X) sess = InferenceSession(onx.SerializeToString()) got = sess.run(None, {"X": X})[0] assert np.allclose(expected, got) -def test_svd_id_onnx(): - X = np.random.uniform(size = (100, 24)) - svd = ConditionalSVD(n_components = 32) + +def test_svd_id_onnx() -> None: + X = np.random.uniform(size=(100, 24)) + svd = ConditionalSVD(n_components=32) _ = svd.fit(X).transform(X) onx = to_onnx(svd, X) sess = InferenceSession(onx.SerializeToString()) got = sess.run(None, {"X": X})[0] - assert np.allclose(X, got) \ No newline at end of file + assert np.allclose(X, got) diff --git a/tests/addons/sklearn/test_date_tokenizer.py b/tests/addons/sklearn/test_date_tokenizer.py index b92fc54..7370c39 100644 --- a/tests/addons/sklearn/test_date_tokenizer.py +++ b/tests/addons/sklearn/test_date_tokenizer.py @@ -1,104 +1,190 @@ -from falcon.addons.sklearn.preprocessing.date_tokenizer import DateTimeTokenizer import numpy as np +import pytest +from numpy import typing as npt +from onnx import TensorProto, checker from onnxruntime import InferenceSession from skl2onnx import to_onnx +from falcon.addons.sklearn.preprocessing.date_tokenizer import DateTimeTokenizer -def test_date_tokenizer_ymd(): - a = np.asarray([["2022-02-02"], ["2022-02-25"], ["2022-05-02"]]) - - dt = DateTimeTokenizer(format=r"%Y-%m-%d") - dt.fit(None) - got = dt.transform(a).astype(object) - exp = np.asarray( - [["2022", "02", "02"], ["2022", "02", "25"], ["2022", "05", "02"]] - ).astype(object) +DATE_FORMAT = r"%Y-%m-%d" +DATETIME_FORMAT = r"%Y-%m-%dT%H:%M:%SZ" + +DATE_VALUES = np.asarray([["2022-02-02"], ["2022-02-25"], ["2022-05-02"]], dtype=object) +DATE_COMPONENTS = np.asarray( + [[2022, 2, 2], [2022, 2, 25], [2022, 5, 2]], dtype=np.float64 +) +DATETIME_CASES = [ + pytest.param( + np.asarray( + [ + ["2022-02-02T12:13:14Z"], + ["2022-02-25T15:16:17Z"], + ["2022-05-02T18:19:20Z"], + ], + dtype=object, + ), + id="t-delimiter-with-z", + ), + pytest.param( + np.asarray( + [ + ["2022-02-02T12:13:14"], + ["2022-02-25T15:16:17"], + ["2022-05-02T18:19:20"], + ], + dtype=object, + ), + id="t-delimiter-without-z", + ), + pytest.param( + np.asarray( + [ + ["2022-02-02 12:13:14Z"], + ["2022-02-25 15:16:17Z"], + ["2022-05-02 18:19:20Z"], + ], + dtype=object, + ), + id="space-delimiter-with-z", + ), + pytest.param( + np.asarray( + [ + ["2022-02-02 12:13:14"], + ["2022-02-25 15:16:17"], + ["2022-05-02 18:19:20"], + ], + dtype=object, + ), + id="space-delimiter-without-z", + ), +] +DATETIME_COMPONENTS = np.asarray( + [ + [2022, 2, 2, 12, 13, 14], + [2022, 2, 25, 15, 16, 17], + [2022, 5, 2, 18, 19, 20], + ], + dtype=np.float64, +) + + +def _expected_features( + components: npt.NDArray[np.float64], + missing: npt.NDArray[np.float64] | None = None, +) -> npt.NDArray[np.float64]: + if components.shape[1] == 3: + cyclic_components = components[:, [1, 2]] + periods = np.asarray([12, 31], dtype=np.float64) + else: + cyclic_components = components[:, [1, 2, 3, 4, 5]] + periods = np.asarray([12, 31, 24, 60, 60], dtype=np.float64) + angles = cyclic_components * ((2 * np.pi) / periods) + if missing is None: + missing = np.zeros((components.shape[0], 1), dtype=np.float64) + return np.concatenate((components, np.sin(angles), np.cos(angles), missing), axis=1) + + +def _assert_onnx_parity( + tokenizer: DateTimeTokenizer, values: npt.NDArray[np.object_] +) -> None: + expected = tokenizer.transform(values) + model = to_onnx(tokenizer, values) + + checker.check_model(model) + assert all( + node.domain in {"", "ai.onnx", "ai.onnx.ml"} for node in model.graph.node + ) + assert {"Cast", "Cos", "Sin", "StringSplit"}.issubset( + {node.op_type for node in model.graph.node} + ) + cast_targets = { + attribute.i + for node in model.graph.node + if node.op_type == "Cast" + for attribute in node.attribute + if attribute.name == "to" + } + assert TensorProto.INT64 in cast_targets - assert np.equal(got, exp).all() + session = InferenceSession(model.SerializeToString()) + actual = session.run(None, {"X": values})[0] + np.testing.assert_allclose(actual, expected, rtol=1e-6, atol=1e-6) -def test_date_tokenizer_ymd_onnx(): - a = np.asarray([["2022-02-02"], ["2022-02-25"], ["2022-05-02"]]) +def test_date_tokenizer_preserves_raw_and_adds_cyclical_features() -> None: + tokenizer = DateTimeTokenizer(format=DATE_FORMAT).fit(DATE_VALUES) - dt = DateTimeTokenizer(format=r"%Y-%m-%d") - dt.fit(None) - exp = dt.transform(a).astype(object) + actual = tokenizer.transform(DATE_VALUES) - onx = to_onnx(dt, a.astype(object)) - sess = InferenceSession(onx.SerializeToString()) + np.testing.assert_allclose( + actual, _expected_features(DATE_COMPONENTS), rtol=1e-6, atol=1e-5 + ) + assert actual.shape == (3, 8) + assert actual.dtype == np.float64 - got = sess.run(None, {"X": a.astype(object)})[0] - assert np.equal(got, exp).all() +def test_date_tokenizer_onnx_parity() -> None: + tokenizer = DateTimeTokenizer(format=DATE_FORMAT).fit(DATE_VALUES) + _assert_onnx_parity(tokenizer, DATE_VALUES) -def test_datetime_tokenizer_ymd_spaced(): - a = np.asarray( - [["2022-02-02 12:13:14"], ["2022-02-25 15:16:17"], ["2022-05-02 18:19:20"]] - ) - dt = DateTimeTokenizer(format=r"%Y-%m-%d %H:%M:%S") - dt.fit(None) - got = dt.transform(a).astype(object) - exp = np.asarray( - [ - ["2022", "02", "02", "12", "13", "14"], - ["2022", "02", "25", "15", "16", "17"], - ["2022", "05", "02", "18", "19", "20"], - ] - ).astype(object) +@pytest.mark.parametrize("values", DATETIME_CASES) +def test_datetime_tokenizer_detects_variant_and_adds_cyclical_features( + values: npt.NDArray[np.object_], +) -> None: + tokenizer = DateTimeTokenizer(format=DATETIME_FORMAT).fit(values) - assert np.equal(got, exp).all() + actual = tokenizer.transform(values) - -def test_datetime_tokenizer_ymd_spaced_onnx(): - a = np.asarray( - [["2022-02-02 12:13:14"], ["2022-02-25 15:16:17"], ["2022-05-02 18:19:20"]] + np.testing.assert_allclose( + actual, _expected_features(DATETIME_COMPONENTS), rtol=1e-6, atol=1e-5 ) + assert actual.shape == (3, 17) + assert actual.dtype == np.float64 - dt = DateTimeTokenizer(format=r"%Y-%m-%d %H:%M:%S") - dt.fit(None) - exp = dt.transform(a).astype(object) - - onx = to_onnx(dt, a.astype(object)) - sess = InferenceSession(onx.SerializeToString()) - got = sess.run(None, {"X": a.astype(object)})[0] +@pytest.mark.parametrize("values", DATETIME_CASES) +def test_datetime_tokenizer_onnx_parity( + values: npt.NDArray[np.object_], +) -> None: + tokenizer = DateTimeTokenizer(format=DATETIME_FORMAT).fit(values) - assert np.equal(got, exp).all() + _assert_onnx_parity(tokenizer, values) -def test_datetime_tokenizer_ymd(): - a = np.asarray( - [["2022-02-02T12:13:14Z"], ["2022-02-25T15:16:17Z"], ["2022-05-02T18:19:20Z"]] +def test_datetime_tokenizer_rejects_mixed_variants_at_fit() -> None: + values = np.asarray( + [["2022-02-02T12:13:14Z"], ["2022-02-25 15:16:17"]], dtype=object ) - dt = DateTimeTokenizer(format=r"%Y-%m-%dT%H:%M:%SZ") - dt.fit(None) - got = dt.transform(a).astype(object) - exp = np.asarray( - [ - ["2022", "02", "02", "12", "13", "14"], - ["2022", "02", "25", "15", "16", "17"], - ["2022", "05", "02", "18", "19", "20"], - ] - ).astype(object) - - assert np.equal(got, exp).all() + with pytest.raises(ValueError, match="single datetime format variant"): + DateTimeTokenizer(format=DATETIME_FORMAT).fit(values) -def test_datetime_tokenizer_ymd_onnx(): - a = np.asarray( - [["2022-02-02T12:13:14Z"], ["2022-02-25T15:16:17Z"], ["2022-05-02T18:19:20Z"]] +def test_datetime_tokenizer_rejects_variant_change_at_inference() -> None: + tokenizer = DateTimeTokenizer(format=DATETIME_FORMAT).fit( + np.asarray([["2022-02-02T12:13:14Z"]], dtype=object) ) - dt = DateTimeTokenizer(format=r"%Y-%m-%dT%H:%M:%SZ") - dt.fit(None) - exp = dt.transform(a).astype(object) + with pytest.raises(ValueError, match="fitted datetime format variant"): + tokenizer.transform(np.asarray([["2022-02-02 12:13:14"]], dtype=object)) - onx = to_onnx(dt, a.astype(object)) - sess = InferenceSession(onx.SerializeToString()) - got = sess.run(None, {"X": a.astype(object)})[0] +def test_date_tokenizer_imputes_missing_values_with_reference_and_indicator() -> None: + training_values = np.asarray( + [["2022-02-02"], [np.nan], ["2022-05-02"]], dtype=object + ) + inference_values = np.asarray([[np.nan], ["2022-02-25"]], dtype=object) + tokenizer = DateTimeTokenizer(format=DATE_FORMAT).fit(training_values) + + actual = tokenizer.transform(inference_values) + expected = _expected_features( + DATE_COMPONENTS[:2], np.asarray([[1.0], [0.0]], dtype=np.float64) + ) - assert np.equal(got, exp).all() + assert tokenizer.reference_value_ == "2022-02-02" + np.testing.assert_allclose(actual, expected, rtol=1e-6, atol=1e-5) + _assert_onnx_parity(tokenizer, inference_values.astype(str)) diff --git a/tests/addons/sklearn/test_missing_values.py b/tests/addons/sklearn/test_missing_values.py new file mode 100644 index 0000000..79f6c48 --- /dev/null +++ b/tests/addons/sklearn/test_missing_values.py @@ -0,0 +1,98 @@ +from typing import Any + +import numpy as np +from numpy import typing as npt +from onnx import checker +from onnxruntime import InferenceSession + +from falcon.tabular.processors.multi_modal_encoder import MultiModalEncoder +from falcon.types import ColumnTypes, DatasetSchema + + +def _runtime_inputs( + model_inputs: list[Any], values: npt.NDArray[np.object_] +) -> dict[str, npt.NDArray[Any]]: + inputs: dict[str, npt.NDArray[Any]] = {} + for index, model_input in enumerate(model_inputs): + column = values[:, index].reshape(-1, 1) + if index == 0: + inputs[model_input.name] = column.astype(np.float32) + else: + inputs[model_input.name] = column.astype(str) + return inputs + + +def test_multimodal_missing_value_preprocessing_has_onnx_parity() -> None: + training = np.asarray( + [ + [1.0, "red", "2024-01-01", "falcons fly over mountains today"], + [np.nan, np.nan, np.nan, np.nan], + [3.0, "blue", "2024-03-15", "hawks circle over valleys today"], + [7.0, "red", "2024-04-20", "eagles glide above forests today"], + ], + dtype=np.object_, + ) + inference = np.asarray( + [ + [np.nan, np.nan, np.nan, np.nan], + [5.0, "unseen", "2024-06-30", "unseen words remain harmless here"], + ], + dtype=np.object_, + ) + schema = DatasetSchema( + column_names=("numeric", "category", "date", "text"), + column_types=( + ColumnTypes.NUMERIC_REGULAR, + ColumnTypes.CAT_LOW_CARD, + ColumnTypes.DATE_YMD_ISO8601, + ColumnTypes.TEXT_UTF8, + ), + target_name="target", + target_kind="regression", + dimensions=training.shape, + ) + encoder = MultiModalEncoder() + encoder.fit(training, np.arange(training.shape[0]), schema) + + expected = encoder.transform(inference) + categorical_imputer = encoder.ct.transformers_[1][1].named_steps["imputer"] + text_imputer = encoder.ct.transformers_[3][1].named_steps["imputer"] + model = encoder.serialize().get_model() + checker.check_model(model) + actual = InferenceSession(model.SerializeToString()).run( + None, _runtime_inputs(list(model.graph.input), inference) + )[0] + + assert expected.shape[0] == inference.shape[0] + assert np.isfinite(expected).all() + assert categorical_imputer.transform(np.asarray([[np.nan]])).item() == ( + "__falcon_missing__" + ) + assert text_imputer.transform(np.asarray([np.nan])).item() == "" + assert all( + node.domain in {"", "ai.onnx", "ai.onnx.ml"} for node in model.graph.node + ) + np.testing.assert_allclose(actual, expected, rtol=1e-5, atol=1e-5) + + +def test_numeric_preprocessing_always_emits_a_missing_indicator() -> None: + training = np.asarray([[1.0], [3.0], [9.0]], dtype=np.object_) + schema = DatasetSchema( + column_names=("numeric",), + column_types=(ColumnTypes.NUMERIC_REGULAR,), + target_name="target", + target_kind="regression", + dimensions=training.shape, + ) + encoder = MultiModalEncoder() + encoder.fit(training, np.arange(training.shape[0]), schema) + + transformed = encoder.transform(np.asarray([[np.nan], [3.0]], dtype=np.object_)) + numeric_pipeline = encoder.ct.transformers_[0][1] + + np.testing.assert_array_equal( + numeric_pipeline.named_steps["imputer"].statistics_, np.asarray([3.0]) + ) + assert transformed.shape == (2, 2) + assert transformed[0, 0] == transformed[1, 0] + assert transformed[0, 1] != transformed[1, 1] diff --git a/tests/addons/sklearn/test_target_encoder.py b/tests/addons/sklearn/test_target_encoder.py new file mode 100644 index 0000000..7703a37 --- /dev/null +++ b/tests/addons/sklearn/test_target_encoder.py @@ -0,0 +1,137 @@ +from __future__ import annotations + +from typing import Any + +import numpy as np +import pytest +from numpy import typing as npt +from onnx import checker +from onnxruntime import InferenceSession + +from falcon.abstract import Pipeline +from falcon.serialization import SerializedModelRepr +from falcon.tabular.processors.multi_modal_encoder import MultiModalEncoder +from falcon.tabular.splitting import cross_validation_indices +from falcon.types import ColumnTypes, DatasetSchema, Float32Array, TargetKind + + +class _TrainingInputRecorder: + fitted_X: npt.NDArray[Any] | None + + def __init__(self) -> None: + self.fitted_X = None + + def fit( + self, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + schema: DatasetSchema, + *, + groups: npt.ArrayLike | None = None, + ) -> None: + self.fitted_X = np.asarray(X).copy() + + def transform(self, X: npt.NDArray[Any]) -> npt.NDArray[Any]: + return np.asarray(X) + + def serialize(self) -> SerializedModelRepr: + raise AssertionError("Serialization is not used by this test step") + + def get_input_type(self) -> object: + return Float32Array + + def get_output_type(self) -> object: + return Float32Array + + +def _schema(shape: tuple[int, int], target_kind: TargetKind) -> DatasetSchema: + return DatasetSchema( + column_names=("category",), + column_types=(ColumnTypes.CAT_HIGH_CARD,), + target_name="target", + target_kind=target_kind, + dimensions=shape, + ) + + +@pytest.mark.parametrize( + ("target_kind", "target"), + [ + pytest.param( + "regression", + np.asarray([0.2, 0.5, 0.8, 2.1, 2.4, 2.7, 5.0, 5.3, 5.6]), + id="regression", + ), + pytest.param( + "classification", + np.asarray([0, 1, 0, 1, 0, 1, 0, 1, 0]), + id="binary", + ), + pytest.param( + "classification", + np.asarray([0, 1, 2, 0, 1, 2, 0, 1, 2]), + id="multiclass", + ), + ], +) +def test_target_encoder_unseen_category_has_standard_onnx_parity( + target_kind: TargetKind, + target: npt.NDArray[Any], +) -> None: + training = np.repeat( + np.asarray([["alpha"], ["beta"], ["gamma"]], dtype=np.object_), 3, axis=0 + ) + unseen = np.asarray([["unseen"]], dtype=np.object_) + encoder = MultiModalEncoder() + encoder.fit(training, target, _schema(training.shape, target_kind)) + + expected = encoder.transform(unseen) + fitted_pipeline = encoder.ct.transformers_[0][1] + target_encoder = fitted_pipeline.named_steps["target_encoder"] + model = encoder.serialize().get_model() + checker.check_model(model) + actual = InferenceSession(model.SerializeToString()).run( + None, + {model.graph.input[0].name: unseen.astype(str)}, + )[0] + + np.testing.assert_allclose( + expected[0], np.asarray(target_encoder.target_mean_).reshape(-1), atol=1e-6 + ) + np.testing.assert_allclose(actual, expected, rtol=1e-6, atol=1e-6) + assert all( + node.domain in {"", "ai.onnx", "ai.onnx.ml"} for node in model.graph.node + ) + + +@pytest.mark.parametrize("use_explicit_groups", [False, True]) +def test_pipeline_training_values_are_cross_fitted_without_group_leakage( + use_explicit_groups: bool, +) -> None: + group_ids = np.repeat(np.arange(6), 2) + groups = group_ids if use_explicit_groups else None + training = np.asarray([[f"group-{group}"] for group in group_ids], dtype=np.object_) + target = np.repeat(np.asarray([1.0, 7.0, 15.0, 31.0, 63.0, 127.0]), 2) + schema = _schema(training.shape, "regression") + encoder = MultiModalEncoder() + recorder = _TrainingInputRecorder() + pipeline = Pipeline( + task="tabular_regression", dataset_size=training.shape, schema=schema + ) + pipeline.add_step(encoder) + pipeline.add_step(recorder) + + pipeline.fit(training, target, schema, groups=groups) + + assert recorder.fitted_X is not None + expected = np.empty(training.shape[0], dtype=np.float32) + for train_indices, validation_indices in cross_validation_indices( + training, + target, + task="tabular_regression", + groups=groups, + ): + expected[validation_indices] = np.mean(target[train_indices]) + + np.testing.assert_allclose(recorder.fitted_X[:, 0], expected, atol=1e-6) + assert not np.allclose(recorder.fitted_X, encoder.transform(training)) diff --git a/tests/addons/sklearn/test_text_vectorizer.py b/tests/addons/sklearn/test_text_vectorizer.py new file mode 100644 index 0000000..70f8920 --- /dev/null +++ b/tests/addons/sklearn/test_text_vectorizer.py @@ -0,0 +1,74 @@ +import numpy as np +from onnx import helper +from onnxruntime import InferenceSession +from skl2onnx import to_onnx +from skl2onnx.common.data_types import StringTensorType + +from falcon.addons.sklearn.preprocessing.text_vectorizer import ( + FalconTfidfVectorizer, +) + + +def test_text_vectorizer_onnx_uses_standard_ops_with_native_parity() -> None: + training_documents = np.asarray( + [ + "THE Falcon, flies quickly", + "falcon, rests and waits", + "alpha beta beta", + "gamma alpha", + ], + dtype=object, + ) + inference_documents = np.asarray( + ["FALCON, flies\tquickly", "the alpha\nbeta", "unknown token"], + dtype=object, + ) + vectorizer = FalconTfidfVectorizer().fit(training_documents) + + expected = vectorizer.transform(inference_documents).toarray() + model = to_onnx( + vectorizer, + initial_types=[("X", StringTensorType([None]))], + target_opset={"": 21, "ai.onnx.ml": 4}, + ) + session = InferenceSession(model.SerializeToString()) + actual = session.run(None, {"X": inference_documents})[0] + + node_types = {node.op_type for node in model.graph.node} + assert {"StringNormalizer", "StringSplit", "TfIdfVectorizer"} <= node_types + assert all( + node.domain in {"", "ai.onnx", "ai.onnx.ml"} for node in model.graph.node + ) + assert "the" not in vectorizer.vocabulary_ + tfidf_node = next( + node for node in model.graph.node if node.op_type == "TfIdfVectorizer" + ) + pool = helper.get_attribute_value( + next( + attribute + for attribute in tfidf_node.attribute + if attribute.name == "pool_strings" + ) + ) + assert b"the" not in pool + np.testing.assert_allclose(actual, expected, rtol=1e-6, atol=1e-6) + + +def test_text_vectorizer_onnx_matches_native_for_non_ascii_case() -> None: + training_documents = np.asarray( + ["CAFÉ déjà", "café DÉJÀ", "İSTANBUL şehir", "istanbul ŞEHİR"], + dtype=object, + ) + vectorizer = FalconTfidfVectorizer(stop_words=None).fit(training_documents) + model = to_onnx( + vectorizer, + initial_types=[("X", StringTensorType([None]))], + target_opset={"": 21, "ai.onnx.ml": 4}, + ) + + expected = vectorizer.transform(training_documents).toarray() + actual = InferenceSession(model.SerializeToString()).run( + None, {"X": training_documents} + )[0] + + np.testing.assert_allclose(actual, expected, rtol=1e-6, atol=1e-6) diff --git a/tests/fnnx_conformance.py b/tests/fnnx_conformance.py new file mode 100644 index 0000000..502206a --- /dev/null +++ b/tests/fnnx_conformance.py @@ -0,0 +1,163 @@ +from __future__ import annotations + +import json +import tarfile +from collections.abc import Iterator +from dataclasses import dataclass +from io import BytesIO +from pathlib import Path + +import onnx +from onnx import ModelProto + +STANDARD_ONNX_DOMAINS = frozenset({"ai.onnx", "ai.onnx.ml"}) +_MODEL_PATH = "ops_artifacts/onnx_main/model.onnx" +_OPS_PATH = "ops.json" + + +@dataclass(frozen=True) +class ExtractedFNNXGraph: + model: ModelProto + fnnx_opset_domains: tuple[str, ...] + declared_ir_version: int + + +def read_bundle_member(archive: tarfile.TarFile, member_name: str) -> bytes: + try: + member = archive.extractfile(member_name) + except KeyError as error: + raise AssertionError(f"FNNX bundle is missing {member_name}") from error + if member is None: + raise AssertionError(f"FNNX bundle member {member_name} is not a file") + return member.read() + + +def _parse_fnnx_opset_domains(raw_ops: object) -> tuple[str, ...]: + if not isinstance(raw_ops, list): + raise TypeError("FNNX ops.json must contain a list of operations") + + domains: list[str] = [] + for op_index, raw_op in enumerate(raw_ops): + if not isinstance(raw_op, dict): + raise TypeError(f"FNNX operation {op_index} must be an object") + attributes = raw_op.get("attributes") + if not isinstance(attributes, dict): + raise TypeError(f"FNNX operation {op_index} has no attributes object") + raw_opsets = attributes.get("opsets") + if not isinstance(raw_opsets, list): + raise TypeError(f"FNNX operation {op_index} has no opsets list") + for opset_index, raw_opset in enumerate(raw_opsets): + if not isinstance(raw_opset, dict): + raise TypeError( + f"FNNX operation {op_index} opset {opset_index} must be an object" + ) + domain = raw_opset.get("domain") + if not isinstance(domain, str): + raise TypeError( + f"FNNX operation {op_index} opset {opset_index} has no domain" + ) + domains.append(domain) + return tuple(domains) + + +def _parse_declared_ir_version(raw_ops: object) -> int: + if not isinstance(raw_ops, list) or not raw_ops: + raise TypeError("FNNX ops.json must contain a non-empty list of operations") + + attributes = raw_ops[0].get("attributes") if isinstance(raw_ops[0], dict) else None + if not isinstance(attributes, dict): + raise TypeError("FNNX operation 0 has no attributes object") + + declared = attributes.get("onnx_ir_version") + if not isinstance(declared, int): + raise TypeError("FNNX operation 0 declares no onnx_ir_version") + return declared + + +def _extract_graph_from_archive(archive: tarfile.TarFile) -> ExtractedFNNXGraph: + model = onnx.load_model_from_string(read_bundle_member(archive, _MODEL_PATH)) + raw_ops = json.loads(read_bundle_member(archive, _OPS_PATH)) + return ExtractedFNNXGraph( + model=model, + fnnx_opset_domains=_parse_fnnx_opset_domains(raw_ops), + declared_ir_version=_parse_declared_ir_version(raw_ops), + ) + + +def extract_fnnx_graph(bundle: bytes | str | Path) -> ExtractedFNNXGraph: + if isinstance(bundle, bytes): + with tarfile.open(fileobj=BytesIO(bundle), mode="r:") as archive: + return _extract_graph_from_archive(archive) + + with tarfile.open(name=Path(bundle), mode="r:") as archive: + return _extract_graph_from_archive(archive) + + +def _normalized_domain(domain: str) -> str: + return "ai.onnx" if domain in {"", "ai.onnx"} else domain + + +def assert_onnx_is_valid(model: ModelProto) -> None: + onnx.checker.check_model(model) + + +def _iter_nested_nodes(node: onnx.NodeProto) -> Iterator[onnx.NodeProto]: + for attribute in node.attribute: + if attribute.HasField("g"): + yield from _iter_graph_nodes(attribute.g) + for graph in attribute.graphs: + yield from _iter_graph_nodes(graph) + + +def _iter_graph_nodes(graph: onnx.GraphProto) -> Iterator[onnx.NodeProto]: + for node in graph.node: + yield node + yield from _iter_nested_nodes(node) + + +def _iter_model_nodes(model: ModelProto) -> Iterator[onnx.NodeProto]: + yield from _iter_graph_nodes(model.graph) + for function in model.functions: + for node in function.node: + yield node + yield from _iter_nested_nodes(node) + + +def assert_standard_node_domains(model: ModelProto) -> None: + violations: list[str] = [] + for node in _iter_model_nodes(model): + domain = _normalized_domain(node.domain) + if domain not in STANDARD_ONNX_DOMAINS: + node_name = node.name or f"" + violations.append(f"{node_name} ({node.op_type}) uses {domain}") + + if violations: + raise AssertionError("Non-standard ONNX node domains: " + "; ".join(violations)) + + +def assert_standard_opset_declarations(graph: ExtractedFNNXGraph) -> None: + model_domains = { + _normalized_domain(opset.domain) for opset in graph.model.opset_import + } + fnnx_domains = {_normalized_domain(domain) for domain in graph.fnnx_opset_domains} + violations = [ + *( + f"model imports {domain}" + for domain in sorted(model_domains - STANDARD_ONNX_DOMAINS) + ), + *( + f"FNNX ops.json declares {domain}" + for domain in sorted(fnnx_domains - STANDARD_ONNX_DOMAINS) + ), + ] + + if violations: + raise AssertionError( + "Non-standard ONNX opset declarations: " + "; ".join(violations) + ) + + +def assert_fnnx_conforms(graph: ExtractedFNNXGraph) -> None: + assert_onnx_is_valid(graph.model) + assert_standard_node_domains(graph.model) + assert_standard_opset_declarations(graph) diff --git a/tests/manual/scaler.py b/tests/manual/scaler.py deleted file mode 100644 index ccf88e9..0000000 --- a/tests/manual/scaler.py +++ /dev/null @@ -1,27 +0,0 @@ -import pandas as pd -import random -import numpy as np - -data = { - 'cat_lc' : [random.randint(0, 9) for i in range(128)], - 'cat_hc' : [f"f{i}" for i in range(128)], - 'static': [0 for _ in range(128)], - 'num': [i for i in range(128)], - 'labels': [i/2 for i in range(128)] -} - -df = pd.DataFrame(data) - -from falcon import initialize -from falcon.utils import run_onnx - -sample = np.asarray([[12, 'unk', 1, 256], [10, 'f10', 0, 10]]) - -manager = initialize(task = 'tabular_regression', data = df, features=['cat_lc', 'cat_hc', 'static', 'num'], target='labels') -manager.train() -manager.save_model(filename='new_sc_enc.onnx') - -pred_onnx = run_onnx('new_sc_enc.onnx', sample) -pred = manager.predict(sample) - -print(pred, pred_onnx) \ No newline at end of file diff --git a/tests/manual/tab_report.py b/tests/manual/tab_report.py deleted file mode 100644 index bc7a556..0000000 --- a/tests/manual/tab_report.py +++ /dev/null @@ -1,7 +0,0 @@ -from falcon.tabular import reporting -import numpy as np - -y = np.random.randint(0, 3, size=100) -y_hat = np.random.randint(0, 3, size=100) - -reporting.print_classification_report(y, y_hat) diff --git a/tests/tabular/learners/test_super_learner.py b/tests/tabular/learners/test_super_learner.py deleted file mode 100644 index 0dda875..0000000 --- a/tests/tabular/learners/test_super_learner.py +++ /dev/null @@ -1,71 +0,0 @@ -import numpy as np - -from falcon.tabular.learners.super_learner import _default_estimators, SuperLearner - - -def fit_superlearner(config): - X = np.random.normal(size=(500, 16)) - task = config["task"] - if task == "tabular_regression": - y = np.random.uniform(size=(500, 1)) - else: - y = np.random.randint(0, 3, size=(500,)) - - model = SuperLearner(**config) - model.fit(X, y) - pred = model.predict(X) - return pred - - -def gen_config(regr=True, size="mini"): - task = "tabular_regression" if regr else "tabular_classification" - base_estimators = _default_estimators[task][size] - config = { - "task": task, - "base_estimators": base_estimators, - "cv": 2, - "filter_estimators": False, - } - return config - - -def assert_preds(pred): - assert pred is not None - assert len(pred) > 0 - assert None not in pred - - -def test_super_learner_regr_mini(): - config = gen_config(True, "mini") - pred = fit_superlearner(config=config) - assert_preds(pred) - - -def test_super_learner_regr_mid(): - config = gen_config(True, "mid") - pred = fit_superlearner(config=config) - assert_preds(pred) - - -def test_super_learner_regr_large(): - config = gen_config(True, "large") - pred = fit_superlearner(config=config) - assert_preds(pred) - - -def test_super_learner_clf_mini(): - config = gen_config(False, "mini") - pred = fit_superlearner(config=config) - assert_preds(pred) - - -def test_super_learner_clf_mid(): - config = gen_config(False, "mid") - pred = fit_superlearner(config=config) - assert_preds(pred) - - -def test_super_learner_clf_large(): - config = gen_config(False, "large") - pred = fit_superlearner(config=config) - assert_preds(pred) diff --git a/tests/tabular/processors/test_label_decoder.py b/tests/tabular/processors/test_label_decoder.py index ca0394e..4af4622 100644 --- a/tests/tabular/processors/test_label_decoder.py +++ b/tests/tabular/processors/test_label_decoder.py @@ -1,15 +1,25 @@ -from falcon.tabular.processors.label_decoder import LabelDecoder import numpy as np +from falcon.tabular.processors.label_decoder import LabelDecoder +from falcon.types import ColumnTypes, DatasetSchema + -def test_label_decoder(): +def test_label_decoder() -> None: labels = np.array(["A", "B", "C", "D"]) + X = np.arange(4).reshape(-1, 1) + schema = DatasetSchema( + column_names=("feature",), + column_types=(ColumnTypes.NUMERIC_REGULAR,), + target_name="target", + target_kind="classification", + dimensions=X.shape, + ) processor = LabelDecoder() - processor.fit(labels) + processor.fit(X, labels, schema) expected_encoded_labels = np.array([0, 1, 2, 3]) - encoded_labels = processor.transform(labels, inverse=False) + encoded_labels = processor.encode(labels) assert False not in np.equal(expected_encoded_labels, encoded_labels) - decoed_labels = processor.transform(np.array([3, 2, 1, 0]), inverse=True) + decoed_labels = processor.transform(np.array([3, 2, 1, 0])) expected_decoed_labels = np.array(["D", "C", "B", "A"], dtype=np.str_) print(decoed_labels.dtype, expected_decoed_labels.dtype) assert False not in np.equal( diff --git a/tests/tabular/processors/test_mm_encoder.py b/tests/tabular/processors/test_mm_encoder.py index 8c02ec5..ae2207b 100644 --- a/tests/tabular/processors/test_mm_encoder.py +++ b/tests/tabular/processors/test_mm_encoder.py @@ -1,48 +1,72 @@ -import pandas as pd import numpy as np +import pandas as pd from onnxruntime import InferenceSession -from falcon.types import ColumnTypes +from sklearn.utils.validation import check_is_fitted + from falcon.tabular.processors.multi_modal_encoder import MultiModalEncoder +from falcon.types import ColumnTypes, DatasetSchema + + +def test_date_encoder_pipeline_is_recognized_as_fitted() -> None: + data = np.asarray([["2022-02-02"], ["2022-02-25"], ["2022-05-02"]]) + schema = DatasetSchema( + column_names=("date",), + column_types=(ColumnTypes.DATE_YMD_ISO8601,), + target_name="target", + target_kind="regression", + dimensions=data.shape, + ) + encoder = MultiModalEncoder() + + encoder.fit(data, np.zeros(data.shape[0]), schema) + + date_pipeline = encoder.ct.transformers_[0][1] + check_is_fitted(date_pipeline) -def test_mm_news100(): +def test_mm_news100() -> None: data = pd.read_csv("tests/extra_files/news100.csv").fillna("") - data.pop('label') + data.pop("label") data = data.to_numpy()[:5, :] - print('data shape ', data.shape) - ct = [ + print("data shape ", data.shape) + ct = ( ColumnTypes.NUMERIC_REGULAR, ColumnTypes.TEXT_UTF8, ColumnTypes.CAT_LOW_CARD, ColumnTypes.TEXT_UTF8, - ] + ) + schema = DatasetSchema( + column_names=("timedelta", "title", "topic", "content"), + column_types=ct, + target_name="target", + target_kind="regression", + dimensions=data.shape, + ) - enc = MultiModalEncoder(ct) + enc = MultiModalEncoder() - enc.fit(data, None) + enc.fit(data, np.zeros(data.shape[0]), schema) y = enc.transform(data) - print('y shape ', y.shape) + print("y shape ", y.shape) - assert y.dtype == np.float32, 'Incorrect return type' + assert y.dtype == np.float32, "Incorrect return type" - assert y.shape[0] == data.shape[0], 'Incorrect number of samples in the output' + assert y.shape[0] == data.shape[0], "Incorrect number of samples in the output" - onx = enc.to_onnx().get_model() - with open('tmp.onnx', 'wb+') as f: - f.write(onx.SerializeToString()) + onx = enc.serialize().get_model() inps = {} for i, inp in enumerate(onx.graph.input): - arr = data[:, i] - if len(arr.shape) < 2: + arr = data[:, i] + if len(arr.shape) < 2: arr = np.expand_dims(arr, 1) print(i, arr.shape) if ct[i] != ColumnTypes.NUMERIC_REGULAR: arr = arr.astype(object) - else: + else: arr = arr.astype(np.float32) - inps[inp.name] = arr + inps[inp.name] = arr sess = InferenceSession(onx.SerializeToString()) got = sess.run(None, inps)[0] - assert np.allclose(y, got, atol=1e-3), 'Incorrect onnx output' + assert np.allclose(y, got, atol=1e-3), "Incorrect onnx output" diff --git a/tests/tabular/processors/test_no_imputation.py b/tests/tabular/processors/test_no_imputation.py new file mode 100644 index 0000000..118b8a6 --- /dev/null +++ b/tests/tabular/processors/test_no_imputation.py @@ -0,0 +1,172 @@ +from typing import Any + +import numpy as np +import pytest +from numpy import typing as npt +from onnx import ModelProto, checker +from onnxruntime import InferenceSession + +from falcon.tabular.pipelines.simple_tabular_pipeline import SimpleTabularPipeline +from falcon.tabular.processors.multi_modal_encoder import MultiModalEncoder +from falcon.types import ColumnTypes, DatasetSchema + +BRANCHING_OPS = frozenset({"Where", "IsNaN", "Equal", "Or", "If", "Loop", "Scan"}) + + +def _schema( + column_names: tuple[str, ...], + column_types: tuple[ColumnTypes, ...], + dimensions: tuple[int, int], +) -> DatasetSchema: + return DatasetSchema( + column_names=column_names, + column_types=column_types, + target_name="target", + target_kind="regression", + dimensions=dimensions, + ) + + +def _runtime_inputs( + model: ModelProto, + values: npt.NDArray[np.object_], + column_types: tuple[ColumnTypes, ...], +) -> dict[str, npt.NDArray[Any]]: + inputs: dict[str, npt.NDArray[Any]] = {} + for index, model_input in enumerate(model.graph.input): + column = values[:, index].reshape(-1, 1) + if column_types[index] == ColumnTypes.NUMERIC_REGULAR: + inputs[model_input.name] = column.astype(np.float32) + else: + inputs[model_input.name] = column.astype(str) + return inputs + + +def _emitted_ops(model: ModelProto) -> set[str]: + return {node.op_type for node in model.graph.node} + + +def test_encoder_without_imputation_exports_a_branch_free_graph() -> None: + column_types = ( + ColumnTypes.NUMERIC_REGULAR, + ColumnTypes.CAT_LOW_CARD, + ColumnTypes.CAT_HIGH_CARD, + ) + training = np.asarray( + [[float(index), f"g{index % 3}", f"c{index}"] for index in range(12)], + dtype=np.object_, + ) + inference = np.asarray([[2.0, "g1", "c3"], [7.5, "unseen", "unseen"]], np.object_) + schema = _schema(("numeric", "low_card", "high_card"), column_types, training.shape) + encoder = MultiModalEncoder(impute_missing=False) + encoder.fit(training, np.arange(training.shape[0], dtype=np.float64), schema) + + expected = encoder.transform(inference) + model = encoder.serialize().get_model() + checker.check_model(model) + actual = InferenceSession(model.SerializeToString()).run( + None, _runtime_inputs(model, inference, column_types) + )[0] + + assert not BRANCHING_OPS & _emitted_ops(model) + np.testing.assert_allclose(actual, expected, rtol=1e-5, atol=1e-5) + + +def test_numeric_features_drop_the_missing_indicator_without_imputation() -> None: + training = np.asarray([[1.0], [3.0], [9.0]], dtype=np.object_) + schema = _schema(("numeric",), (ColumnTypes.NUMERIC_REGULAR,), training.shape) + + imputing = MultiModalEncoder() + imputing.fit(training, np.arange(3, dtype=np.float64), schema) + plain = MultiModalEncoder(impute_missing=False) + plain.fit(training, np.arange(3, dtype=np.float64), schema) + + assert imputing.transform(training).shape == (3, 2) + assert plain.transform(training).shape == (3, 1) + + +def test_missing_numeric_values_are_rejected_without_imputation() -> None: + training = np.asarray([[1.0], [np.nan], [9.0]], dtype=np.object_) + schema = _schema(("numeric",), (ColumnTypes.NUMERIC_REGULAR,), training.shape) + encoder = MultiModalEncoder(impute_missing=False) + + with pytest.raises(ValueError, match="imputation is disabled"): + encoder.fit(training, np.arange(3, dtype=np.float64), schema) + + +def test_missing_categories_become_regular_categories_without_imputation() -> None: + column_types = (ColumnTypes.CAT_LOW_CARD,) + training = np.asarray([["red"], [np.nan], ["blue"], ["red"]], dtype=np.object_) + inference = np.asarray([[np.nan], ["red"]], dtype=np.object_) + schema = _schema(("category",), column_types, training.shape) + encoder = MultiModalEncoder(impute_missing=False) + encoder.fit(training, np.arange(4, dtype=np.float64), schema) + + expected = encoder.transform(inference) + model = encoder.serialize().get_model() + actual = InferenceSession(model.SerializeToString()).run( + None, _runtime_inputs(model, inference, column_types) + )[0] + + assert list(encoder.ct.transformers_[0][1].named_steps["ohe"].categories_[0]) == [ + "blue", + "nan", + "red", + ] + assert not BRANCHING_OPS & _emitted_ops(model) + np.testing.assert_allclose(actual, expected, rtol=1e-5, atol=1e-5) + + +def test_date_features_are_rejected_without_imputation() -> None: + training = np.asarray([["2022-02-02"], ["2022-02-25"]], dtype=np.object_) + schema = _schema(("date",), (ColumnTypes.DATE_YMD_ISO8601,), training.shape) + encoder = MultiModalEncoder(impute_missing=False) + + with pytest.raises(ValueError, match="not supported while imputation is disabled"): + encoder.fit(training, np.zeros(2), schema) + + +@pytest.mark.parametrize("impute_missing", [True, False]) +def test_pipeline_forwards_the_imputation_setting(impute_missing: bool) -> None: + training = np.asarray([[float(index)] for index in range(12)], dtype=np.object_) + schema = _schema(("numeric",), (ColumnTypes.NUMERIC_REGULAR,), training.shape) + pipeline = SimpleTabularPipeline( + task="tabular_regression", + dataset_size=training.shape, + learner=_RecordingLearner, + schema=schema, + impute_missing=impute_missing, + ) + pipeline.fit(training, np.arange(12, dtype=np.float64), schema) + + encoder = pipeline.steps[0] + assert isinstance(encoder, MultiModalEncoder) + assert encoder.impute_missing is impute_missing + + +class _RecordingLearner: + def __init__(self, task: str, dataset_size: tuple[int, ...]) -> None: + self.task = task + self.dataset_size = dataset_size + + def fit( + self, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + schema: DatasetSchema, + *, + groups: npt.ArrayLike | None = None, + ) -> None: + self.n_features = X.shape[1] + + def transform(self, X: npt.NDArray[Any]) -> npt.NDArray[Any]: + return X + + def serialize(self) -> Any: + raise NotImplementedError + + def get_input_type(self) -> object: + return MultiModalEncoder().get_output_type() + + def get_output_type(self) -> object: + return MultiModalEncoder().get_output_type() diff --git a/tests/tabular/processors/test_scaler.py b/tests/tabular/processors/test_scaler.py index afb2c7b..12263df 100644 --- a/tests/tabular/processors/test_scaler.py +++ b/tests/tabular/processors/test_scaler.py @@ -1,12 +1,25 @@ import numpy as np + from falcon.tabular.processors.scaler_and_encoder import ScalerAndEncoder -from falcon.types import ColumnTypes +from falcon.types import ColumnTypes, DatasetSchema + -def test_scaler_encoder(): +def test_scaler_encoder() -> None: X = np.array([[1, 1, 1], [1, 2, 1]]) - expected_transformed = np.array([[0, 1, 0, 0], [0, 0, 1, 0]]) - encoder = ScalerAndEncoder(mask=[ColumnTypes.NUMERIC_REGULAR, ColumnTypes.CAT_LOW_CARD, ColumnTypes.NUMERIC_REGULAR]) - encoder.fit(X) + expected_transformed = np.array([[0, 0, 1, 0, 0, 0], [0, 0, 0, 1, 0, 0]]) + schema = DatasetSchema( + column_names=("numeric_a", "category", "numeric_b"), + column_types=( + ColumnTypes.NUMERIC_REGULAR, + ColumnTypes.CAT_LOW_CARD, + ColumnTypes.NUMERIC_REGULAR, + ), + target_name="target", + target_kind="regression", + dimensions=X.shape, + ) + encoder = ScalerAndEncoder() + encoder.fit(X, np.zeros(X.shape[0]), schema) transformed = encoder.transform(X) diff --git a/tests/tabular/tab_report.py b/tests/tabular/tab_report.py deleted file mode 100644 index 14524b8..0000000 --- a/tests/tabular/tab_report.py +++ /dev/null @@ -1,33 +0,0 @@ -from falcon.tabular import reporting -import numpy as np - - -def test_classification_report(): - y = np.random.randint(0, 3, size=100) - y_hat = np.random.randint(0, 3, size=100) - - metrics = reporting.print_classification_report(y, y_hat, silent = True) - - assert isinstance(metrics, dict) - required_metrics = ['ACC', 'BACC', 'PRECISION', 'RECALL', 'F1', 'B_PRECISION', 'B_RECALL', 'B_F1', 'SCORE'] - for rm in required_metrics: - assert rm in metrics.keys() - assert metrics[rm] is not None - #assert isinstance(metrics['CONF_MAT'], list) - assert metrics['SCORE'] >= 0 and metrics['SCORE'] <= 1 - -def test_regression_report(): - y = np.random.normal(0, 1, size=10000) - y_hat = np.random.normal(0, 1.25, size=10000) - - metrics = reporting.print_regression_report(y, y_hat, silent = True) - - assert isinstance(metrics, dict) - required_metrics = ['SCORE', 'R2', 'RMSE', 'MSE', 'MAE', 'RMSLE'] - for rm in required_metrics: - assert rm in metrics.keys() - assert metrics[rm] is not None - assert metrics['SCORE'] >= 0 and metrics['SCORE'] <= 1 - assert metrics['MSE'] >= 0 - assert metrics['RMSE'] >= 0 - assert metrics['MAE'] >= 0 \ No newline at end of file diff --git a/tests/tabular/test_best_candidate_selection.py b/tests/tabular/test_best_candidate_selection.py new file mode 100644 index 0000000..ab3c871 --- /dev/null +++ b/tests/tabular/test_best_candidate_selection.py @@ -0,0 +1,273 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any + +import numpy as np +import pytest +from numpy import typing as npt + +from falcon import Predictor +from falcon.config import PortfolioSource, RunConfig +from falcon.constants import TABULAR_CLASSIFICATION_TASK, TABULAR_REGRESSION_TASK +from falcon.runtime import Runtime +from falcon.tabular.calibration import fit_temperature +from falcon.tabular.candidates import EstimatorSpec, GreedyWeightedEnsemble +from falcon.tabular.conformal import fit_conformal_quantile +from falcon.tabular.models.sklearn_model import SklearnModel +from falcon.tabular.training import CandidateLearner +from falcon.types import ColumnTypes, DatasetSchema +from tests.fnnx_conformance import assert_fnnx_conforms, extract_fnnx_graph + + +def _schema(task: str, n_rows: int, n_features: int) -> DatasetSchema: + return DatasetSchema( + column_names=tuple(f"feature_{index}" for index in range(n_features)), + column_types=(ColumnTypes.NUMERIC_REGULAR,) * n_features, + target_name="target", + target_kind=( + "classification" if task == TABULAR_CLASSIFICATION_TASK else "regression" + ), + dimensions=(n_rows, n_features), + ) + + +def _regression_data() -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]: + rng = np.random.default_rng(11) + X = rng.normal(size=(80, 2)) + y = 3.0 * X[:, 0] - X[:, 1] + return X, y + + +def _classification_data() -> tuple[npt.NDArray[np.float64], npt.NDArray[np.int64]]: + rng = np.random.default_rng(29) + X = rng.normal(size=(90, 2)) + y = (X[:, 0] + X[:, 1] > 0).astype(np.int64) + return X, y + + +def _worst_first_regression_specs() -> tuple[EstimatorSpec, ...]: + return ( + EstimatorSpec("bad-ridge", "linear", {"alpha": 1e9}), + EstimatorSpec("good-ridge", "linear", {"alpha": 1e-2}), + ) + + +def _worst_first_classification_specs() -> tuple[EstimatorSpec, ...]: + return ( + EstimatorSpec("stump", "random_forest", {"n_estimators": 1, "max_depth": 1}), + EstimatorSpec("logistic", "linear", {"C": 1000.0, "max_iter": 500}), + ) + + +def _no_ensemble_config( + specs: tuple[EstimatorSpec, ...], + **overrides: Any, +) -> RunConfig: + return RunConfig( + candidate_sources=(PortfolioSource(specs=specs),), + ensemble_enabled=False, + plateau_enabled=False, + oof_folds=3, + eval_strategy=None, + **overrides, + ) + + +def test_all_candidates_are_trained_and_the_best_one_wins() -> None: + X, y = _regression_data() + specs = _worst_first_regression_specs() + predictor = Predictor( + TABULAR_REGRESSION_TASK, + config=_no_ensemble_config(specs), + ).fit((X, y)) + + leaderboard = predictor.leaderboard() + assert list(leaderboard["candidate"]) == ["bad-ridge", "good-ridge"] + assert list(leaderboard["weight"]) == [0.0, 1.0] + assert all(fit_time > 0 for fit_time in leaderboard["fit_time"]) + scores = dict(zip(leaderboard["candidate"], leaderboard["score"], strict=True)) + assert scores["good-ridge"] > scores["bad-ridge"] + + assert predictor._learner is not None + assert not isinstance(predictor._learner.model, GreedyWeightedEnsemble) + np.testing.assert_allclose(predictor.predict(X), y, atol=0.5) + + +def test_single_candidate_path_keeps_one_fit_and_no_oof_round( + monkeypatch: pytest.MonkeyPatch, +) -> None: + X, y = _regression_data() + fit_calls: list[int] = [] + original_fit = SklearnModel.fit + + def counting_fit(self: SklearnModel, *args: Any, **kwargs: Any) -> None: + fit_calls.append(1) + original_fit(self, *args, **kwargs) + + monkeypatch.setattr(SklearnModel, "fit", counting_fit) + spec = EstimatorSpec("only-ridge", "linear", {"alpha": 1.0}) + config = _no_ensemble_config((spec,)) + learner = CandidateLearner(TABULAR_REGRESSION_TASK, X.shape, config) + + learner.fit(X, y, _schema(TABULAR_REGRESSION_TASK, *X.shape)) + + assert len(fit_calls) == 1 + assert learner._evaluation_run is None + records = learner.leaderboard_records() + assert len(records) == 1 + assert records[0]["candidate"] == "only-ridge" + assert records[0]["weight"] == 1.0 + + +def test_single_candidate_with_conformal_adds_only_the_required_oof_round( + monkeypatch: pytest.MonkeyPatch, +) -> None: + X, y = _regression_data() + fit_calls: list[int] = [] + original_fit = SklearnModel.fit + + def counting_fit(self: SklearnModel, *args: Any, **kwargs: Any) -> None: + fit_calls.append(1) + original_fit(self, *args, **kwargs) + + monkeypatch.setattr(SklearnModel, "fit", counting_fit) + spec = EstimatorSpec("only-ridge", "linear", {"alpha": 1.0}) + config = _no_ensemble_config((spec,), conformal_alpha=0.2) + learner = CandidateLearner(TABULAR_REGRESSION_TASK, X.shape, config) + + learner.fit(X, y, _schema(TABULAR_REGRESSION_TASK, *X.shape)) + + assert len(fit_calls) == config.oof_folds + 1 + assert learner.conformal_quantile_ is not None + + +def test_calibration_temperature_is_fitted_on_the_winner_oof_predictions() -> None: + X, y = _classification_data() + specs = _worst_first_classification_specs() + config = _no_ensemble_config(specs, calibrate=True) + learner = CandidateLearner(TABULAR_CLASSIFICATION_TASK, X.shape, config) + + learner.fit(X, y, _schema(TABULAR_CLASSIFICATION_TASK, *X.shape)) + + run = learner._evaluation_run + assert run is not None + assert [candidate.spec.name for candidate in run.candidates] == [ + "stump", + "logistic", + ] + winner_index = int(np.argmax(run.ensemble.weights)) + winner = run.candidates[winner_index] + assert winner.spec.name == "logistic" + assert run.ensemble.weights == (0.0, 1.0) + + oof_result = learner._weighted_oof_predictions() + assert oof_result is not None + evaluation_indices, probabilities = oof_result + np.testing.assert_array_equal(probabilities, winner.oof_predictions) + assert learner.temperature_ == fit_temperature( + winner.oof_predictions, + y[evaluation_indices], + ) + + +def test_conformal_quantile_is_fitted_on_the_winner_oof_predictions() -> None: + X, y = _regression_data() + specs = _worst_first_regression_specs() + config = _no_ensemble_config(specs, conformal_alpha=0.2) + learner = CandidateLearner(TABULAR_REGRESSION_TASK, X.shape, config) + + learner.fit(X, y, _schema(TABULAR_REGRESSION_TASK, *X.shape)) + + run = learner._evaluation_run + assert run is not None + winner = run.candidates[int(np.argmax(run.ensemble.weights))] + loser = run.candidates[int(np.argmin(run.ensemble.weights))] + assert winner.spec.name == "good-ridge" + + evaluation_targets = y[run.evaluation_indices] + winner_quantile = fit_conformal_quantile( + winner.oof_predictions, + evaluation_targets, + 0.2, + ) + loser_quantile = fit_conformal_quantile( + loser.oof_predictions, + evaluation_targets, + 0.2, + ) + assert learner.conformal_quantile_ == winner_quantile + assert winner_quantile < loser_quantile + + +def test_exported_bundle_contains_a_single_refit_model(tmp_path: Path) -> None: + X, y = _regression_data() + specs = ( + EstimatorSpec("forest-a", "random_forest", {"n_estimators": 3, "max_depth": 3}), + EstimatorSpec("forest-b", "random_forest", {"n_estimators": 5, "max_depth": 3}), + ) + predictor = Predictor( + TABULAR_REGRESSION_TASK, + config=_no_ensemble_config(specs), + ).fit((X, y)) + + assert len(predictor.leaderboard()) == 2 + artifact_path = tmp_path / "single-model.fnnx" + bundle = predictor.save(artifact_path) + extracted = extract_fnnx_graph(bundle) + + assert_fnnx_conforms(extracted) + tree_nodes = [ + node + for node in extracted.model.graph.node + if node.op_type == "TreeEnsembleRegressor" + ] + assert len(tree_nodes) == 1 + np.testing.assert_allclose( + Runtime(str(artifact_path)).predict(X.astype(np.float32)), + predictor.predict(X), + rtol=1e-5, + atol=1e-5, + ) + + +def test_calibration_and_conformal_round_trip_with_ensembling_off( + tmp_path: Path, +) -> None: + X, y = _classification_data() + predictor = Predictor( + TABULAR_CLASSIFICATION_TASK, + config=_no_ensemble_config( + _worst_first_classification_specs(), + calibrate=True, + ), + ).fit((X, y)) + artifact_path = tmp_path / "calibrated.fnnx" + predictor.save(artifact_path) + probabilities = predictor.predict_proba(X) + + np.testing.assert_allclose(probabilities.sum(axis=1), 1.0, atol=1e-6) + np.testing.assert_allclose( + Runtime(str(artifact_path)).predict_proba(X.astype(np.float32)), + probabilities, + rtol=1e-5, + atol=1e-6, + ) + + X_reg, y_reg = _regression_data() + regression_predictor = Predictor( + TABULAR_REGRESSION_TASK, + config=_no_ensemble_config( + _worst_first_regression_specs(), + conformal_alpha=0.2, + ), + ).fit((X_reg, y_reg)) + regression_path = tmp_path / "conformal.fnnx" + regression_predictor.save(regression_path) + lower, upper = Runtime(str(regression_path)).predict_interval( + X_reg.astype(np.float32) + ) + predictions = regression_predictor.predict(X_reg) + + assert np.all(lower <= upper) + np.testing.assert_allclose((lower + upper) / 2, predictions, rtol=1e-4, atol=1e-4) diff --git a/tests/tabular/test_candidates.py b/tests/tabular/test_candidates.py new file mode 100644 index 0000000..2d92572 --- /dev/null +++ b/tests/tabular/test_candidates.py @@ -0,0 +1,566 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, cast + +import numpy as np +import onnx +import onnxruntime as ort +import pytest +from numpy import typing as npt +from sklearn.datasets import make_classification, make_regression +from sklearn.metrics import balanced_accuracy_score + +from falcon.constants import TABULAR_CLASSIFICATION_TASK, TABULAR_REGRESSION_TASK +from falcon.serialization import SerializedModelRepr, serialize_to_onnx +from falcon.tabular.candidates import ( + CandidateModel, + CandidateTrainer, + EstimatorSpec, + default_portfolio, +) +from falcon.tabular.models.gbdt import get_gbdt_model_classes +from falcon.tabular.models.sklearn_model import SklearnModel +from falcon.tabular.splitting import holdout_indices + + +class _Clock: + def __init__(self) -> None: + self.value = 0.0 + + def __call__(self) -> float: + return self.value + + def advance(self, seconds: float) -> None: + self.value += seconds + + +class _RecordingModel: + def __init__(self, clock: _Clock, duration: float) -> None: + self.clock = clock + self.duration = duration + self.X: npt.NDArray[Any] | None = None + self.y: npt.NDArray[Any] | None = None + self.sample_weight: npt.NDArray[np.float64] | None = None + self.validation_data: tuple[npt.NDArray[Any], npt.NDArray[Any]] | None = None + self.early_stopping_rounds: int | None = None + + def fit( + self, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + *, + sample_weight: npt.NDArray[np.float64] | None = None, + validation_data: tuple[npt.NDArray[Any], npt.NDArray[Any]] | None = None, + early_stopping_rounds: int | None = None, + ) -> None: + self.X = X.copy() + self.y = y.copy() + self.sample_weight = None if sample_weight is None else sample_weight.copy() + self.validation_data = validation_data + self.early_stopping_rounds = early_stopping_rounds + self.clock.advance(self.duration) + + def predict(self, X: npt.NDArray[Any]) -> npt.NDArray[np.int64]: + return np.zeros(len(X), dtype=np.int64) + + def serialize(self) -> SerializedModelRepr: + raise NotImplementedError + + +class _RecordingFactory: + def __init__(self, clock: _Clock, durations: Mapping[str, float]) -> None: + self.clock = clock + self.durations = durations + self.models: list[_RecordingModel] = [] + self.random_states: list[int] = [] + + def __call__( + self, + spec: EstimatorSpec, + task: str, + random_state: int, + n_classes: int | None, + ) -> CandidateModel: + model = _RecordingModel(self.clock, self.durations[spec.name]) + self.models.append(model) + self.random_states.append(random_state) + return model + + +def _spec( + name: str, + *, + early_stopping_rounds: int | None = None, +) -> EstimatorSpec: + return EstimatorSpec( + name=name, + family="hist_gradient_boosting", + parameters={}, + early_stopping_rounds=early_stopping_rounds, + ) + + +def test_default_portfolio_is_gbdt_first_and_interleaved( + monkeypatch: pytest.MonkeyPatch, +) -> None: + from falcon.tabular import candidates + + monkeypatch.setattr( + candidates, + "get_gbdt_model_classes", + lambda task, n_classes=None: { + "lightgbm": object, + "xgboost": object, + "catboost": object, + }, + ) + + portfolio = default_portfolio(TABULAR_CLASSIFICATION_TASK, n_classes=2) + families = [spec.family for spec in portfolio] + + assert families[:6] == [ + "lightgbm", + "hist_gradient_boosting", + "xgboost", + "extra_trees", + "catboost", + "linear", + ] + assert len({spec.name for spec in portfolio}) == len(portfolio) + assert any(spec.family == "random_forest" for spec in portfolio) + assert any( + spec.family == "lightgbm" and spec.parameters.get("num_leaves") == 128 + for spec in portfolio + ) + + +def test_default_portfolio_applies_catboost_multiclass_parity_gate( + caplog: pytest.LogCaptureFixture, +) -> None: + if "catboost" not in get_gbdt_model_classes( + TABULAR_CLASSIFICATION_TASK, + n_classes=2, + ): + pytest.skip("catboost is not installed") + + binary_portfolio = default_portfolio( + TABULAR_CLASSIFICATION_TASK, + n_classes=2, + ) + regression_portfolio = default_portfolio(TABULAR_REGRESSION_TASK) + with caplog.at_level("INFO", logger="falcon"): + multiclass_portfolio = default_portfolio( + TABULAR_CLASSIFICATION_TASK, + n_classes=3, + ) + + assert any(spec.family == "catboost" for spec in binary_portfolio) + assert any(spec.family == "catboost" for spec in regression_portfolio) + assert all(spec.family != "catboost" for spec in multiclass_portfolio) + assert "CatBoost is excluded from multiclass classification" in caplog.text + + +def test_default_portfolio_degrades_to_sklearn_with_one_log( + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, +) -> None: + from falcon.tabular import candidates + + monkeypatch.setattr( + candidates, + "get_gbdt_model_classes", + lambda task, n_classes=None: {}, + ) + + portfolio = default_portfolio(TABULAR_REGRESSION_TASK) + caplog.clear() + X = np.arange(80, dtype=np.float32).reshape(40, 2) + y = np.zeros(40, dtype=np.float32) + + with caplog.at_level("INFO", logger="falcon"): + run = CandidateTrainer( + TABULAR_REGRESSION_TASK, + time_limit=np.finfo(float).eps, + ).fit(X, y) + + assert {spec.family for spec in portfolio} == { + "hist_gradient_boosting", + "extra_trees", + "linear", + "random_forest", + } + assert len(run.candidates) == 1 + assert run.candidates[0].spec.family == "hist_gradient_boosting" + assert run.stopped_for_budget + assert run.candidates[0].model.predict(X).shape == (len(X),) + graph = serialize_to_onnx( + [run.candidates[0].model.serialize()], + task=TABULAR_REGRESSION_TASK, + ) + onnx.checker.check_model(graph) + assert run.elapsed_time <= ( + run.candidates[0].fit_time + float(np.finfo(float).eps) + 1.0 + ) + assert caplog.text.count("sklearn-only candidate portfolio") == 1 + assert "time limit is insufficient for the first candidate" in caplog.text + + +def test_estimator_spec_validates_guards_and_centralized_seed() -> None: + spec = EstimatorSpec( + name="bounded", + family="linear", + parameters={"alpha": 1.0}, + min_rows=10, + max_rows=100, + max_features=5, + ) + + assert spec.is_applicable(n_rows=10, n_features=5) + assert not spec.is_applicable(n_rows=9, n_features=5) + assert not spec.is_applicable(n_rows=10, n_features=6) + + with pytest.raises(ValueError, match="min_rows"): + EstimatorSpec("invalid", "linear", min_rows=10, max_rows=5) + with pytest.raises(ValueError, match="early_stopping_rounds"): + EstimatorSpec("invalid", "linear", early_stopping_rounds=0) + + +def test_budget_reserve_stops_before_next_candidate_and_guarantees_first( + caplog: pytest.LogCaptureFixture, +) -> None: + clock = _Clock() + factory = _RecordingFactory(clock, {"first": 9.0, "second": 1.0}) + trainer = CandidateTrainer( + TABULAR_REGRESSION_TASK, + time_limit=10.0, + reserve_fraction=0.2, + model_factory=factory, + clock=clock, + ) + X = np.arange(24, dtype=np.float32).reshape(12, 2) + y = np.arange(12, dtype=np.float32) + + with caplog.at_level("INFO", logger="falcon"): + run = trainer.fit(X, y, specs=[_spec("first"), _spec("second")]) + + assert [candidate.spec.name for candidate in run.candidates] == ["first"] + assert run.stopped_for_budget + assert run.elapsed_time == 9.0 + assert "time limit is insufficient for the first candidate" in caplog.text + assert "Candidate 1/2" in caplog.text + assert "estimated remaining time" in caplog.text + + +def test_classification_leaves_sample_weights_off_by_default() -> None: + clock = _Clock() + factory = _RecordingFactory(clock, {"weighted": 0.0}) + trainer = CandidateTrainer( + TABULAR_CLASSIFICATION_TASK, + model_factory=factory, + clock=clock, + ) + X = np.arange(48, dtype=np.float32).reshape(24, 2) + y = np.asarray([0] * 20 + [1] * 4) + + trainer.fit(X, y, specs=[_spec("weighted")]) + + model = factory.models[0] + assert model.X is not None + assert len(model.X) == len(X) + assert model.sample_weight is None + + +def test_balanced_class_weight_opts_into_sample_weights_without_resampling() -> None: + clock = _Clock() + factory = _RecordingFactory(clock, {"weighted": 0.0}) + trainer = CandidateTrainer( + TABULAR_CLASSIFICATION_TASK, + class_weight="balanced", + model_factory=factory, + clock=clock, + ) + X = np.arange(48, dtype=np.float32).reshape(24, 2) + y = np.asarray([0] * 20 + [1] * 4) + + trainer.fit(X, y, specs=[_spec("weighted")]) + + # The first model built is the throwaway used by the up-front capability check. + model = factory.models[-1] + assert model.X is not None + assert model.sample_weight is not None + assert len(model.X) == len(X) + assert len(model.sample_weight) == len(X) + assert model.sample_weight[y == 1].mean() > model.sample_weight[y == 0].mean() + + +class _WeightBlindEstimator: + def fit(self, X: npt.NDArray[Any], y: npt.NDArray[Any]) -> None: + del X, y + + +class _WeightBlindModel: + def __init__(self) -> None: + self.estimator = _WeightBlindEstimator() + + def fit( + self, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + *, + sample_weight: npt.NDArray[np.float64] | None = None, + validation_data: tuple[npt.NDArray[Any], npt.NDArray[Any]] | None = None, + early_stopping_rounds: int | None = None, + ) -> None: + self.estimator.fit(X, y) + + def predict(self, X: npt.NDArray[Any]) -> npt.NDArray[np.int64]: + return np.zeros(len(X), dtype=np.int64) + + def serialize(self) -> SerializedModelRepr: + raise NotImplementedError + + +def test_balanced_class_weight_rejects_families_that_cannot_take_weights() -> None: + def factory( + spec: EstimatorSpec, + task: str, + random_state: int, + n_classes: int | None, + ) -> CandidateModel: + return _WeightBlindModel() + + trainer = CandidateTrainer( + TABULAR_CLASSIFICATION_TASK, + class_weight="balanced", + model_factory=factory, + ) + X = np.arange(48, dtype=np.float32).reshape(24, 2) + y = np.asarray([0] * 12 + [1] * 12) + spec = EstimatorSpec("blind", "weight_blind", {}) + + with pytest.raises(ValueError, match="weight_blind"): + trainer.fit(X, y, specs=[spec]) + + +def test_a_model_factory_without_an_estimator_is_allowed_through() -> None: + clock = _Clock() + factory = _RecordingFactory(clock, {"opaque": 0.0}) + trainer = CandidateTrainer( + TABULAR_CLASSIFICATION_TASK, + class_weight="balanced", + model_factory=factory, + clock=clock, + ) + X = np.arange(48, dtype=np.float32).reshape(24, 2) + y = np.asarray([0] * 12 + [1] * 12) + + run = trainer.fit(X, y, specs=[_spec("opaque")]) + + assert not hasattr(factory.models[-1], "estimator") + assert len(run.candidates) == 1 + + +def test_sklearn_models_omit_the_sample_weight_keyword_when_it_is_unset() -> None: + unset = object() + received: list[object] = [] + + class _SpyEstimator: + def fit( + self, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + sample_weight: Any = unset, + ) -> None: + received.append(sample_weight) + + X = np.arange(20, dtype=np.float32).reshape(10, 2) + y = np.arange(10) % 2 + weights = np.ones(10, dtype=np.float64) + + SklearnModel(_SpyEstimator(), TABULAR_CLASSIFICATION_TASK).fit(X, y) + SklearnModel(_SpyEstimator(), TABULAR_CLASSIFICATION_TASK).fit( + X, + y, + sample_weight=weights, + ) + + assert received[0] is unset + assert np.array_equal(np.asarray(received[1]), weights) + + +def test_early_stopping_validation_split_is_group_aware_and_reproducible() -> None: + X = np.column_stack( + ( + np.arange(24, dtype=np.float32), + np.tile(np.asarray([0.0, 1.0], dtype=np.float32), 12), + ) + ) + groups = np.repeat(np.arange(12), 2) + y = np.repeat(np.arange(12) % 2, 2) + spec = _spec("early", early_stopping_rounds=5) + trained_indices: list[npt.NDArray[np.int64]] = [] + + for _ in range(2): + clock = _Clock() + factory = _RecordingFactory(clock, {"early": 0.0}) + trainer = CandidateTrainer( + TABULAR_CLASSIFICATION_TASK, + random_state=17, + model_factory=factory, + clock=clock, + ) + trainer.fit(X, y, groups=groups, specs=[spec]) + model = factory.models[0] + assert factory.random_states == [17] + assert model.X is not None + assert model.validation_data is not None + validation_X, _ = model.validation_data + train_indices = model.X[:, 0].astype(np.int64) + validation_indices = validation_X[:, 0].astype(np.int64) + assert set(groups[train_indices]).isdisjoint(groups[validation_indices]) + assert model.early_stopping_rounds == 5 + trained_indices.append(train_indices) + + assert np.array_equal(trained_indices[0], trained_indices[1]) + + +def test_weighted_sklearn_candidate_is_reproducible_and_serializable() -> None: + X, y = make_classification( + n_samples=320, + n_features=6, + n_informative=5, + n_redundant=0, + weights=[0.95, 0.05], + class_sep=3.0, + flip_y=0, + random_state=3, + ) + X = X.astype(np.float32) + train_indices, eval_indices = holdout_indices( + X, + y, + TABULAR_CLASSIFICATION_TASK, + groups=np.arange(len(X)), + random_state=23, + ) + spec = EstimatorSpec( + name="hist", + family="hist_gradient_boosting", + parameters={"max_iter": 40, "min_samples_leaf": 5}, + ) + predictions: list[npt.NDArray[Any]] = [] + models: list[CandidateModel] = [] + + for _ in range(2): + run = CandidateTrainer( + TABULAR_CLASSIFICATION_TASK, + random_state=23, + ).fit(X[train_indices], y[train_indices], specs=[spec]) + model = run.candidates[0].model + predictions.append(model.predict(X[eval_indices])) + models.append(model) + + assert np.array_equal(predictions[0], predictions[1]) + assert balanced_accuracy_score(y[eval_indices], predictions[0]) > 0.8 + + graph = serialize_to_onnx( + [models[0].serialize()], + task=TABULAR_CLASSIFICATION_TASK, + ) + onnx.checker.check_model(graph) + + +def test_hist_gradient_boosting_uses_external_early_stopping() -> None: + X, y = make_regression( + n_samples=80, + n_features=5, + n_informative=4, + noise=8.0, + random_state=9, + ) + X = X.astype(np.float32) + spec = EstimatorSpec( + name="hist_early", + family="hist_gradient_boosting", + parameters={"max_iter": 25, "min_samples_leaf": 5}, + early_stopping_rounds=3, + ) + + run = CandidateTrainer( + TABULAR_REGRESSION_TASK, + random_state=11, + ).fit(X, y, specs=[spec], groups=np.arange(len(X))) + estimator = cast(Any, run.candidates[0].model).estimator + + assert estimator.early_stopping is False + assert estimator.warm_start is False + assert 1 <= estimator.max_iter <= 25 + assert run.candidates[0].model.predict(X).shape == (len(X),) + + +@pytest.mark.parametrize("family", ["lightgbm", "xgboost", "catboost"]) +def test_optional_gbdt_candidate_accepts_external_early_stopping_split( + family: str, +) -> None: + if family not in get_gbdt_model_classes(TABULAR_CLASSIFICATION_TASK, n_classes=2): + pytest.skip(f"{family} is not installed") + X, y = make_classification( + n_samples=80, + n_features=5, + n_informative=4, + n_redundant=0, + random_state=12, + ) + X = X.astype(np.float32) + parameter_name = "iterations" if family == "catboost" else "n_estimators" + spec = EstimatorSpec( + name=family, + family=family, + parameters={parameter_name: 20}, + early_stopping_rounds=3, + ) + + run = CandidateTrainer( + TABULAR_CLASSIFICATION_TASK, + random_state=11, + ).fit(X, y, specs=[spec], groups=np.arange(len(X))) + model = run.candidates[0].model + graph = serialize_to_onnx( + [model.serialize()], + task=TABULAR_CLASSIFICATION_TASK, + ) + session = ort.InferenceSession( + graph.SerializeToString(), providers=["CPUExecutionProvider"] + ) + labels, probabilities = session.run( + None, + {session.get_inputs()[0].name: X}, + ) + + assert np.array_equal(np.asarray(labels).reshape(-1), model.predict(X)) + np.testing.assert_allclose( + probabilities, + cast(Any, model).predict_proba(X), + rtol=1e-5, + atol=1e-6, + ) + + +@pytest.mark.parametrize( + ("kwargs", "message"), + [ + ({"time_limit": 0.0}, "time_limit"), + ({"reserve_fraction": 1.0}, "reserve_fraction"), + ], +) +def test_candidate_trainer_rejects_invalid_budget_settings( + kwargs: dict[str, Any], message: str +) -> None: + with pytest.raises(ValueError, match=message): + CandidateTrainer(TABULAR_REGRESSION_TASK, **kwargs) + + +def test_candidate_trainer_rejects_unknown_task() -> None: + with pytest.raises(ValueError, match="Unknown task"): + CandidateTrainer("forecasting") diff --git a/tests/tabular/test_decision.py b/tests/tabular/test_decision.py new file mode 100644 index 0000000..fc2146b --- /dev/null +++ b/tests/tabular/test_decision.py @@ -0,0 +1,253 @@ +from __future__ import annotations + +import numpy as np +import onnx +import onnxruntime as ort +import pytest +from numpy import typing as npt +from onnx import TensorProto, helper +from sklearn.metrics import balanced_accuracy_score, f1_score, matthews_corrcoef + +from falcon.config import ONNX_IR_VERSION, ONNX_OPSET_VERSION +from falcon.serialization import SerializedModelRepr +from falcon.tabular.decision import fit_decision_weights, serialize_decision_rule + + +def _binary_probabilities( + n_rows: int = 800, + prevalence: float = 0.12, + seed: int = 5, +) -> tuple[npt.NDArray[np.float32], npt.NDArray[np.int64]]: + rng = np.random.default_rng(seed) + targets = (rng.random(n_rows) < prevalence).astype(np.int64) + logits = ( + rng.normal(size=n_rows) + 1.4 * targets + np.log(prevalence / (1 - prevalence)) + ) + positive = 1.0 / (1.0 + np.exp(-logits)) + probabilities = np.column_stack((1.0 - positive, positive)).astype(np.float32) + return probabilities, targets + + +def _multiclass_probabilities( + n_rows: int = 900, + seed: int = 3, +) -> tuple[npt.NDArray[np.float32], npt.NDArray[np.int64]]: + rng = np.random.default_rng(seed) + prior = np.asarray([0.7, 0.2, 0.1]) + targets = rng.choice(3, size=n_rows, p=prior).astype(np.int64) + signal = rng.normal(size=(n_rows, 3)) + signal[np.arange(n_rows), targets] += 1.5 + scores = prior * np.exp(signal) + probabilities = (scores / scores.sum(axis=1, keepdims=True)).astype(np.float32) + return probabilities, targets + + +def test_binary_weights_are_equivalent_to_a_threshold() -> None: + probabilities, targets = _binary_probabilities() + + weights = fit_decision_weights(probabilities, targets, "balanced_accuracy") + threshold = weights[0] / (weights[0] + weights[1]) + weighted_labels = np.argmax( + probabilities * np.asarray(weights, dtype=np.float32), + axis=1, + ) + + assert weights != (1.0, 1.0) + np.testing.assert_array_equal( + weighted_labels, + (probabilities[:, 1] > threshold).astype(np.int64), + ) + assert balanced_accuracy_score(targets, weighted_labels) > balanced_accuracy_score( + targets, np.argmax(probabilities, axis=1) + ) + + +def _score( + metric: str, + targets: npt.NDArray[np.int64], + predicted: npt.NDArray[np.int64], +) -> float: + if metric == "balanced_accuracy": + return float(balanced_accuracy_score(targets, predicted)) + if metric == "mcc": + return float(matthews_corrcoef(targets, predicted)) + return float(f1_score(targets, predicted, average="macro", zero_division=0.0)) + + +@pytest.mark.parametrize("metric", ["balanced_accuracy", "f1", "mcc"]) +def test_binary_tuning_never_scores_below_plain_argmax(metric: str) -> None: + probabilities, targets = _binary_probabilities() + + weights = fit_decision_weights(probabilities, targets, metric) + labels = np.argmax(probabilities * np.asarray(weights, dtype=np.float32), axis=1) + + assert len(weights) == 2 + assert not np.array_equal(labels, np.zeros_like(labels)) + assert _score(metric, targets, labels) >= _score( + metric, targets, np.argmax(probabilities, axis=1) + ) + + +def test_binary_f1_is_the_macro_average_over_both_classes() -> None: + """`"f1"` must not depend on which label the encoder happened to map to 1.""" + probabilities, targets = _binary_probabilities() + + weights = fit_decision_weights(probabilities, targets, "f1") + flipped = fit_decision_weights(probabilities[:, ::-1], 1 - targets, "f1") + + assert weights == pytest.approx(tuple(reversed(flipped)), abs=1e-6) + + +def test_multiclass_coordinate_ascent_improves_the_metric() -> None: + probabilities, targets = _multiclass_probabilities() + + weights = fit_decision_weights(probabilities, targets, "balanced_accuracy") + tuned = balanced_accuracy_score( + targets, + np.argmax(probabilities * np.asarray(weights, dtype=np.float32), axis=1), + ) + + assert len(weights) == 3 + assert tuned > balanced_accuracy_score(targets, np.argmax(probabilities, axis=1)) + + +def test_multiclass_weights_are_normalized_without_changing_labels() -> None: + probabilities, targets = _multiclass_probabilities() + + weights = np.asarray( + fit_decision_weights(probabilities, targets, "balanced_accuracy"), + dtype=np.float32, + ) + + assert float(weights.mean()) == pytest.approx(1.0, abs=1e-6) + for scale in (0.25, 7.4, 100.0): + np.testing.assert_array_equal( + np.argmax(probabilities * weights, axis=1), + np.argmax(probabilities * (weights * scale), axis=1), + ) + + +def test_small_class_count_returns_the_no_op_rule() -> None: + probabilities, targets = _binary_probabilities(n_rows=400, prevalence=0.05, seed=11) + assert int(np.bincount(targets).min()) < 50 + + assert fit_decision_weights(probabilities, targets, "balanced_accuracy") == ( + 1.0, + 1.0, + ) + + +def test_rule_that_cannot_beat_argmax_returns_the_no_op_rule() -> None: + rng = np.random.default_rng(19) + targets = np.repeat(np.asarray([0, 1], dtype=np.int64), 200) + confidence = rng.uniform(0.9, 0.99, size=len(targets)) + positive = np.where(targets == 1, confidence, 1.0 - confidence) + probabilities = np.column_stack((1.0 - positive, positive)).astype(np.float32) + + assert fit_decision_weights(probabilities, targets, "balanced_accuracy") == ( + 1.0, + 1.0, + ) + + +def test_unknown_metric_is_rejected() -> None: + probabilities, targets = _binary_probabilities(n_rows=200) + with pytest.raises(ValueError, match="decision metric"): + fit_decision_weights(probabilities, targets, "accuracy") + + +@pytest.mark.parametrize( + ("probabilities", "targets", "message"), + [ + (np.zeros((4, 1), dtype=np.float32), np.zeros(4, dtype=np.int64), "two class"), + ( + np.full((4, 2), 0.5, dtype=np.float32), + np.zeros(3, dtype=np.int64), + "one value per probability row", + ), + ( + np.full((4, 2), 0.5, dtype=np.float32), + np.zeros(4, dtype=np.float64), + "integer encoded", + ), + ], +) +def test_invalid_inputs_are_rejected( + probabilities: npt.NDArray[np.float32], + targets: npt.NDArray[np.int64], + message: str, +) -> None: + with pytest.raises(ValueError, match=message): + fit_decision_weights(probabilities, targets, "balanced_accuracy") + + +def _classifier_graph(n_classes: int) -> SerializedModelRepr: + """A minimal `[labels, probabilities]` graph shaped like a learner export.""" + input_info = helper.make_tensor_value_info( + "model_input", + TensorProto.FLOAT, + [None, n_classes], + ) + nodes = [ + helper.make_node("Softmax", ["model_input"], ["probabilities"], axis=1), + helper.make_node( + "ArgMax", + ["probabilities"], + ["label"], + axis=1, + keepdims=0, + ), + ] + graph = helper.make_graph( + nodes, + "classifier", + [input_info], + [ + helper.make_tensor_value_info("label", TensorProto.INT64, [None]), + helper.make_tensor_value_info( + "probabilities", + TensorProto.FLOAT, + [None, n_classes], + ), + ], + ) + model = helper.make_model( + graph, + opset_imports=[helper.make_opsetid("", ONNX_OPSET_VERSION)], + ir_version=ONNX_IR_VERSION, + ) + return SerializedModelRepr(model, 1, 2, ["FLOAT32"], [[None, n_classes]]) + + +@pytest.mark.parametrize("weights", [(0.2, 0.8), (1.0, 3.0, 0.5)]) +def test_serialized_rule_matches_the_weighted_argmax( + weights: tuple[float, ...], +) -> None: + n_classes = len(weights) + serialized = serialize_decision_rule(_classifier_graph(n_classes), weights) + model = serialized.get_model() + onnx.checker.check_model(model, full_check=True) + + rng = np.random.default_rng(7) + logits = rng.normal(size=(64, n_classes)).astype(np.float32) + session = ort.InferenceSession( + model.SerializeToString(), + providers=["CPUExecutionProvider"], + ) + labels, probabilities = session.run(None, {"model_input": logits}) + expected = np.argmax( + probabilities * np.asarray(weights, dtype=np.float32), + axis=1, + ) + + assert np.asarray(labels).dtype == np.int64 + np.testing.assert_array_equal(np.asarray(labels), expected) + assert {"Mul", "ArgMax"} <= { + node.op_type for node in model.graph.node if "falcon_decision" in node.name + } + assert all(node.domain in {"", "ai.onnx"} for node in model.graph.node) + + +def test_serialized_rule_rejects_a_mismatched_class_count() -> None: + with pytest.raises(ValueError, match="one weight per probability column"): + serialize_decision_rule(_classifier_graph(3), (0.5, 0.5)) diff --git a/tests/tabular/test_ensembling.py b/tests/tabular/test_ensembling.py new file mode 100644 index 0000000..005b55c --- /dev/null +++ b/tests/tabular/test_ensembling.py @@ -0,0 +1,700 @@ +from __future__ import annotations + +from collections import defaultdict +from collections.abc import Callable, Mapping +from pathlib import Path +from typing import Any + +import numpy as np +import onnx +import onnxruntime as ort +import pytest +from numpy import typing as npt +from onnx import TensorProto, helper +from sklearn.datasets import make_classification + +from falcon.config import ONNX_OPSET_VERSION +from falcon.constants import TABULAR_CLASSIFICATION_TASK, TABULAR_REGRESSION_TASK +from falcon.runtime import Runtime +from falcon.serialization import ( + FNNXSerializer, + SerializedModelRepr, + serialize_to_onnx, +) +from falcon.tabular.candidates import ( + CandidateModel, + EstimatorSpec, + OOFEnsembleTrainer, + greedy_weighted_selection, + score_oof_predictions, +) +from falcon.tabular.processors.label_decoder import LabelDecoder +from falcon.tabular.processors.multi_modal_encoder import MultiModalEncoder +from falcon.types import ColumnTypes, DatasetSchema +from tests.fnnx_conformance import assert_fnnx_conforms, extract_fnnx_graph + + +class _ConstantModel: + def __init__(self, task: str, value: float = 0.0) -> None: + self.task = task + self.value = value + self.fit_rows: npt.NDArray[np.int64] | None = None + self.prediction_rows: list[npt.NDArray[np.int64]] = [] + + def fit( + self, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + *, + sample_weight: npt.NDArray[np.float64] | None = None, + validation_data: tuple[npt.NDArray[Any], npt.NDArray[Any]] | None = None, + early_stopping_rounds: int | None = None, + ) -> None: + self.fit_rows = X[:, 0].astype(np.int64) + if self.task == TABULAR_REGRESSION_TASK: + self.value = float(np.mean(y)) + + def predict(self, X: npt.NDArray[Any]) -> npt.NDArray[Any]: + self.prediction_rows.append(X[:, 0].astype(np.int64)) + if self.task == TABULAR_CLASSIFICATION_TASK: + return np.zeros(len(X), dtype=np.int64) + return np.full(len(X), self.value, dtype=np.float32) + + def predict_proba(self, X: npt.NDArray[Any]) -> npt.NDArray[np.float32]: + self.prediction_rows.append(X[:, 0].astype(np.int64)) + return np.tile( + np.asarray([[0.6, 0.4]], dtype=np.float32), + (len(X), 1), + ) + + def serialize(self) -> SerializedModelRepr: + raise NotImplementedError + + +class _ConstantFactory: + def __init__(self) -> None: + self.models: dict[str, list[_ConstantModel]] = defaultdict(list) + self.seeds: dict[str, list[int]] = defaultdict(list) + + def __call__( + self, + spec: EstimatorSpec, + task: str, + random_state: int, + n_classes: int | None, + ) -> CandidateModel: + model = _ConstantModel(task) + self.models[spec.name].append(model) + self.seeds[spec.name].append(random_state) + return model + + +class _DummyRegressionFactory: + def __call__( + self, + spec: EstimatorSpec, + task: str, + random_state: int, + n_classes: int | None, + ) -> CandidateModel: + constant = spec.parameters.get("constant") + if not isinstance(constant, (int, float)): + raise ValueError("Dummy regression candidates require a constant") + return _SerializableConstantRegressor(float(constant)) + + +class _SerializableConstantRegressor: + def __init__(self, constant: float) -> None: + self.constant = constant + self.n_features: int | None = None + + def fit( + self, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + *, + sample_weight: npt.NDArray[np.float64] | None = None, + validation_data: tuple[npt.NDArray[Any], npt.NDArray[Any]] | None = None, + early_stopping_rounds: int | None = None, + ) -> None: + self.n_features = X.shape[1] + + def predict(self, X: npt.NDArray[Any]) -> npt.NDArray[np.float32]: + return np.full(len(X), self.constant, dtype=np.float32) + + def serialize(self) -> SerializedModelRepr: + if self.n_features is None: + raise RuntimeError("Model must be fitted before serialization") + input_info = helper.make_tensor_value_info( + "model_input", + TensorProto.FLOAT, + [None, self.n_features], + ) + output_info = helper.make_tensor_value_info( + "prediction", + TensorProto.FLOAT, + [None, 1], + ) + weights = helper.make_tensor( + "weights", + TensorProto.FLOAT, + [self.n_features, 1], + [0.0] * self.n_features, + ) + bias = helper.make_tensor( + "bias", + TensorProto.FLOAT, + [1], + [self.constant], + ) + graph = helper.make_graph( + [ + helper.make_node( + "MatMul", + ["model_input", "weights"], + ["zero_prediction"], + ), + helper.make_node( + "Add", + ["zero_prediction", "bias"], + ["prediction"], + ), + ], + "constant_regressor", + [input_info], + [output_info], + initializer=[weights, bias], + ) + model = helper.make_model( + graph, + opset_imports=[helper.make_opsetid("", ONNX_OPSET_VERSION)], + ) + return SerializedModelRepr( + model, + n_inputs=1, + n_outputs=1, + initial_types=["FLOAT32"], + initial_shapes=[[None, self.n_features]], + ) + + +class _Clock: + def __init__(self) -> None: + self.value = 0.0 + + def __call__(self) -> float: + return self.value + + def advance(self, seconds: float) -> None: + self.value += seconds + + +class _TimedSerializableConstantRegressor(_SerializableConstantRegressor): + def __init__(self, constant: float, clock: _Clock, duration: float) -> None: + super().__init__(constant) + self.clock = clock + self.duration = duration + + def fit( + self, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + *, + sample_weight: npt.NDArray[np.float64] | None = None, + validation_data: tuple[npt.NDArray[Any], npt.NDArray[Any]] | None = None, + early_stopping_rounds: int | None = None, + ) -> None: + super().fit( + X, + y, + sample_weight=sample_weight, + validation_data=validation_data, + early_stopping_rounds=early_stopping_rounds, + ) + self.clock.advance(self.duration) + + +class _TimedRegressionFactory: + def __init__( + self, + clock: _Clock, + warning_is_visible: Callable[[], bool], + ) -> None: + self.clock = clock + self.warning_is_visible = warning_is_visible + self.calls: list[str] = [] + self.warning_visible_before_second_fold = False + + def __call__( + self, + spec: EstimatorSpec, + task: str, + random_state: int, + n_classes: int | None, + ) -> CandidateModel: + del task, random_state, n_classes + self.calls.append(spec.name) + if len(self.calls) == 2: + self.warning_visible_before_second_fold = self.warning_is_visible() + constant = spec.parameters.get("constant") + if not isinstance(constant, (int, float)): + raise ValueError("Timed regression candidates require a constant") + return _TimedSerializableConstantRegressor( + float(constant), + self.clock, + duration=3.0, + ) + + +def _spec(name: str, parameters: Mapping[str, object] | None = None) -> EstimatorSpec: + return EstimatorSpec(name, "linear", parameters or {}) + + +def test_oof_collection_uses_group_aware_cross_validation() -> None: + X = np.column_stack( + ( + np.arange(24, dtype=np.float32), + np.tile(np.asarray([0.0, 1.0], dtype=np.float32), 12), + ) + ) + groups = np.repeat(np.arange(12), 2) + y = np.repeat(np.arange(12) % 2, 2) + factory = _ConstantFactory() + + run = OOFEnsembleTrainer( + TABULAR_CLASSIFICATION_TASK, + n_splits=4, + plateau_enabled=False, + model_factory=factory, + random_state=19, + ).fit(X, y, groups=groups, specs=[_spec("constant")]) + + assert np.array_equal(run.evaluation_indices, np.arange(len(X))) + assert len(run.candidates[0].models) == 4 + assert run.candidates[0].oof_predictions.shape == (len(X), 2) + assert run.candidates[0].oof_score == score_oof_predictions( + run.candidates[0].oof_predictions, + y, + TABULAR_CLASSIFICATION_TASK, + ) + assert factory.seeds["constant"] == [19, 20, 21, 22] + for model in factory.models["constant"]: + assert model.fit_rows is not None + assert len(model.prediction_rows) == 1 + training_groups = set(groups[model.fit_rows]) + evaluation_groups = set(groups[model.prediction_rows[0]]) + assert training_groups.isdisjoint(evaluation_groups) + + +def test_large_dataset_uses_single_holdout_oof_model() -> None: + row_count = 2_500 + X = np.column_stack( + ( + np.arange(row_count, dtype=np.float32), + np.linspace(-1.0, 1.0, row_count, dtype=np.float32), + ) + ) + y = np.linspace(0.0, 10.0, row_count, dtype=np.float32) + factory = _ConstantFactory() + + run = OOFEnsembleTrainer( + TABULAR_REGRESSION_TASK, + plateau_enabled=False, + model_factory=factory, + random_state=7, + ).fit(X, y, groups=np.arange(row_count), specs=[_spec("mean")]) + + assert len(run.candidates[0].models) == 1 + assert 0 < len(run.evaluation_indices) < row_count + assert len(run.evaluation_indices) == row_count // 4 + assert np.array_equal( + factory.models["mean"][0].prediction_rows[0], + run.evaluation_indices, + ) + + +def test_oof_budget_shortfall_warns_after_first_fold_and_keeps_first_candidate( + caplog: pytest.LogCaptureFixture, +) -> None: + clock = _Clock() + factory = _TimedRegressionFactory( + clock, + lambda: "time limit is insufficient" in caplog.text, + ) + X = np.arange(60, dtype=np.float32).reshape(30, 2) + y = np.zeros(len(X), dtype=np.float32) + specs = [ + _spec("first", {"constant": 0.0}), + _spec("second", {"constant": 1.0}), + ] + + with caplog.at_level("INFO", logger="falcon"): + run = OOFEnsembleTrainer( + TABULAR_REGRESSION_TASK, + n_splits=3, + plateau_enabled=False, + time_limit=5.0, + reserve_fraction=0.0, + model_factory=factory, + clock=clock, + ).fit(X, y, groups=np.arange(len(X)), specs=specs) + + assert factory.calls == ["first", "first", "first"] + assert factory.warning_visible_before_second_fold + assert caplog.text.count("time limit is insufficient") == 1 + assert "Candidate 1/2" in caplog.text + assert "estimated remaining time" in caplog.text + assert run.stopped_for_budget + assert run.elapsed_time == 9.0 + assert run.candidates[0].fit_time == 9.0 + assert run.ensemble.predict(X).shape == (len(X),) + + graph = run.ensemble.serialize().get_model() + onnx.checker.check_model(graph) + session = ort.InferenceSession( + graph.SerializeToString(), + providers=["CPUExecutionProvider"], + ) + runtime_predictions = session.run( + None, + {session.get_inputs()[0].name: X}, + )[0] + np.testing.assert_allclose(runtime_predictions, run.ensemble.predict(X)) + + +@pytest.mark.parametrize( + ("task", "y", "predictions"), + [ + ( + TABULAR_CLASSIFICATION_TASK, + np.asarray([0, 0, 1, 1]), + [ + np.asarray([[0.9, 0.1], [0.8, 0.2], [0.2, 0.8], [0.1, 0.9]]), + np.asarray([[0.4, 0.6], [0.7, 0.3], [0.3, 0.7], [0.6, 0.4]]), + ], + ), + ( + TABULAR_REGRESSION_TASK, + np.asarray([0.0, 1.0, 2.0, 3.0]), + [ + np.asarray([1.0, 2.0, 3.0, 4.0]), + np.asarray([-1.0, 0.0, 1.0, 2.0]), + ], + ), + ], +) +def test_greedy_selection_never_scores_below_best_candidate( + task: str, + y: npt.NDArray[Any], + predictions: list[npt.NDArray[Any]], +) -> None: + selection = greedy_weighted_selection( + predictions, + y, + task, + max_iterations=10, + ) + + best_individual = max( + score_oof_predictions(values, y, task) for values in predictions + ) + assert selection.score >= best_individual + assert sum(selection.weights) == pytest.approx(1.0) + if task == TABULAR_REGRESSION_TASK: + assert selection.weights == pytest.approx((0.5, 0.5)) + assert selection.score == pytest.approx(0.0) + + +def _rare_class_probabilities() -> tuple[npt.NDArray[Any], npt.NDArray[Any]]: + """Rows the rare class ranks higher on, but never above the majority class.""" + y = np.asarray([0] * 18 + [1] * 2) + probabilities = np.tile(np.asarray([[0.95, 0.05]]), (len(y), 1)) + probabilities[y == 1] = (0.7, 0.3) + return probabilities, y + + +def test_prior_correction_changes_which_class_a_score_credits() -> None: + probabilities, y = _rare_class_probabilities() + + plain = score_oof_predictions( + probabilities, + y, + TABULAR_CLASSIFICATION_TASK, + prior_correct=False, + ) + corrected = score_oof_predictions( + probabilities, + y, + TABULAR_CLASSIFICATION_TASK, + prior_correct=True, + ) + + assert plain == pytest.approx(0.5) + assert corrected == pytest.approx(1.0) + + +def test_greedy_selection_forwards_the_prior_correction_setting() -> None: + informative, y = _rare_class_probabilities() + flat = np.tile(np.asarray([[0.95, 0.05]]), (len(y), 1)) + + corrected = greedy_weighted_selection( + [informative, flat], + y, + TABULAR_CLASSIFICATION_TASK, + max_iterations=3, + ) + plain = greedy_weighted_selection( + [informative, flat], + y, + TABULAR_CLASSIFICATION_TASK, + max_iterations=3, + prior_correct=False, + ) + + assert corrected.score > plain.score + + +def test_plateau_stops_incremental_candidate_training_deterministically( + caplog: pytest.LogCaptureFixture, +) -> None: + X = np.column_stack( + ( + np.arange(40, dtype=np.float32), + np.linspace(-1.0, 1.0, 40, dtype=np.float32), + ) + ) + y = np.arange(40) % 2 + specs = [_spec(f"constant-{index}") for index in range(6)] + + def train() -> tuple[list[str], tuple[float, ...], npt.NDArray[Any]]: + run = OOFEnsembleTrainer( + TABULAR_CLASSIFICATION_TASK, + n_splits=2, + max_iterations=5, + plateau_patience=2, + plateau_tolerance=0.0, + model_factory=_ConstantFactory(), + random_state=13, + ).fit(X, y, groups=np.arange(len(X)), specs=specs) + assert run.stopped_for_plateau + assert not run.stopped_for_budget + return ( + [candidate.spec.name for candidate in run.candidates], + run.ensemble_score_history, + run.ensemble.predict(X), + ) + + with caplog.at_level("INFO", logger="falcon"): + first = train() + second = train() + + assert first[0] == ["constant-0", "constant-1", "constant-2"] + assert first[0] == second[0] + assert first[1] == second[1] + assert np.array_equal(first[2], second[2]) + assert "OOF score plateau" in caplog.text + + +def _classification_schema(row_count: int, feature_count: int) -> DatasetSchema: + return DatasetSchema( + column_names=tuple(f"feature_{index}" for index in range(feature_count)), + column_types=(ColumnTypes.NUMERIC_REGULAR,) * feature_count, + target_name="target", + target_kind="classification", + dimensions=(row_count, feature_count), + ) + + +def test_parallel_classification_graph_matches_fold_bagged_native_predictions() -> None: + X, y = make_classification( + n_samples=96, + n_features=5, + n_informative=4, + n_redundant=0, + random_state=5, + ) + X = X.astype(np.float32) + specs = [ + _spec("logistic-low-c", {"C": 0.2, "max_iter": 200}), + _spec("logistic-high-c", {"C": 5.0, "max_iter": 200}), + ] + run = OOFEnsembleTrainer( + TABULAR_CLASSIFICATION_TASK, + n_splits=3, + max_iterations=10, + plateau_enabled=False, + random_state=23, + ).fit(X, y, groups=np.arange(len(X)), specs=specs) + decoder = LabelDecoder() + decoder.fit( + X, + np.where(y == 0, "negative", "positive"), + _classification_schema(len(X), X.shape[1]), + ) + graph = serialize_to_onnx( + [run.ensemble.serialize(), decoder.serialize()], + task=TABULAR_CLASSIFICATION_TASK, + ) + + onnx.checker.check_model(graph) + assert all( + node.domain in {"", "ai.onnx", "ai.onnx.ml"} for node in graph.graph.node + ) + assert any("fold-1" in node.name for node in graph.graph.node) + session = ort.InferenceSession( + graph.SerializeToString(), + providers=["CPUExecutionProvider"], + ) + runtime_outputs = session.run(None, {session.get_inputs()[0].name: X}) + runtime_probabilities = next( + output for output in runtime_outputs if np.asarray(output).ndim == 2 + ) + runtime_labels = next( + output + for output in runtime_outputs + if np.asarray(output).dtype.kind in {"O", "U"} + ) + native_labels = decoder.transform(run.ensemble.predict(X)) + + np.testing.assert_allclose( + runtime_probabilities, + run.ensemble.predict_proba(X), + rtol=1e-5, + atol=1e-6, + ) + np.testing.assert_array_equal(np.asarray(runtime_labels), native_labels) + + +def test_fnnx_round_trip_shares_preprocessing_across_fold_branches( + tmp_path: Path, +) -> None: + X, y = make_classification( + n_samples=72, + n_features=3, + n_informative=2, + n_redundant=0, + random_state=17, + ) + raw_X = X.astype(np.object_) + schema = _classification_schema(len(X), X.shape[1]) + groups = np.arange(len(X)) + encoder = MultiModalEncoder() + encoder.fit(raw_X, y, schema, groups=groups) + encoded_X = encoder.transform(raw_X) + run = OOFEnsembleTrainer( + TABULAR_CLASSIFICATION_TASK, + n_splits=3, + plateau_enabled=False, + random_state=31, + ).fit( + encoded_X, + y, + groups=groups, + specs=[_spec("logistic", {"C": 1.0, "max_iter": 200})], + ) + decoder = LabelDecoder() + decoder.fit( + encoded_X, + np.where(y == 0, "negative", "positive"), + schema, + ) + bundle = FNNXSerializer( + [encoder.serialize(), run.ensemble.serialize(), decoder.serialize()], + task=TABULAR_CLASSIFICATION_TASK, + schema=schema, + ).serialize() + extracted = extract_fnnx_graph(bundle) + + assert_fnnx_conforms(extracted) + fold_classifier_nodes = [ + node + for node in extracted.model.graph.node + if node.op_type == "LinearClassifier" and "fold-" in node.name + ] + assert len(fold_classifier_nodes) == 3 + assert len({tuple(node.input) for node in fold_classifier_nodes}) == 1 + assert fold_classifier_nodes[0].input[0].startswith("falcon-pl-0/") + + artifact_path = tmp_path / "fold-bagged-classifier.fnnx" + artifact_path.write_bytes(bundle) + runtime = Runtime(str(artifact_path)) + native_probabilities = run.ensemble.predict_proba(encoded_X) + native_labels = decoder.transform(run.ensemble.predict(encoded_X)) + + np.testing.assert_allclose( + runtime.predict_proba(raw_X), + native_probabilities, + rtol=1e-5, + atol=1e-6, + ) + np.testing.assert_allclose( + native_probabilities.sum(axis=1), + 1.0, + atol=1e-6, + ) + np.testing.assert_array_equal(runtime.predict(raw_X), native_labels) + + +def test_parallel_regression_graph_applies_fold_and_member_weighted_means() -> None: + X = np.column_stack( + ( + np.arange(60, dtype=np.float32), + np.linspace(-1.0, 1.0, 60, dtype=np.float32), + ) + ) + y = np.zeros(len(X), dtype=np.float32) + specs = [ + _spec("negative", {"constant": -1.0}), + _spec("positive", {"constant": 1.0}), + ] + run = OOFEnsembleTrainer( + TABULAR_REGRESSION_TASK, + n_splits=3, + max_iterations=5, + plateau_enabled=False, + model_factory=_DummyRegressionFactory(), + random_state=29, + ).fit(X, y, groups=np.arange(len(X)), specs=specs) + serialized = run.ensemble.serialize() + graph = serialized.get_model() + + assert len(run.ensemble.members) == 2 + assert run.ensemble.weights == pytest.approx((0.5, 0.5)) + onnx.checker.check_model(graph) + assert all( + node.domain in {"", "ai.onnx", "ai.onnx.ml"} for node in graph.graph.node + ) + assert any("member-1" in node.name for node in graph.graph.node) + assert any("average_folds" in node.name for node in graph.graph.node) + session = ort.InferenceSession( + graph.SerializeToString(), + providers=["CPUExecutionProvider"], + ) + runtime_predictions = session.run( + None, + {session.get_inputs()[0].name: X}, + )[0] + + np.testing.assert_allclose( + runtime_predictions, + run.ensemble.predict(X), + rtol=1e-6, + atol=1e-7, + ) + + +@pytest.mark.parametrize( + ("kwargs", "message"), + [ + ({"max_iterations": 0}, "max_iterations"), + ({"plateau_patience": 0}, "plateau_patience"), + ({"plateau_tolerance": -1.0}, "plateau_tolerance"), + ({"n_splits": 1}, "n_splits"), + ], +) +def test_oof_ensemble_trainer_rejects_invalid_settings( + kwargs: dict[str, Any], + message: str, +) -> None: + with pytest.raises(ValueError, match=message): + OOFEnsembleTrainer(TABULAR_REGRESSION_TASK, **kwargs) diff --git a/tests/tabular/test_eval_strategy.py b/tests/tabular/test_eval_strategy.py index 08769a8..1f3cfc3 100644 --- a/tests/tabular/test_eval_strategy.py +++ b/tests/tabular/test_eval_strategy.py @@ -1,117 +1,116 @@ -from falcon import initialize +from __future__ import annotations + +from typing import Any, NoReturn + import numpy as np -from sklearn.model_selection import KFold import pytest +from numpy import typing as npt +from sklearn.model_selection import KFold + +from falcon import Predictor +from falcon.config import PortfolioSource, RunConfig +from falcon.tabular.candidates import EstimatorSpec + class _BrokenKFold(KFold): - def split(self, *args, **kwargs): + def split(self, *args: Any, **kwargs: Any) -> NoReturn: raise ValueError("pytest :: Broken KFold") -def _broken_split(*args, **kwargs): + +def _broken_split(*args: Any, **kwargs: Any) -> NoReturn: raise ValueError("pytest :: Broken split") -def test_auto_eval_strategy(): - m = initialize( - task="tabular_classification", - eval_strategy="auto", - data=(np.random.rand(250, 2), np.random.randint(0, 2, 250).reshape(-1, 1)), - config = 'PlainLearner' +def _config() -> RunConfig: + return RunConfig( + candidate_sources=( + PortfolioSource( + specs=(EstimatorSpec("linear", "linear", {"max_iter": 200}),) + ), + ), + ensemble_enabled=False, + oof_folds=3, ) - m.train() - - s = m.performance_summary(None) - assert 'eval_cv' in s.keys() - assert 'eval' not in s.keys() +def _classification_data( + n_rows: int, +) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.int64]]: + rng = np.random.default_rng(42) + X = rng.normal(size=(n_rows, 2)) + y = (X[:, 0] > 0).astype(np.int64) + return X, y - m = initialize( - task="tabular_classification", +def test_auto_eval_strategy() -> None: + small_X, small_y = _classification_data(250) + small = Predictor( + "tabular_classification", + config=_config(), eval_strategy="auto", - data=(np.random.rand(2500, 2), np.random.randint(0, 2, 2500).reshape(-1, 1)), - config = 'PlainLearner' - ) - - m.train() + ).fit((small_X, small_y)) - s = m.performance_summary(None) + assert "eval_cv" in small._performance_metrics + assert "eval" not in small._performance_metrics - assert 'eval' in s.keys() - assert 'eval_cv' not in s.keys() - -def test_cv_eval_strategy(): - - m = initialize( - task="tabular_classification", - eval_strategy="cv", - data=(np.random.rand(100, 2), np.random.randint(0, 2, 100).reshape(-1, 1)), - config = 'PlainLearner' - ) - - m.train() - - s = m.performance_summary(None) - - assert 'eval_cv' in s.keys() - assert 'eval' not in s.keys() - -def test_holdout_eval_strategy(): - - m = initialize( - task="tabular_classification", - eval_strategy="holdout", - data=(np.random.rand(100, 2), np.random.randint(0, 2, 100).reshape(-1, 1)), - config = 'PlainLearner' - ) + large_X, large_y = _classification_data(2_500) + large = Predictor( + "tabular_classification", + config=_config(), + eval_strategy="auto", + ).fit((large_X, large_y)) - m.train() + assert "eval" in large._performance_metrics + assert "eval_cv" not in large._performance_metrics - s = m.performance_summary(None) - assert 'eval_cv' not in s.keys() - assert 'eval' in s.keys() +@pytest.mark.parametrize( + ("strategy", "expected_key"), + [("cv", "eval_cv"), ("holdout", "eval")], +) +def test_named_eval_strategy(strategy: str, expected_key: str) -> None: + X, y = _classification_data(100) + predictor = Predictor( + "tabular_classification", + config=_config(), + eval_strategy=strategy, + ).fit((X, y)) -def test_custom_cv_eval_strategy(): + assert expected_key in predictor._performance_metrics - cv = _BrokenKFold(n_splits=5, shuffle=True, random_state=42) - m = initialize( - task="tabular_classification", - eval_strategy=cv, - data=(np.random.rand(100, 2), np.random.randint(0, 2, 100).reshape(-1, 1)), - config = 'PlainLearner' +def test_custom_cv_eval_strategy() -> None: + X, y = _classification_data(100) + predictor = Predictor( + "tabular_classification", + config=_config(), + eval_strategy=_BrokenKFold(n_splits=5, shuffle=True, random_state=42), ) - with pytest.raises(ValueError, match="pytest :: Broken KFold"): - m.train() + predictor.fit((X, y)) + -def test_custom_holdout_eval_strategy(): - m = initialize( - task="tabular_classification", +def test_custom_holdout_eval_strategy() -> None: + X, y = _classification_data(100) + predictor = Predictor( + "tabular_classification", + config=_config(), eval_strategy=_broken_split, - data=(np.random.rand(100, 2), np.random.randint(0, 2, 100).reshape(-1, 1)), - config = 'PlainLearner' ) with pytest.raises(ValueError, match="pytest :: Broken split"): - m.train() - -def test_no_eval_strategy(): - m = initialize( - task="tabular_classification", - eval_strategy=None, - data=(np.random.rand(100, 2), np.random.randint(0, 2, 100).reshape(-1, 1)), - config = 'PlainLearner' - ) + predictor.fit((X, y)) - m.train() - s = m.performance_summary(None) +def test_no_eval_strategy() -> None: + X, y = _classification_data(100) + predictor = Predictor( + "tabular_classification", + config=_config(), + eval_strategy=None, + ).fit((X, y)) - assert 'eval_cv' not in s.keys() - assert 'eval' not in s.keys() - assert 'train' in s.keys() - assert len(s.keys()) == 1 \ No newline at end of file + assert set(predictor._performance_metrics) == {"train"} + assert predictor.predict(X).shape == (len(X),) + assert predictor.save() diff --git a/tests/tabular/test_evaluation.py b/tests/tabular/test_evaluation.py new file mode 100644 index 0000000..4352e80 --- /dev/null +++ b/tests/tabular/test_evaluation.py @@ -0,0 +1,34 @@ +import numpy as np +import pytest + +from falcon.tabular.evaluation import classification_metrics, regression_metrics + + +def test_classification_metrics_are_structured_and_silent( + capsys: pytest.CaptureFixture[str], +) -> None: + labels = np.asarray(["a", "a", "b", "b", "c", "c"]) + predictions = np.asarray(["a", "b", "b", "b", "c", "a"]) + + result = classification_metrics(labels, predictions) + + assert result["N_SAMPLES"] == 6 + assert result["N_CLASSES"] == 3 + assert 0.0 <= result["BACC"] <= 1.0 + assert 0.0 <= result["SC_SCORE"] <= 1.0 + assert capsys.readouterr().out == "" + + +def test_regression_metrics_are_structured_and_silent( + capsys: pytest.CaptureFixture[str], +) -> None: + targets = np.asarray([1.0, 2.0, 3.0, 4.0]) + predictions = np.asarray([1.0, 2.5, 2.5, 4.0]) + + result = regression_metrics(targets, predictions) + + assert result["N_SAMPLES"] == 4 + assert result["MSE"] >= 0.0 + assert result["RMSE"] >= 0.0 + assert result["MAE"] >= 0.0 + assert capsys.readouterr().out == "" diff --git a/tests/tabular/test_group_splitting.py b/tests/tabular/test_group_splitting.py new file mode 100644 index 0000000..26e9235 --- /dev/null +++ b/tests/tabular/test_group_splitting.py @@ -0,0 +1,234 @@ +from __future__ import annotations + +from collections.abc import Iterator +from typing import Any + +import numpy as np +import pandas as pd +import pytest +from numpy import typing as npt +from sklearn.model_selection import BaseCrossValidator + +from falcon import Predictor +from falcon.config import PortfolioSource, RunConfig +from falcon.sklapi import FalconClassifier +from falcon.tabular.candidates import EstimatorSpec +from falcon.tabular.splitting import ( + callable_holdout_indices, + cross_validation_indices, + holdout_indices, + resolve_groups, +) + + +def _assert_groups_do_not_straddle( + groups: npt.NDArray[np.int64], + train_indices: npt.NDArray[np.int64], + eval_indices: npt.NDArray[np.int64], +) -> None: + assert set(groups[train_indices]).isdisjoint(groups[eval_indices]) + + +def _duplicate_classification_data() -> tuple[ + npt.NDArray[np.object_], npt.NDArray[np.int64] +]: + group_ids = np.repeat(np.arange(12), 2) + X = np.column_stack((group_ids, group_ids % 3)).astype(np.object_) + y = (group_ids % 2).astype(np.int64) + return X, y + + +def _single_linear_config() -> RunConfig: + return RunConfig( + candidate_sources=( + PortfolioSource( + specs=(EstimatorSpec("linear", "linear", {"max_iter": 200}),) + ), + ), + ensemble_enabled=False, + ) + + +def test_duplicate_rows_never_straddle_default_holdout_or_cv() -> None: + X, y = _duplicate_classification_data() + groups = resolve_groups(X, ("group", "category")) + + train_indices, eval_indices = holdout_indices(X, y, task="tabular_classification") + _assert_groups_do_not_straddle(groups, train_indices, eval_indices) + + for train_indices, eval_indices in cross_validation_indices( + X, y, task="tabular_classification" + ): + _assert_groups_do_not_straddle(groups, train_indices, eval_indices) + + +def test_named_groups_override_automatic_groups_and_remain_features() -> None: + frame = pd.DataFrame( + { + "account": np.repeat([f"account-{index}" for index in range(8)], 4), + "value": np.arange(32, dtype=np.float64), + "target": np.tile([0, 0, 1, 1], 8), + } + ) + predictor = Predictor( + "tabular_classification", + config=_single_linear_config(), + eval_strategy="holdout", + ).fit(frame, group_by="account") + + assert predictor._training_data is not None + assert predictor._fit_indices is not None + assert predictor._eval_indices is not None + assert predictor._training_data.schema.column_names == ("account", "value") + groups = predictor._training_data.groups + _assert_groups_do_not_straddle( + groups, + predictor._fit_indices, + predictor._eval_indices, + ) + + +def test_sklearn_fit_accepts_named_groups() -> None: + frame = pd.DataFrame( + { + "account": np.repeat([f"account-{index}" for index in range(8)], 4), + "value": np.arange(32, dtype=np.float64), + } + ) + target = np.tile([0, 0, 1, 1], 8) + estimator = FalconClassifier( + preset=_single_linear_config(), + eval_strategy="holdout", + ) + + estimator.fit(frame, target, group_by="account") + + predictor = estimator.predictor_ + assert predictor._training_data is not None + assert predictor._fit_indices is not None + assert predictor._eval_indices is not None + groups = predictor._training_data.groups + assert set(groups[predictor._fit_indices]).isdisjoint( + groups[predictor._eval_indices] + ) + + +def test_explicit_groups_are_respected_after_ingestion_drops_rows() -> None: + frame = pd.DataFrame( + { + "value": np.arange(13, dtype=np.float64), + "target": [*np.tile([0, 1], 6), np.nan], + } + ) + explicit_groups = np.asarray([*np.repeat(np.arange(6), 2), 99]) + predictor = Predictor( + "tabular_classification", + config=_single_linear_config(), + eval_strategy="holdout", + ).fit(frame, group_by=explicit_groups) + + assert predictor._training_data is not None + assert predictor._fit_indices is not None + assert predictor._eval_indices is not None + assert predictor._training_data.schema.column_names == ("value",) + groups = predictor._training_data.groups + assert groups.size == 12 + assert len(np.unique(groups)) == 6 + _assert_groups_do_not_straddle( + groups, + predictor._fit_indices, + predictor._eval_indices, + ) + + +def test_classification_cv_is_stratified_by_group() -> None: + X, y = _duplicate_classification_data() + groups = resolve_groups(X, ("group", "category"), group_by="group") + + for train_indices, eval_indices in cross_validation_indices( + X, + y, + task="tabular_classification", + groups=groups, + n_splits=4, + ): + _assert_groups_do_not_straddle(groups, train_indices, eval_indices) + assert set(y[train_indices]) == {0, 1} + assert set(y[eval_indices]) == {0, 1} + + +class _CapturingGroupCV(BaseCrossValidator): + def __init__(self) -> None: + self.groups_seen: npt.NDArray[Any] | None = None + + def get_n_splits( + self, + X: npt.NDArray[Any] | None = None, + y: npt.NDArray[Any] | None = None, + groups: npt.NDArray[Any] | None = None, + ) -> int: + return 2 + + def split( + self, + X: npt.NDArray[Any], + y: npt.NDArray[Any] | None = None, + groups: npt.NDArray[Any] | None = None, + ) -> Iterator[tuple[npt.NDArray[np.int64], npt.NDArray[np.int64]]]: + assert groups is not None + self.groups_seen = groups.copy() + unique_groups = np.unique(groups) + for eval_groups in np.array_split(unique_groups, 2): + eval_mask = np.isin(groups, eval_groups) + yield np.flatnonzero(~eval_mask), np.flatnonzero(eval_mask) + + +def test_custom_cross_validator_receives_groups() -> None: + X, y = _duplicate_classification_data() + groups = resolve_groups(X, ("group", "category")) + cv = _CapturingGroupCV() + + splits = cross_validation_indices( + X, y, task="tabular_classification", groups=groups, cv=cv + ) + + assert len(splits) == 2 + assert cv.groups_seen is not None + np.testing.assert_array_equal(cv.groups_seen, groups) + + +def test_callable_splitter_receives_groups_and_is_validated() -> None: + X, y = _duplicate_classification_data() + groups = resolve_groups(X, ("group", "category")) + captured_groups: npt.NDArray[Any] | None = None + + def split( + split_X: npt.NDArray[Any], + split_y: npt.NDArray[Any], + split_groups: npt.NDArray[Any], + ) -> tuple[npt.NDArray[np.int64], npt.NDArray[np.int64]]: + nonlocal captured_groups + captured_groups = split_groups.copy() + return holdout_indices( + split_X, + split_y, + task="tabular_classification", + groups=split_groups, + ) + + train_indices, eval_indices = callable_holdout_indices(split, X, y, groups) + + assert captured_groups is not None + np.testing.assert_array_equal(captured_groups, groups) + _assert_groups_do_not_straddle(groups, train_indices, eval_indices) + + def invalid_split( + split_X: npt.NDArray[Any], + split_y: npt.NDArray[Any], + split_groups: npt.NDArray[Any], + ) -> tuple[npt.NDArray[np.int64], npt.NDArray[np.int64]]: + del split_X, split_y, split_groups + return np.arange(0, len(X), 2), np.arange(1, len(X), 2) + + with pytest.raises(ValueError, match="places group .* on both sides"): + callable_holdout_indices(invalid_split, X, y, groups) diff --git a/tests/tabular/test_inference.py b/tests/tabular/test_inference.py index 072cd3d..004140c 100644 --- a/tests/tabular/test_inference.py +++ b/tests/tabular/test_inference.py @@ -1,140 +1,53 @@ -from falcon import initialize -from falcon.utils import run_onnx -import numpy as np -from sklearn.metrics import r2_score -import random -from falcon.task_configurations import get_task_configuration - -def eval_saved_model(manager, is_regr=False, format="onnx", prefix = ''): - X = manager._data[0] - y = manager._data[1] - pred = manager.predict(X) - manager.save_model(format=format, filename=f"{prefix}test_model") - if format == "onnx": - pred_ = run_onnx(f"{prefix}test_model.{format}", X) - if len(pred_) > 1: - print( - "Onnx model returned multiple predictions. Only the first one will be used for testing." - ) - pred_ = pred_[0].squeeze() - print(pred_.shape) - else: - ValueError("Non onnx format was selected. Currently only onnx is supported.") - if not is_regr: - eq_ = np.equal(pred, pred_) - print(eq_) - return False not in eq_ - else: - ac = np.isclose(pred, pred_) - print(ac, np.max(pred - pred_)) - assert False not in ac - ac = len(ac[ac == False]) - print(ac, len(pred)) - ac = ac / len(pred) < 0.1 - me1 = r2_score(pred, y) - me2 = r2_score(pred_, y) - mset = 0.001 - msec = np.abs(me1 - me2) < mset - print(me1, me2, np.abs(me1 - me2), msec) - print(ac) - return ac, msec, (pred, pred_) +from __future__ import annotations +from pathlib import Path -def inference_classification(config, config_name): - random.seed(42) - np.random.seed(42) - manager = initialize( - task="tabular_classification", data="tests/extra_files/iris.csv", **config - ) - manager.train(pre_eval=False) - print('model ', manager._pipeline._pipeline[1].model) - print('task ', manager._pipeline._pipeline[1].task) - assert eval_saved_model(manager=manager, is_regr=False, format="onnx", prefix = f"clf_{config_name}_") - - -def inference_regression(config, config_name): - manager = initialize( - task="tabular_regression", - data="tests/extra_files/prices.csv", - features="SqFt,Bedrooms,Bathrooms,Offers,Brick,Neighborhood".split(","), - target="Price", +import numpy as np +import pytest + +from falcon import Predictor +from falcon.config import PortfolioSource, RunConfig +from falcon.runtime import Runtime +from falcon.tabular.candidates import EstimatorSpec + + +@pytest.mark.parametrize( + "task", + ["tabular_classification", "tabular_regression"], +) +def test_predictor_inference_round_trip(task: str, tmp_path: Path) -> None: + rng = np.random.default_rng(42) + X = rng.normal(size=(80, 3)) + if task == "tabular_classification": + y = np.where(X[:, 0] + X[:, 1] > 0, "positive", "negative") + parameters: dict[str, object] = {"max_iter": 200} + else: + y = 2 * X[:, 0] - X[:, 1] + 0.5 * X[:, 2] + parameters = {"alpha": 1.0} + config = RunConfig( + candidate_sources=( + PortfolioSource(specs=(EstimatorSpec("linear", "linear", parameters),)), + ), + ensemble_enabled=False, + eval_strategy=None, ) - manager.train(pre_eval=False, **config) - ac, msec, data = eval_saved_model(manager=manager, is_regr=True, format="onnx", prefix = f"regr_{config_name}_") - assert ac - assert msec - -def test_inference_regr_superlearner_mini(): - config = get_task_configuration(task = 'tabular_regression', configuration_name='SuperLearner.mini') - config['extra_pipeline_options']['learner_kwargs']['cv'] = 2 - inference_regression(config=config, config_name='SuperLearner.mini') - -def test_inference_regr_superlearner_default(): - config = get_task_configuration(task = 'tabular_regression', configuration_name='SuperLearner') - inference_regression(config=config, config_name='SuperLearner') - -def test_inference_regr_superlearner_mid(): - config = get_task_configuration(task = 'tabular_regression', configuration_name='SuperLearner.mid') - config['extra_pipeline_options']['learner_kwargs']['cv'] = 2 - inference_regression(config=config, config_name='SuperLearner.mid') - -def test_inference_regr_superlearner_large(): - config = get_task_configuration(task = 'tabular_regression', configuration_name='SuperLearner.large') - config['extra_pipeline_options']['learner_kwargs']['cv'] = 2 - inference_regression(config=config, config_name='SuperLearner.large') - -def test_inference_regr_superlearner_xlarge(): - config = get_task_configuration(task = 'tabular_regression', configuration_name='SuperLearner.xlarge') - config['extra_pipeline_options']['learner_kwargs']['cv'] = 2 - inference_regression(config=config, config_name='SuperLearner.xlarge') - -def test_inference_clf_superlearner_mini(): - config = get_task_configuration(task = 'tabular_classification', configuration_name='SuperLearner.mini') - config['extra_pipeline_options']['learner_kwargs']['cv'] = 2 - inference_classification(config=config, config_name='SuperLearner.mini') - -def test_inference_clf_superlearner_mid(): - config = get_task_configuration(task = 'tabular_classification', configuration_name='SuperLearner.mid') - config['extra_pipeline_options']['learner_kwargs']['cv'] = 2 - inference_classification(config=config, config_name='SuperLearner.mid') - -def test_inference_clf_superlearner_large(): - config = get_task_configuration(task = 'tabular_classification', configuration_name='SuperLearner.large') - config['extra_pipeline_options']['learner_kwargs']['cv'] = 2 - inference_classification(config=config, config_name='SuperLearner.large') - -def test_inference_clf_superlearner_xlarge(): - config = get_task_configuration(task = 'tabular_classification', configuration_name='SuperLearner.xlarge') - config['extra_pipeline_options']['learner_kwargs']['cv'] = 2 - inference_classification(config=config, config_name='SuperLearner.xlarge') - -def test_inference_clf_superlearner_default(): - config = get_task_configuration(task = 'tabular_classification', configuration_name='SuperLearner') - inference_classification(config=config, config_name='SuperLearner') - -def test_inference_clf_optuna_hgbt(): - config = get_task_configuration(task = 'tabular_classification', configuration_name='OptunaLearner.hgbt') - config['extra_pipeline_options']['learner_kwargs']['n_trials'] = 2 - inference_classification(config=config, config_name='OptunaLearnerHGBT') - -def test_inference_regr_optuna_hgbt(): - config = get_task_configuration(task = 'tabular_regression', configuration_name='OptunaLearner.hgbt') - config['extra_pipeline_options']['learner_kwargs']['n_trials'] = 2 - inference_regression(config=config, config_name='OptunaLearnerHGBT') - -def test_inference_clf_plain(): - config = get_task_configuration(task = 'tabular_classification', configuration_name='PlainLearner') - inference_classification(config=config, config_name='PlainLearner') - -def test_inference_regr_plain(): - config = get_task_configuration(task = 'tabular_regression', configuration_name='PlainLearner') - inference_regression(config=config, config_name='PlainLearner') - -def test_inference_clf_plain_hgbt(): - config = get_task_configuration(task = 'tabular_classification', configuration_name='PlainLearner.hgbt') - inference_classification(config=config, config_name='PlainLearnerHGBT') - -def test_inference_regr_plain_hgbt(): - config = get_task_configuration(task = 'tabular_regression', configuration_name='PlainLearner.hgbt') - inference_regression(config=config, config_name='PlainLearnerHGBT') - + predictor = Predictor(task, config=config).fit((X, y)) + artifact_path = tmp_path / f"{task}.fnnx" + predictor.save(artifact_path) + runtime = Runtime(str(artifact_path)) + + if task == "tabular_classification": + np.testing.assert_array_equal(runtime.predict(X), predictor.predict(X)) + np.testing.assert_allclose( + runtime.predict_proba(X), + predictor.predict_proba(X), + rtol=1e-5, + atol=1e-6, + ) + else: + np.testing.assert_allclose( + runtime.predict(X), + predictor.predict(X), + rtol=1e-5, + atol=1e-5, + ) diff --git a/tests/tabular/test_portfolio_ordering.py b/tests/tabular/test_portfolio_ordering.py new file mode 100644 index 0000000..fcd58cb --- /dev/null +++ b/tests/tabular/test_portfolio_ordering.py @@ -0,0 +1,187 @@ +from __future__ import annotations + +import numpy as np +import pytest + +from falcon import Predictor +from falcon.config import PortfolioSource, RunConfig +from falcon.constants import TABULAR_CLASSIFICATION_TASK, TABULAR_REGRESSION_TASK +from falcon.tabular.candidates import EstimatorSpec +from falcon.tabular.portfolio_ordering import ( + DatasetMetaFeatures, + extract_dataset_meta_features, + reorder_portfolio, +) +from falcon.types import ColumnTypes, DatasetSchema, TargetKind + + +def _schema( + column_types: tuple[ColumnTypes, ...], + *, + n_rows: int, + target_kind: TargetKind = "classification", +) -> DatasetSchema: + return DatasetSchema( + column_names=tuple(f"feature_{index}" for index in range(len(column_types))), + column_types=column_types, + target_name="target", + target_kind=target_kind, + dimensions=(n_rows, len(column_types)), + ) + + +def _known_specs() -> tuple[EstimatorSpec, ...]: + names = ( + "xgboost_default", + "catboost_default", + "catboost_zeroshot_r177", + "lightgbm_zeroshot_large", + "lightgbm_default", + "extra_trees_zeroshot", + "random_forest_zeroshot", + "linear_default", + ) + return tuple( + EstimatorSpec(name, "linear", {"alpha": float(index + 1)}) + for index, name in enumerate(names) + ) + + +def test_extract_dataset_meta_features_uses_schema_and_class_distribution() -> None: + schema = _schema( + ( + ColumnTypes.NUMERIC_REGULAR, + ColumnTypes.CAT_LOW_CARD, + ColumnTypes.CAT_HIGH_CARD, + ColumnTypes.TEXT_UTF8, + ColumnTypes.DATE_YMD_ISO8601, + ColumnTypes.NUMERIC_REGULAR, + ), + n_rows=8, + ) + + meta_features = extract_dataset_meta_features( + np.asarray([0, 0, 0, 0, 0, 0, 1, 1]), + schema, + ) + + assert meta_features == DatasetMetaFeatures( + n_rows=8, + n_features=6, + class_balance=0.25, + categorical_fraction=2 / 6, + text_fraction=1 / 6, + ) + + regression_schema = _schema( + (ColumnTypes.NUMERIC_REGULAR,), + n_rows=8, + target_kind="regression", + ) + regression = extract_dataset_meta_features(np.arange(8), regression_schema) + assert regression.class_balance is None + + +def test_reordering_varies_by_dataset_and_is_seed_deterministic() -> None: + specs = _known_specs() + small_wide = DatasetMetaFeatures(1_100, 125, None, 0.0, 0.0) + large_narrow = DatasetMetaFeatures(35_000, 18, None, 0.15, 0.0) + + small_order = reorder_portfolio( + specs, + small_wide, + TABULAR_REGRESSION_TASK, + random_state=17, + ) + repeated_small_order = reorder_portfolio( + specs, + small_wide, + TABULAR_REGRESSION_TASK, + random_state=17, + ) + large_order = reorder_portfolio( + specs, + large_narrow, + TABULAR_REGRESSION_TASK, + random_state=17, + ) + + assert small_order == repeated_small_order + assert small_order != large_order + assert small_order[0].name == "catboost_default" + assert large_order[0].name == "catboost_zeroshot_r177" + + +def test_reordering_keeps_unknown_slots_and_falls_back_outside_corpus() -> None: + known = _known_specs() + unknown = EstimatorSpec("extension_candidate", "linear") + specs = (known[0], unknown, *known[1:]) + in_range = DatasetMetaFeatures(1_100, 125, None, 0.0, 0.0) + + reordered = reorder_portfolio( + specs, + in_range, + TABULAR_REGRESSION_TASK, + random_state=42, + ) + + assert reordered[1] is unknown + out_of_range = DatasetMetaFeatures(50, 125, None, 0.0, 0.0) + assert ( + reorder_portfolio( + specs, + out_of_range, + TABULAR_REGRESSION_TASK, + random_state=42, + ) + == specs + ) + + +def test_predictor_orders_before_portfolio_limit_and_respects_toggle() -> None: + specs = _known_specs() + + def selected_candidate( + n_rows: int, + n_features: int, + *, + enabled: bool, + ) -> str: + values = np.arange(n_rows * n_features, dtype=np.float64).reshape( + n_rows, n_features + ) + target = values[:, 0] * 0.5 - values[:, -1] + config = RunConfig( + candidate_sources=(PortfolioSource(specs=specs, max_candidates=1),), + dataset_aware_ordering=enabled, + ensemble_enabled=False, + eval_strategy=None, + random_state=23, + ) + predictor = Predictor( + TABULAR_REGRESSION_TASK, + config=config, + ).fit((values, target)) + return str(predictor.leaderboard().iloc[0]["candidate"]) + + small_wide = selected_candidate(300, 120, enabled=True) + large_narrow = selected_candidate(30_000, 12, enabled=True) + + assert small_wide == "catboost_default" + assert large_narrow == "catboost_zeroshot_r177" + assert selected_candidate(300, 120, enabled=True) == small_wide + assert selected_candidate(50, 120, enabled=True) == specs[0].name + assert selected_candidate(300, 120, enabled=False) == specs[0].name + + +def test_classification_profile_requires_class_balance() -> None: + specs = _known_specs() + incomplete = DatasetMetaFeatures(1_000, 100, None, 0.0, 0.0) + + with pytest.raises(ValueError, match="class_balance"): + reorder_portfolio( + specs, + incomplete, + TABULAR_CLASSIFICATION_TASK, + random_state=42, + ) diff --git a/tests/tabular/test_public_api.py b/tests/tabular/test_public_api.py new file mode 100644 index 0000000..e840a0f --- /dev/null +++ b/tests/tabular/test_public_api.py @@ -0,0 +1,12 @@ +from importlib import import_module + +import pytest + +from falcon import tabular + + +def test_time_series_adapter_is_removed() -> None: + assert not hasattr(tabular, "TSAdapter") + + with pytest.raises(ModuleNotFoundError): + import_module("falcon.tabular.adapters.ts.adapter") diff --git a/tests/test_benchmarks.py b/tests/test_benchmarks.py new file mode 100644 index 0000000..719e3cb --- /dev/null +++ b/tests/test_benchmarks.py @@ -0,0 +1,384 @@ +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any, cast + +import numpy as np +import pandas as pd +import pytest + +from benchmarks import run as benchmark_runner +from falcon.config import RunConfig + + +class _FakePredictor: + initialization: dict[str, object] = {} + ordering_modes: list[bool] = [] + + def __init__( + self, + task: str, + preset: str, + config: RunConfig, + time_limit: float | None, + random_state: int, + eval_strategy: None, + ) -> None: + self.initialization = { + "task": task, + "preset": preset, + "dataset_aware_ordering": config.dataset_aware_ordering, + "time_limit": time_limit, + "random_state": random_state, + "eval_strategy": eval_strategy, + } + type(self).initialization = self.initialization + type(self).ordering_modes.append(config.dataset_aware_ordering) + + def fit( + self, + data: tuple[pd.DataFrame, pd.Series[Any]], + ) -> _FakePredictor: + assert len(data[0]) == len(data[1]) + return self + + def predict(self, data: pd.DataFrame) -> np.ndarray[Any, np.dtype[np.str_]]: + return np.where(data["feature"].to_numpy() % 2 == 0, "even", "odd") + + def save(self, path: str | Path) -> bytes: + artifact = b"fake-fnnx-artifact" + Path(path).write_bytes(artifact) + return artifact + + +class _FakeRuntime: + prediction_calls = 0 + + def __init__(self, model_path: str) -> None: + assert Path(model_path).read_bytes() == b"fake-fnnx-artifact" + + def predict(self, data: pd.DataFrame) -> np.ndarray[Any, np.dtype[np.str_]]: + type(self).prediction_calls += 1 + return np.where(data["feature"].to_numpy() % 2 == 0, "even", "odd") + + +class _FakeAutoGluonPredictor: + initialization: dict[str, object] = {} + fit_options: dict[str, object] = {} + + def __init__(self, **kwargs: object) -> None: + type(self).initialization = kwargs + + def fit(self, **kwargs: object) -> _FakeAutoGluonPredictor: + type(self).fit_options = kwargs + return self + + def predict(self, data: pd.DataFrame) -> np.ndarray[Any, np.dtype[np.str_]]: + return np.where(data["feature"].to_numpy() % 2 == 0, "even", "odd") + + +def test_fixed_suite_has_ten_openml_datasets_for_both_tasks() -> None: + assert len(benchmark_runner.DATASETS) == 10 + assert len({dataset.openml_id for dataset in benchmark_runner.DATASETS}) == 10 + assert {dataset.task for dataset in benchmark_runner.DATASETS} == { + "tabular_classification", + "tabular_regression", + } + + +def test_benchmark_dataset_records_falcon_and_optional_autogluon_metrics( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + features = pd.DataFrame({"feature": np.arange(40)}) + targets = pd.Series( + np.where(features["feature"] % 2 == 0, "even", "odd"), + name="target", + ) + dataset = benchmark_runner.BenchmarkDataset( + "parity", + 123, + "tabular_classification", + ) + monkeypatch.setattr( + benchmark_runner, + "load_openml_dataset", + lambda requested: (features, targets), + ) + monkeypatch.setattr(benchmark_runner, "Predictor", _FakePredictor) + monkeypatch.setattr(benchmark_runner, "Runtime", _FakeRuntime) + _FakeRuntime.prediction_calls = 0 + _FakePredictor.ordering_modes = [] + + result = benchmark_runner.benchmark_dataset( + dataset, + workspace=tmp_path, + preset="fast", + time_limit=7.5, + random_state=17, + test_size=0.25, + inference_repeats=3, + autogluon_predictor_type=_FakeAutoGluonPredictor, + dataset_aware_ordering=True, + ) + + assert result["status"] == "ok" + assert result["metric"] == "balanced_accuracy" + assert result["score"] == pytest.approx(1.0) + assert result["autogluon_score"] == pytest.approx(1.0) + assert result["artifact_size_bytes"] == len(b"fake-fnnx-artifact") + assert float(cast(float, result["wall_time_seconds"])) >= 0.0 + assert float(cast(float, result["artifact_inference_latency_seconds"])) >= 0.0 + assert result["train_rows"] == 30 + assert result["test_rows"] == 10 + assert _FakeRuntime.prediction_calls == 4 + assert _FakePredictor.initialization == { + "task": "tabular_classification", + "preset": "fast", + "dataset_aware_ordering": True, + "time_limit": 7.5, + "random_state": 17, + "eval_strategy": None, + } + assert result["dataset_aware_ordering"] is True + assert _FakeAutoGluonPredictor.fit_options["presets"] == "medium_quality" + assert _FakeAutoGluonPredictor.fit_options["time_limit"] == 7.5 + + +def test_regression_score_is_rmse() -> None: + dataset = benchmark_runner.BenchmarkDataset( + "regression", + 456, + "tabular_regression", + ) + + metric, score = benchmark_runner.score_predictions( + dataset, + pd.Series([1.0, 2.0, 3.0]), + np.asarray([1.0, 4.0, 3.0]), + ) + + assert metric == "rmse" + assert score == pytest.approx(np.sqrt(4.0 / 3.0)) + + +def test_ordering_improvement_uses_the_task_metric_direction() -> None: + assert benchmark_runner._relative_ordering_improvement( + "tabular_classification", + static_score=0.8, + dataset_aware_score=0.9, + ) == pytest.approx(0.125) + assert benchmark_runner._relative_ordering_improvement( + "tabular_regression", + static_score=2.0, + dataset_aware_score=1.0, + ) == pytest.approx(0.5) + + +def test_ordering_comparison_runs_both_modes_and_applies_quality_gate( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + features = pd.DataFrame({"feature": np.arange(40)}) + targets = pd.Series( + np.where(features["feature"] % 2 == 0, "even", "odd"), + name="target", + ) + dataset = benchmark_runner.BenchmarkDataset( + "ordering", + 321, + "tabular_classification", + ) + monkeypatch.setattr( + benchmark_runner, + "load_openml_dataset", + lambda requested: (features, targets), + ) + monkeypatch.setattr(benchmark_runner, "Predictor", _FakePredictor) + monkeypatch.setattr(benchmark_runner, "Runtime", _FakeRuntime) + _FakePredictor.ordering_modes = [] + + result = benchmark_runner.benchmark_dataset_ordering( + dataset, + workspace=tmp_path, + preset="balanced", + time_limit=None, + random_state=11, + test_size=0.25, + inference_repeats=1, + autogluon_predictor_type=None, + ) + + assert _FakePredictor.ordering_modes == [False, True] + assert result["static_score"] == pytest.approx(1.0) + assert result["dataset_aware_score"] == pytest.approx(1.0) + assert result["ordering_relative_improvement"] == pytest.approx(0.0) + assert benchmark_runner.ordering_quality_gate([result]) + assert not benchmark_runner.ordering_quality_gate( + [ + { + "status": "ok", + "ordering_relative_improvement": -0.01, + } + ] + ) + assert not benchmark_runner.ordering_quality_gate( + [{"status": "failed", "ordering_relative_improvement": 1.0}] + ) + + +def test_runner_persists_successes_and_failures_as_json( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + datasets = ( + benchmark_runner.BenchmarkDataset( + "working", + 1, + "tabular_classification", + ), + benchmark_runner.BenchmarkDataset( + "broken", + 2, + "tabular_regression", + ), + ) + + def fake_benchmark( + dataset: benchmark_runner.BenchmarkDataset, + **kwargs: object, + ) -> benchmark_runner.BenchmarkResult: + assert kwargs["autogluon_predictor_type"] is None + if dataset.name == "broken": + raise RuntimeError("download unavailable") + return { + "status": "ok", + "dataset": dataset.name, + "openml_id": dataset.openml_id, + "task": dataset.task, + "metric": "balanced_accuracy", + "score": 0.75, + "wall_time_seconds": 1.0, + "artifact_size_bytes": 100, + "artifact_inference_latency_seconds": 0.01, + "train_rows": 8, + "test_rows": 2, + } + + monkeypatch.setattr(benchmark_runner, "benchmark_dataset", fake_benchmark) + monkeypatch.setattr( + benchmark_runner, + "load_autogluon_predictor_type", + lambda: None, + ) + output_path = tmp_path / "nested" / "results.json" + + report = benchmark_runner.run_benchmarks( + output_path, + datasets=datasets, + preset="balanced", + time_limit=None, + random_state=42, + test_size=0.2, + inference_repeats=5, + include_autogluon=True, + ) + + assert json.loads(output_path.read_text(encoding="utf-8")) == report + assert report["autogluon_baseline"] == "unavailable" + assert report["dataset_aware_ordering"] is False + assert report["ordering_gate_passed"] is None + results = report["results"] + assert isinstance(results, list) + assert results[0]["status"] == "ok" + assert results[1] == { + "status": "failed", + "dataset": "broken", + "openml_id": 2, + "task": "tabular_regression", + "error": "RuntimeError: download unavailable", + } + + +def test_runner_records_dataset_ordering_comparison_gate( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + datasets = ( + benchmark_runner.BenchmarkDataset( + "first", + 11, + "tabular_classification", + ), + benchmark_runner.BenchmarkDataset( + "second", + 12, + "tabular_regression", + ), + ) + compared: list[str] = [] + + def fake_comparison( + dataset: benchmark_runner.BenchmarkDataset, + **kwargs: object, + ) -> benchmark_runner.BenchmarkResult: + assert kwargs["autogluon_predictor_type"] is None + compared.append(dataset.name) + return { + "status": "ok", + "dataset": dataset.name, + "ordering_relative_improvement": 0.01, + } + + monkeypatch.setattr( + benchmark_runner, + "benchmark_dataset_ordering", + fake_comparison, + ) + output_path = tmp_path / "ordering.json" + + report = benchmark_runner.run_benchmarks( + output_path, + datasets=datasets, + compare_dataset_ordering=True, + ) + + assert compared == ["first", "second"] + assert report["compare_dataset_ordering"] is True + assert report["ordering_gate_passed"] is True + assert json.loads(output_path.read_text(encoding="utf-8")) == report + + +@pytest.mark.parametrize( + ("options", "message"), + [ + ({"test_size": 0.0}, "test_size"), + ({"test_size": 1.0}, "test_size"), + ({"inference_repeats": 0}, "inference_repeats"), + ({"datasets": ()}, "dataset"), + ({"dataset_aware_ordering": 1}, "dataset_aware_ordering"), + ({"compare_dataset_ordering": 1}, "compare_dataset_ordering"), + ], +) +def test_runner_rejects_invalid_settings( + options: dict[str, Any], + message: str, + tmp_path: Path, +) -> None: + arguments: dict[str, Any] = { + "datasets": benchmark_runner.DATASETS[:1], + "preset": "fast", + "time_limit": None, + "random_state": 42, + "test_size": 0.2, + "inference_repeats": 3, + "include_autogluon": False, + } + arguments.update(options) + + with pytest.raises(ValueError, match=message): + benchmark_runner.run_benchmarks( + tmp_path / "results.json", + **arguments, + ) diff --git a/tests/test_calibration.py b/tests/test_calibration.py new file mode 100644 index 0000000..ec41ddd --- /dev/null +++ b/tests/test_calibration.py @@ -0,0 +1,211 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any + +import numpy as np +import pandas as pd +import pytest +from numpy import typing as npt +from sklearn.datasets import make_classification +from sklearn.metrics import log_loss + +from falcon import Predictor +from falcon.config import PortfolioSource, RunConfig +from falcon.runtime import Runtime +from falcon.tabular.candidates import EstimatorSpec +from tests.fnnx_conformance import assert_fnnx_conforms, extract_fnnx_graph + + +def _calibration_config(*, calibrate: bool) -> RunConfig: + return RunConfig( + candidate_sources=( + PortfolioSource( + specs=( + EstimatorSpec( + "linear", + "linear", + {"C": 10_000.0, "max_iter": 500}, + ), + ) + ), + ), + ensemble_enabled=False, + plateau_enabled=False, + oof_folds=5, + eval_strategy="holdout", + calibrate=calibrate, + # Temperature alone preserves labels; a tuned decision rule fitted on + # calibrated versus raw OOF probabilities legitimately would not. + decision_metric=None, + ) + + +def _noisy_classification_frame() -> tuple[pd.DataFrame, list[str]]: + X, y = make_classification( + n_samples=600, + n_features=10, + n_informative=4, + n_redundant=2, + class_sep=0.5, + flip_y=0.2, + random_state=41, + ) + features = [f"feature_{index}" for index in range(X.shape[1])] + frame = pd.DataFrame(X, columns=features) + frame["target"] = np.where(y, "yes", "no") + return frame, features + + +def test_temperature_calibration_preserves_labels_and_round_trips( + tmp_path: Path, +) -> None: + frame, features = _noisy_classification_frame() + uncalibrated = Predictor( + "tabular_classification", + config=_calibration_config(calibrate=False), + random_state=17, + ).fit(frame, features=features, target="target") + calibrated = Predictor( + "tabular_classification", + config=_calibration_config(calibrate=True), + random_state=17, + ).fit(frame, features=features, target="target") + + assert uncalibrated._eval_indices is not None + assert calibrated._eval_indices is not None + np.testing.assert_array_equal( + calibrated._eval_indices, + uncalibrated._eval_indices, + ) + evaluation = frame.iloc[calibrated._eval_indices] + evaluation_X = evaluation[features] + uncalibrated_probabilities = uncalibrated.predict_proba(evaluation_X) + calibrated_probabilities = calibrated.predict_proba(evaluation_X) + calibrated_predictions = calibrated.predict(evaluation_X) + + np.testing.assert_array_equal( + calibrated_predictions, + uncalibrated.predict(evaluation_X), + ) + assert not np.allclose( + calibrated_probabilities, + uncalibrated_probabilities, + rtol=1e-5, + atol=1e-6, + ) + assert calibrated.classes_ is not None + calibrated_loss = log_loss( + evaluation["target"], + calibrated_probabilities, + labels=calibrated.classes_, + ) + uncalibrated_loss = log_loss( + evaluation["target"], + uncalibrated_probabilities, + labels=calibrated.classes_, + ) + assert calibrated_loss <= uncalibrated_loss + 1e-6 + + artifact_path = tmp_path / "calibrated.fnnx" + bundle = calibrated.save(artifact_path) + graph = extract_fnnx_graph(bundle) + assert_fnnx_conforms(graph) + calibration_ops = { + node.op_type + for node in graph.model.graph.node + if "falcon_temperature" in node.name + } + assert calibration_ops == {"Clip", "Div", "Log", "Softmax"} + + runtime = Runtime(str(artifact_path)) + np.testing.assert_array_equal( + runtime.predict(evaluation_X), + calibrated_predictions, + ) + np.testing.assert_allclose( + runtime.predict_proba(evaluation_X), + calibrated_probabilities, + rtol=1e-5, + atol=1e-6, + ) + + +def test_calibration_oof_splits_keep_groups_disjoint( + monkeypatch: pytest.MonkeyPatch, +) -> None: + from falcon.tabular import candidates + + original_out_of_fold_indices = candidates.out_of_fold_indices + captured: list[ + tuple[ + npt.NDArray[np.int64], + list[tuple[npt.NDArray[np.int64], npt.NDArray[np.int64]]], + ] + ] = [] + + def recording_out_of_fold_indices( + X: npt.NDArray[Any], + y: npt.NDArray[Any], + task: str, + groups: npt.ArrayLike | None = None, + *, + n_splits: int = 5, + random_state: int = 42, + ) -> list[tuple[npt.NDArray[np.int64], npt.NDArray[np.int64]]]: + splits = original_out_of_fold_indices( + X, + y, + task, + groups, + n_splits=n_splits, + random_state=random_state, + ) + assert groups is not None + captured.append((np.asarray(groups, dtype=np.int64), splits)) + return splits + + monkeypatch.setattr( + candidates, + "out_of_fold_indices", + recording_out_of_fold_indices, + ) + group_ids = np.repeat(np.arange(12), 3) + frame = pd.DataFrame( + { + "account": [f"account-{group_id}" for group_id in group_ids], + "value": np.arange(len(group_ids), dtype=np.float64), + "target": np.where(group_ids % 2, "yes", "no"), + } + ) + config = RunConfig( + candidate_sources=( + PortfolioSource( + specs=(EstimatorSpec("linear", "linear", {"max_iter": 200}),) + ), + ), + ensemble_enabled=False, + plateau_enabled=False, + oof_folds=3, + eval_strategy=None, + calibrate=True, + ) + + Predictor("tabular_classification", config=config).fit( + frame, + target="target", + group_by="account", + ) + + assert len(captured) == 1 + groups, splits = captured[0] + for train_indices, eval_indices in splits: + assert set(groups[train_indices]).isdisjoint(groups[eval_indices]) + + +def test_regression_rejects_probability_calibration() -> None: + with pytest.raises(ValueError, match="classification"): + Predictor( + "tabular_regression", + config=RunConfig(calibrate=True), + ) diff --git a/tests/test_codegen_c.py b/tests/test_codegen_c.py new file mode 100644 index 0000000..7a2e88d --- /dev/null +++ b/tests/test_codegen_c.py @@ -0,0 +1,583 @@ +from __future__ import annotations + +import shutil +import subprocess +from collections.abc import Callable +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +import numpy as np +import pandas as pd +import pytest +from numpy import typing as npt +from onnx import TensorProto + +from falcon import Predictor +from falcon.codegen import CArtifact, CodegenError, compile_to_c +from falcon.codegen.bundle import read_bundle +from falcon.config import PortfolioSource, RunConfig +from falcon.tabular.candidates import EstimatorSpec +from falcon.tabular.models.gbdt import get_gbdt_model_classes +from falcon.types import ColumnTypes + +pytest.importorskip( + "fnnx.extras.compilers.c", + reason="generating C requires the FNNX ahead-of-time compiler", +) + +_ROWS = 240 +_BATCH = 64 +_STRICT_FLAGS = ("-std=c99", "-Wall", "-Wextra", "-Werror") +_MISSING_ROWS = 24 + + +@dataclass(frozen=True) +class Fixture: + artifact: CArtifact + predictor: Predictor + frame: pd.DataFrame + bundle_path: Path + + +FixtureFactory = Callable[[str, bool], Fixture] + + +def _training_frame(task: str, *, impute: bool) -> pd.DataFrame: + generator = np.random.default_rng(11) + frame = pd.DataFrame( + { + "num_a": generator.normal(size=_ROWS), + "num_b": generator.uniform(-3.0, 3.0, size=_ROWS), + "cat_color": generator.choice(["red", "green", "blue"], size=_ROWS), + "cat_size": generator.choice(["s", "m", "l", "xl"], size=_ROWS), + } + ) + signal = ( + 1.5 * frame["num_a"] + - 0.8 * frame["num_b"] + + np.where(frame["cat_color"] == "red", 1.2, -0.4) + + np.where(frame["cat_size"] == "xl", 0.9, 0.0) + + generator.normal(scale=0.4, size=_ROWS) + ) + if impute: + frame.loc[generator.choice(_ROWS, _MISSING_ROWS, replace=False), "num_a"] = ( + np.nan + ) + frame.loc[ + generator.choice(_ROWS, _MISSING_ROWS, replace=False), "cat_color" + ] = None + frame["target"] = ( + np.where(signal > float(np.median(signal)), "yes", "no") + if task == "tabular_classification" + else signal + ) + return frame + + +def _config(impute: bool) -> RunConfig: + return RunConfig( + candidate_sources=( + PortfolioSource( + specs=( + EstimatorSpec( + "trees", "random_forest", {"n_estimators": 8, "max_depth": 4} + ), + ) + ), + ), + ensemble_enabled=False, + eval_strategy=None, + impute_missing=impute, + random_state=0, + ) + + +def _build(task: str, impute: bool, directory: Path) -> Fixture: + frame = _training_frame(task, impute=impute) + features = ["num_a", "num_b", "cat_color", "cat_size"] + predictor = Predictor(task, config=_config(impute)).fit( + frame, features=features, target="target" + ) + bundle_path = directory / "model.fnnx" + predictor.save(bundle_path) + artifact = compile_to_c( + bundle_path, directory / "c", prefix="demo", batch_size=_BATCH + ) + return Fixture( + artifact=artifact, + predictor=predictor, + frame=frame[features], + bundle_path=bundle_path, + ) + + +@pytest.fixture(scope="module") +def fixture_factory(tmp_path_factory: pytest.TempPathFactory) -> FixtureFactory: + root = tmp_path_factory.mktemp("codegen-c") + cache: dict[tuple[str, bool], Fixture] = {} + + def get(task: str, impute: bool) -> Fixture: + key = (task, impute) + if key not in cache: + directory = root / f"{task}-{'imputed' if impute else 'plain'}" + directory.mkdir() + cache[key] = _build(task, impute, directory) + return cache[key] + + return get + + +def _encode(mapping: Any, values: npt.NDArray[Any]) -> npt.NDArray[np.int64]: + """Reference implementation of the encoder emitted into the helper header.""" + table = {category: index for index, category in enumerate(mapping.categories)} + missing = set(mapping.missing_tokens) + codes = [] + for value in values: + text = None if pd.isna(value) else str(value) + if text is None or text in missing: + codes.append(mapping.missing_code if mapping.imputed else -1) + else: + codes.append(table.get(text, -1)) + return np.asarray(codes, dtype=np.int64).reshape(-1, 1) + + +def _feed(fixture: Fixture, rows: pd.DataFrame) -> dict[str, npt.NDArray[Any]]: + mappings = {item.name: item for item in fixture.artifact.mapping.categoricals} + return { + name: ( + _encode(mappings[name], rows[name].to_numpy()) + if name in mappings + else rows[name].to_numpy().astype(np.float32).reshape(-1, 1) + ) + for name in rows.columns + } + + +def _run(fixture: Fixture, rows: pd.DataFrame) -> dict[str, npt.NDArray[Any]]: + from fnnx.extras.compilers.c import load_compiled + + if shutil.which("cc") is None: + pytest.skip("no C compiler available") + return load_compiled(fixture.artifact.header_path).run(_feed(fixture, rows)) + + +@pytest.mark.parametrize("impute", [True, False]) +def test_regression_artifact_matches_predictor( + fixture_factory: FixtureFactory, impute: bool +) -> None: + fixture = fixture_factory("tabular_regression", impute) + rows = fixture.frame.iloc[:_BATCH] + predicted = _run(fixture, rows)["y_pred"].reshape(-1) + expected = np.asarray(fixture.predictor.predict(rows), dtype=np.float32) + assert np.abs(predicted - expected).max() < 1e-4 + + +@pytest.mark.parametrize("impute", [True, False]) +def test_classification_artifact_matches_predictor( + fixture_factory: FixtureFactory, impute: bool +) -> None: + fixture = fixture_factory("tabular_classification", impute) + rows = fixture.frame.iloc[:_BATCH] + outputs = _run(fixture, rows) + labels = np.asarray(fixture.artifact.mapping.class_labels, dtype=object) + predicted = labels[outputs["y_pred"].reshape(-1)] + assert np.array_equal(predicted, np.asarray(fixture.predictor.predict(rows))) + expected = fixture.predictor.predict_proba(rows) + assert np.abs(outputs["probabilities"] - expected).max() < 1e-5 + + +def _postprocessed_config(task: str, **overrides: Any) -> RunConfig: + settings: dict[str, Any] = { + "candidate_sources": ( + PortfolioSource( + specs=( + EstimatorSpec( + "trees", "random_forest", {"n_estimators": 8, "max_depth": 4} + ), + ) + ), + ), + "ensemble_enabled": False, + "eval_strategy": None, + "impute_missing": False, + "random_state": 0, + "oof_folds": 3, + } + settings.update(overrides) + return RunConfig(**settings) + + +def _build_with(task: str, config: RunConfig, directory: Path) -> Fixture: + frame = _training_frame(task, impute=False) + features = ["num_a", "num_b", "cat_color", "cat_size"] + predictor = Predictor(task, config=config).fit( + frame, features=features, target="target" + ) + bundle_path = directory / "model.fnnx" + predictor.save(bundle_path) + artifact = compile_to_c( + bundle_path, directory / "c", prefix="demo", batch_size=_BATCH + ) + return Fixture(artifact, predictor, frame[features], bundle_path) + + +def test_calibrated_and_tuned_classification_artifact_matches_predictor( + tmp_path: Path, +) -> None: + fixture = _build_with( + "tabular_classification", + _postprocessed_config("tabular_classification", calibrate=True), + tmp_path, + ) + bundle = read_bundle(fixture.bundle_path) + node_names = {node.name for node in bundle.model.graph.node} + assert any("falcon_temperature" in name for name in node_names) + assert any("falcon_decision" in name for name in node_names) + + rows = fixture.frame.iloc[:_BATCH] + outputs = _run(fixture, rows) + labels = np.asarray(fixture.artifact.mapping.class_labels, dtype=object) + predicted = labels[outputs["y_pred"].reshape(-1)] + + assert np.array_equal(predicted, np.asarray(fixture.predictor.predict(rows))) + expected = fixture.predictor.predict_proba(rows) + assert np.abs(outputs["probabilities"] - expected).max() < 1e-5 + + +def test_conformal_regression_artifact_matches_predictor(tmp_path: Path) -> None: + fixture = _build_with( + "tabular_regression", + _postprocessed_config("tabular_regression", conformal_alpha=0.2), + tmp_path, + ) + bundle = read_bundle(fixture.bundle_path) + assert any("falcon_conformal" in node.name for node in bundle.model.graph.node) + + rows = fixture.frame.iloc[:_BATCH] + outputs = _run(fixture, rows) + predicted = outputs["y_pred"].reshape(-1) + expected = np.asarray(fixture.predictor.predict(rows), dtype=np.float32) + lower = outputs["y_lower"].reshape(-1) + upper = outputs["y_upper"].reshape(-1) + + assert np.abs(predicted - expected).max() < 1e-4 + assert np.all(lower <= upper) + + +def test_artifact_serves_a_partial_batch(fixture_factory: FixtureFactory) -> None: + fixture = fixture_factory("tabular_regression", False) + rows = fixture.frame.iloc[: _BATCH // 4] + predicted = _run(fixture, rows)["y_pred"].reshape(-1) + expected = np.asarray(fixture.predictor.predict(rows), dtype=np.float32) + assert predicted.shape == (len(rows),) + assert np.abs(predicted - expected).max() < 1e-4 + + +def test_generated_graph_holds_no_strings(fixture_factory: FixtureFactory) -> None: + fixture = fixture_factory("tabular_classification", True) + for name in ("num_a", "num_b", "cat_color", "cat_size"): + tensor = next( + item + for item in fixture.artifact.report["entrypoint"]["inputs"] + if item["name"] == name + ) + assert tensor["dtype"] in {"float32", "int64"} + outputs = { + item["name"]: item for item in fixture.artifact.report["entrypoint"]["outputs"] + } + assert outputs["y_pred"]["dtype"] == "int64" + assert outputs["probabilities"]["dtype"] == "float32" + + +def test_exported_bundle_is_left_alone(fixture_factory: FixtureFactory) -> None: + fixture = fixture_factory("tabular_classification", False) + bundle = read_bundle(fixture.bundle_path) + string_inputs = { + value_info.name + for value_info in bundle.model.graph.input + if value_info.type.tensor_type.elem_type == TensorProto.STRING + } + assert string_inputs == {"cat_color", "cat_size"} + manifest_dtypes = { + item["name"]: item["dtype"] for item in bundle.manifest["inputs"] + } + assert manifest_dtypes["cat_color"] == "Array[string]" + + +def test_categorical_mapping_reports_the_vocabulary( + fixture_factory: FixtureFactory, +) -> None: + fixture = fixture_factory("tabular_regression", False) + mappings = {item.name: item for item in fixture.artifact.mapping.categoricals} + assert set(mappings) == {"cat_color", "cat_size"} + assert mappings["cat_color"].categories == ("blue", "green", "red") + assert mappings["cat_size"].categories == ("l", "m", "s", "xl") + assert mappings["cat_color"].column_type == "CAT_LOW_CARD" + assert not mappings["cat_color"].imputed + assert mappings["cat_color"].missing_code == -1 + + +def test_imputed_categorical_maps_missing_to_the_sentinel( + fixture_factory: FixtureFactory, +) -> None: + fixture = fixture_factory("tabular_regression", True) + mapping = next( + item + for item in fixture.artifact.mapping.categoricals + if item.name == "cat_color" + ) + assert mapping.imputed + assert "nan" in mapping.missing_tokens + assert mapping.categories[mapping.missing_code] == "__falcon_missing__" + + +def test_missing_category_predicts_like_the_predictor( + fixture_factory: FixtureFactory, +) -> None: + """A missing value reaches the artifact as the sentinel code the pipeline fills with.""" + fixture = fixture_factory("tabular_regression", True) + rows = fixture.frame.iloc[:_BATCH].copy() + rows.loc[rows.index[0], "cat_color"] = None + predicted = _run(fixture, rows)["y_pred"].reshape(-1) + expected = np.asarray(fixture.predictor.predict(rows), dtype=np.float32) + assert np.abs(predicted - expected).max() < 1e-4 + + +def test_unknown_category_predicts_like_the_predictor( + fixture_factory: FixtureFactory, +) -> None: + fixture = fixture_factory("tabular_regression", False) + rows = fixture.frame.iloc[:_BATCH].copy() + rows.loc[rows.index[0], "cat_color"] = "chartreuse" + assert _feed(fixture, rows)["cat_color"][0, 0] == -1 + predicted = _run(fixture, rows)["y_pred"].reshape(-1) + expected = np.asarray(fixture.predictor.predict(rows), dtype=np.float32) + assert np.abs(predicted - expected).max() < 1e-4 + + +def test_helper_header_declares_the_tables(fixture_factory: FixtureFactory) -> None: + fixture = fixture_factory("tabular_classification", False) + helper = fixture.artifact.helper_path.read_text() + assert "int64_t demo_encode_cat_color(const char* value);" in helper + assert "const char* demo_class_label(int64_t index);" in helper + assert '"chartreuse"' not in helper + for category in ("blue", "green", "red", "l", "m", "s", "xl"): + assert f'"{category}"' in helper + for label in fixture.artifact.mapping.class_labels: + assert f'"{label}"' in helper + + +def test_helper_header_documents_the_signature(fixture_factory: FixtureFactory) -> None: + fixture = fixture_factory("tabular_classification", True) + helper = fixture.artifact.helper_path.read_text() + assert "demo_run() takes its inputs in this order:" in helper + assert "cat_color (int64_t) -- category code from demo_encode_cat_color()" in helper + assert "num_a (float) -- numeric feature, as-is" in helper + assert "__falcon_missing__" in helper + + +@pytest.mark.parametrize("task", ["tabular_classification", "tabular_regression"]) +def test_generated_headers_compile_and_agree_with_the_predictor( + fixture_factory: FixtureFactory, task: str, tmp_path: Path +) -> None: + compiler = shutil.which("cc") + if compiler is None: + pytest.skip("no C compiler available") + fixture = fixture_factory(task, True) + rows = fixture.frame.iloc[:8] + source = tmp_path / "main.c" + source.write_text(_driver_source(task, rows)) + for header in (fixture.artifact.header_path, fixture.artifact.helper_path): + shutil.copy(header, tmp_path / header.name) + + binary = tmp_path / "driver" + subprocess.run( + [compiler, *_STRICT_FLAGS, "-O1", str(source), "-lm", "-o", str(binary)], + check=True, + capture_output=True, + cwd=tmp_path, + ) + completed = subprocess.run( + [str(binary)], check=True, capture_output=True, text=True + ) + produced = [line for line in completed.stdout.splitlines() if line] + + if task == "tabular_classification": + assert produced == list(np.asarray(fixture.predictor.predict(rows))) + else: + expected = np.asarray(fixture.predictor.predict(rows), dtype=np.float32) + assert np.abs(np.asarray(produced, dtype=np.float32) - expected).max() < 1e-4 + + +def _driver_source(task: str, rows: pd.DataFrame) -> str: + """A C program that feeds `rows` through the generated headers and prints the result.""" + literals = ",\n".join( + " {{{num_a}, {num_b}f, {color}, {size}}}".format( + num_a="NAN" if pd.isna(row.num_a) else f"{row.num_a}f", + num_b=row.num_b, + color="NULL" if pd.isna(row.cat_color) else f'"{row.cat_color}"', + size=f'"{row.cat_size}"', + ) + for row in rows.itertuples() + ) + classification = task == "tabular_classification" + report = ( + 'printf("%s\\n", demo_class_label(y_pred[i]));' + if classification + else 'printf("%.7g\\n", (double)y_pred[i]);' + ) + output_type = "int64_t" if classification else "float" + declarations = ( + " float probabilities[DEMO_OUTPUT_PROBABILITIES_COUNT];\n" + if classification + else "" + ) + arguments = ( + "cat_size, probabilities, y_pred" if classification else "cat_size, y_pred" + ) + return f"""#include +#include + +#define DEMO_IMPLEMENTATION +#include "demo.h" + +#define DEMO_FALCON_IMPLEMENTATION +#include "demo_falcon.h" + +struct row {{ float num_a; float num_b; const char* color; const char* size; }}; + +int main(void) +{{ + static const struct row rows[] = {{ +{literals} + }}; + const int32_t batch = (int32_t)(sizeof rows / sizeof rows[0]); + float num_a[DEMO_INPUT_NUM_A_COUNT]; + float num_b[DEMO_INPUT_NUM_B_COUNT]; + int64_t cat_color[DEMO_INPUT_CAT_COLOR_COUNT]; + int64_t cat_size[DEMO_INPUT_CAT_SIZE_COUNT]; + {output_type} y_pred[DEMO_OUTPUT_Y_PRED_COUNT]; +{declarations} + for (int32_t i = 0; i < batch; i++) {{ + num_a[i] = rows[i].num_a; + num_b[i] = rows[i].num_b; + cat_color[i] = demo_encode_cat_color(rows[i].color); + cat_size[i] = demo_encode_cat_size(rows[i].size); + }} + if (demo_run(batch, num_a, num_b, cat_color, {arguments}) != DEMO_OK) {{ + return 1; + }} + for (int32_t i = 0; i < batch; i++) {{ + {report} + }} + return 0; +}} +""" + + +@pytest.mark.parametrize("column", ["text", "date"]) +def test_text_and_date_features_are_rejected(column: str, tmp_path: Path) -> None: + indices = np.arange(_ROWS) + if column == "text": + feature = np.asarray( + [ + f"falcon document {index} contains several useful words about " + f"{'alpha' if index % 2 else 'beta'} and its many properties" + for index in indices + ] + ) + expected_type = ColumnTypes.TEXT_UTF8 + else: + feature = ( + pd.Timestamp("2020-01-01") + pd.to_timedelta(indices, unit="D") + ).strftime("%Y-%m-%d") + expected_type = ColumnTypes.DATE_YMD_ISO8601 + frame = pd.DataFrame( + {"num": indices * 0.5, column: feature, "target": 0.3 * indices} + ) + predictor = Predictor("tabular_regression", config=_config(True)).fit( + frame, features=["num", column], target="target" + ) + assert predictor._training_data is not None + assert predictor._training_data.schema.column_types[1] == expected_type + bundle_path = tmp_path / "model.fnnx" + predictor.save(bundle_path) + + with pytest.raises(CodegenError) as error: + compile_to_c(bundle_path, tmp_path / "c") + assert column in str(error.value) + assert "Text and date features" in str(error.value) + + +def test_missing_model_is_reported(tmp_path: Path) -> None: + with pytest.raises(CodegenError, match="Model not found"): + compile_to_c(tmp_path / "absent.fnnx", tmp_path / "c") + + +def test_batch_size_must_be_positive( + fixture_factory: FixtureFactory, tmp_path: Path +) -> None: + fixture = fixture_factory("tabular_regression", False) + with pytest.raises(CodegenError, match="batch_size"): + compile_to_c(fixture.bundle_path, tmp_path / "c", batch_size=0) + + +def test_prefix_defaults_to_the_task( + fixture_factory: FixtureFactory, tmp_path: Path +) -> None: + fixture = fixture_factory("tabular_regression", False) + artifact = compile_to_c(fixture.bundle_path, tmp_path / "c", batch_size=8) + assert artifact.prefix == "tabular_regression" + assert artifact.entrypoint == "tabular_regression_run" + assert artifact.batch_size == 8 + assert artifact.header_path.name == "tabular_regression.h" + assert artifact.helper_path.name == "tabular_regression_falcon.h" + + +def test_gbdt_ensemble_compiles(tmp_path: Path) -> None: + families = get_gbdt_model_classes("tabular_regression") + missing = {"lightgbm", "xgboost"} - set(families) + if missing: + pytest.skip(f"{', '.join(sorted(missing))} is not installed") + + frame = _training_frame("tabular_regression", impute=False) + config = RunConfig( + candidate_sources=( + PortfolioSource( + specs=( + EstimatorSpec( + "rf", "random_forest", {"n_estimators": 8, "max_depth": 4} + ), + EstimatorSpec( + "xgb", "xgboost", {"n_estimators": 8, "max_depth": 3} + ), + EstimatorSpec( + "lgbm", "lightgbm", {"n_estimators": 8, "num_leaves": 7} + ), + ) + ), + ), + plateau_enabled=False, + ensemble_max_iterations=6, + impute_missing=False, + oof_folds=3, + eval_strategy=None, + random_state=0, + ) + features = ["num_a", "num_b", "cat_color", "cat_size"] + predictor = Predictor("tabular_regression", config=config).fit( + frame, features=features, target="target" + ) + bundle_path = tmp_path / "model.fnnx" + predictor.save(bundle_path) + artifact = compile_to_c( + bundle_path, tmp_path / "c", prefix="ens", batch_size=_BATCH + ) + assert artifact.report["memory"]["static_bytes"] > 0 + + rows = frame[features].iloc[:_BATCH] + fixture = Fixture(artifact, predictor, rows, bundle_path) + predicted = _run(fixture, rows)["y_pred"].reshape(-1) + expected = np.asarray(predictor.predict(rows), dtype=np.float32) + assert np.abs(predicted - expected).max() < 1e-4 diff --git a/tests/test_conformal.py b/tests/test_conformal.py new file mode 100644 index 0000000..abbc67c --- /dev/null +++ b/tests/test_conformal.py @@ -0,0 +1,193 @@ +from __future__ import annotations + +import io +import json +import tarfile +from pathlib import Path +from typing import Any + +import numpy as np +import pandas as pd +import pytest +from numpy import typing as npt +from sklearn.datasets import make_regression + +from falcon import Predictor +from falcon.config import PortfolioSource, RunConfig +from falcon.runtime import Runtime +from falcon.tabular.candidates import EstimatorSpec +from falcon.tabular.conformal import fit_conformal_quantile +from tests.fnnx_conformance import assert_fnnx_conforms, extract_fnnx_graph + + +def _conformal_config(*, alpha: float = 0.1) -> RunConfig: + return RunConfig( + candidate_sources=( + PortfolioSource( + specs=( + EstimatorSpec("ridge", "linear", {"alpha": 1.0}), + EstimatorSpec("ridge-regularized", "linear", {"alpha": 10.0}), + ) + ), + ), + ensemble_enabled=True, + ensemble_max_iterations=5, + plateau_enabled=False, + oof_folds=5, + eval_strategy="holdout", + conformal_alpha=alpha, + ) + + +def _regression_frame() -> tuple[pd.DataFrame, list[str]]: + X, y = make_regression( + n_samples=800, + n_features=8, + n_informative=6, + noise=18.0, + random_state=41, + ) + features = [f"feature_{index}" for index in range(X.shape[1])] + frame = pd.DataFrame(X, columns=features) + frame["target"] = y + return frame, features + + +def _manifest(bundle: bytes) -> dict[str, Any]: + with tarfile.open(fileobj=io.BytesIO(bundle), mode="r:") as archive: + member = archive.extractfile("manifest.json") + assert member is not None + return json.load(member) + + +def test_conformal_interval_round_trips_and_has_expected_coverage( + tmp_path: Path, +) -> None: + frame, features = _regression_frame() + predictor = Predictor( + "tabular_regression", + config=_conformal_config(), + random_state=17, + ).fit(frame, features=features, target="target") + assert predictor._eval_indices is not None + evaluation = frame.iloc[predictor._eval_indices] + evaluation_X = evaluation[features] + + artifact_path = tmp_path / "conformal.fnnx" + bundle = predictor.save(artifact_path) + graph = extract_fnnx_graph(bundle) + assert_fnnx_conforms(graph) + assert [output["name"] for output in _manifest(bundle)["outputs"]] == [ + "y_pred", + "y_lower", + "y_upper", + ] + conformal_ops = { + node.op_type + for node in graph.model.graph.node + if "falcon_conformal" in node.name + } + assert conformal_ops == {"Add", "Sub"} + + runtime = Runtime(str(artifact_path)) + predictions = runtime.predict(evaluation_X) + lower, upper = runtime.predict_interval(evaluation_X) + + np.testing.assert_allclose( + predictions, + predictor.predict(evaluation_X), + rtol=1e-5, + atol=1e-5, + ) + assert np.all(lower <= predictions) + assert np.all(predictions <= upper) + coverage = np.mean( + (evaluation["target"].to_numpy() >= lower) + & (evaluation["target"].to_numpy() <= upper) + ) + assert 0.84 <= coverage <= 0.96 + + +def test_conformal_quantile_uses_finite_sample_correction() -> None: + predictions = np.zeros(4, dtype=np.float32) + targets = np.asarray([1.0, 2.0, 3.0, 4.0], dtype=np.float32) + + assert fit_conformal_quantile(predictions, targets, alpha=0.4) == 3.0 + + +def test_conformal_splits_keep_groups_disjoint( + monkeypatch: pytest.MonkeyPatch, +) -> None: + from falcon.tabular import candidates + + original_out_of_fold_indices = candidates.out_of_fold_indices + captured: list[ + tuple[ + npt.NDArray[np.int64], + list[tuple[npt.NDArray[np.int64], npt.NDArray[np.int64]]], + ] + ] = [] + + def recording_out_of_fold_indices( + X: npt.NDArray[Any], + y: npt.NDArray[Any], + task: str, + groups: npt.ArrayLike | None = None, + *, + n_splits: int = 5, + random_state: int = 42, + ) -> list[tuple[npt.NDArray[np.int64], npt.NDArray[np.int64]]]: + splits = original_out_of_fold_indices( + X, + y, + task, + groups, + n_splits=n_splits, + random_state=random_state, + ) + assert groups is not None + captured.append((np.asarray(groups, dtype=np.int64), splits)) + return splits + + monkeypatch.setattr( + candidates, + "out_of_fold_indices", + recording_out_of_fold_indices, + ) + group_ids = np.repeat(np.arange(16), 3) + frame = pd.DataFrame( + { + "account": [f"account-{group_id}" for group_id in group_ids], + "value": np.arange(len(group_ids), dtype=np.float64), + "target": group_ids.astype(np.float64), + } + ) + config = RunConfig( + candidate_sources=( + PortfolioSource(specs=(EstimatorSpec("ridge", "linear", {"alpha": 1.0}),)), + ), + ensemble_enabled=False, + plateau_enabled=False, + oof_folds=4, + eval_strategy=None, + conformal_alpha=0.1, + ) + + Predictor("tabular_regression", config=config).fit( + frame, + target="target", + group_by="account", + ) + + assert len(captured) == 1 + groups, splits = captured[0] + for train_indices, eval_indices in splits: + assert set(groups[train_indices]).isdisjoint(groups[eval_indices]) + + +def test_classification_rejects_conformal_intervals() -> None: + with pytest.raises(ValueError, match="regression"): + Predictor( + "tabular_classification", + config=RunConfig(conformal_alpha=0.1), + ) diff --git a/tests/test_correctness_bugs.py b/tests/test_correctness_bugs.py new file mode 100644 index 0000000..450be8f --- /dev/null +++ b/tests/test_correctness_bugs.py @@ -0,0 +1,173 @@ +from __future__ import annotations + +import warnings +from typing import Any, get_origin, get_type_hints + +import numpy as np +import pytest +from numpy import typing as npt + +from falcon import Predictor, sklapi +from falcon.abstract import Pipeline +from falcon.runtime import Runtime +from falcon.serialization import ( + DEFAULT_PRODUCER_NAME, + SerializedModelRepr, + input_tags, +) +from falcon.sklapi import FalconTabularClassifier, FalconTabularRegressor +from falcon.tabular.ingestion import ingest_data +from falcon.types import ColumnTypes, DatasetSchema + + +class _TypedStep: + def __init__(self, input_type: type[Any], output_type: type[Any]) -> None: + self.input_type = input_type + self.output_type = output_type + + def get_input_type(self) -> type[Any]: + return self.input_type + + def get_output_type(self) -> type[Any]: + return self.output_type + + def fit( + self, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + schema: DatasetSchema, + *, + groups: npt.ArrayLike | None = None, + ) -> None: + return None + + def transform(self, X: npt.NDArray[Any]) -> npt.NDArray[Any]: + return X + + def serialize(self) -> SerializedModelRepr: + raise NotImplementedError + + +class _RecordingRuntime: + def __init__(self, outputs: dict[str, npt.NDArray[Any]]) -> None: + self.outputs = outputs + self.inputs: dict[str, npt.NDArray[Any]] | None = None + + def compute( + self, + inputs: dict[str, npt.NDArray[Any]], + attributes: dict[str, Any], + ) -> dict[str, npt.NDArray[Any]]: + self.inputs = inputs + return self.outputs + + +def test_pipeline_validates_the_second_element_type() -> None: + pipeline = Pipeline(task="tabular_regression", dataset_size=(1, 1)) + pipeline.add_step(_TypedStep(str, int)) + + with pytest.raises(RuntimeError, match="input type"): + pipeline.add_step(_TypedStep(str, float)) + + +def test_datetime_input_tag_uses_the_producer_name() -> None: + assert input_tags[ColumnTypes.DATETIME_YMDHMS_ISO8601] == [ + f"{DEFAULT_PRODUCER_NAME}::datetime_ymdhms_iso8601:v1" + ] + + +@pytest.mark.parametrize( + ("estimator_class", "expected_task"), + [ + (FalconTabularClassifier, "tabular_classification"), + (FalconTabularRegressor, "tabular_regression"), + ], +) +def test_sklapi_resolves_configuration_for_its_task( + monkeypatch: pytest.MonkeyPatch, + estimator_class: type[FalconTabularClassifier | FalconTabularRegressor], + expected_task: str, +) -> None: + calls: list[tuple[str, str]] = [] + + class RecordingPredictor: + def __init__( + self, + task: str, + preset: str, + eval_strategy: str, + ) -> None: + del eval_strategy + calls.append((task, preset)) + + monkeypatch.setattr(sklapi, "Predictor", RecordingPredictor) + + estimator = estimator_class(preset="balanced") + estimator._new_predictor() + + assert calls == [(expected_task, "balanced")] + + +@pytest.mark.parametrize( + ("features", "target", "message"), + [ + ([], 1, "Features List cannot be empty"), + (["feature"], 1, "Expected list of integers as features"), + ([0], "target", "Expected integer as target"), + ], +) +def test_ingestion_raises_for_invalid_numpy_column_selectors( + features: list[int] | list[str], target: int | str, message: str +) -> None: + data = np.arange(12).reshape(4, 3) + + with pytest.raises(ValueError, match=message): + ingest_data( + data, + task="tabular_regression", + features=features, + target=target, + ) + + +def test_runtime_predict_does_not_mutate_caller_inputs() -> None: + outputs: dict[str, npt.NDArray[np.float32]] = { + "y_pred": np.asarray([1.0, 2.0], dtype=np.float32) + } + backend = _RecordingRuntime(outputs) + runtime = Runtime.__new__(Runtime) + runtime.runtime = backend + runtime._input_names = ["first", "second"] + first: npt.NDArray[np.float32] = np.asarray([1.0, 2.0], dtype=np.float32) + second: npt.NDArray[np.float32] = np.asarray([3.0, 4.0], dtype=np.float32) + inputs = {"first": first, "second": second} + + result = runtime._predict(inputs) + + assert result is outputs + assert inputs["first"] is first + assert inputs["second"] is second + assert first.shape == (2,) + assert second.shape == (2,) + assert backend.inputs is not None + assert backend.inputs["first"].shape == (2, 1) + assert backend.inputs["second"].shape == (2, 1) + assert get_origin(get_type_hints(Runtime._predict)["return"]) is dict + + +def test_predictor_does_not_replace_warnings_warn( + monkeypatch: pytest.MonkeyPatch, +) -> None: + def user_warn( + message: Warning | str, + category: type[Warning] | None = None, + stacklevel: int = 1, + source: Any = None, + ) -> None: + return None + + monkeypatch.setattr(warnings, "warn", user_warn) + + Predictor("tabular_regression", preset="fast", eval_strategy=None) + + assert warnings.warn is user_warn diff --git a/tests/test_datasets.py b/tests/test_datasets.py index 88706eb..c04fe8e 100644 --- a/tests/test_datasets.py +++ b/tests/test_datasets.py @@ -1,12 +1,13 @@ from falcon.datasets import load_churn_dataset, load_insurance_dataset -def test_churn_dataset(): + +def test_churn_dataset() -> None: df = load_churn_dataset() - assert df is not None + assert df is not None assert df.shape == (10000, 11) -def test_insurance_dataset(): +def test_insurance_dataset() -> None: df = load_insurance_dataset() - assert df is not None - assert df.shape == (1338, 7) \ No newline at end of file + assert df is not None + assert df.shape == (1338, 7) diff --git a/tests/test_decision_rule.py b/tests/test_decision_rule.py new file mode 100644 index 0000000..e728398 --- /dev/null +++ b/tests/test_decision_rule.py @@ -0,0 +1,260 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any + +import numpy as np +import pandas as pd +import pytest +from sklearn.datasets import make_classification + +from falcon import Predictor +from falcon.config import PortfolioSource, RunConfig +from falcon.constants import TABULAR_CLASSIFICATION_TASK, TABULAR_REGRESSION_TASK +from falcon.runtime import Runtime +from falcon.tabular.candidates import EstimatorSpec +from falcon.tabular.models.sklearn_model import SklearnModel +from falcon.tabular.training import CandidateLearner +from falcon.types import ColumnTypes, DatasetSchema +from tests.fnnx_conformance import assert_fnnx_conforms, extract_fnnx_graph + + +def _imbalanced_frame( + n_classes: int, + n_samples: int = 1_400, +) -> tuple[pd.DataFrame, list[str]]: + weights = [0.9] if n_classes == 2 else [0.65, 0.25] + X, y = make_classification( + n_samples=n_samples, + n_features=8, + n_informative=5, + n_redundant=0, + n_classes=n_classes, + n_clusters_per_class=1, + weights=weights, + class_sep=1.1, + flip_y=0.05, + random_state=13, + ) + features = [f"feature_{index}" for index in range(X.shape[1])] + frame = pd.DataFrame(X, columns=features) + frame["target"] = np.asarray([f"class_{label}" for label in y]) + return frame, features + + +def _decision_config(**overrides: Any) -> RunConfig: + settings: dict[str, Any] = { + "candidate_sources": ( + PortfolioSource( + specs=( + EstimatorSpec( + "hist", + "hist_gradient_boosting", + {"max_iter": 40, "min_samples_leaf": 20}, + ), + ) + ), + ), + "ensemble_enabled": False, + "plateau_enabled": False, + "oof_folds": 3, + "eval_strategy": None, + "random_state": 5, + } + settings.update(overrides) + return RunConfig(**settings) + + +def _schema(task: str, n_rows: int, n_features: int) -> DatasetSchema: + return DatasetSchema( + column_names=tuple(f"feature_{index}" for index in range(n_features)), + column_types=(ColumnTypes.NUMERIC_REGULAR,) * n_features, + target_name="target", + target_kind=( + "classification" if task == TABULAR_CLASSIFICATION_TASK else "regression" + ), + dimensions=(n_rows, n_features), + ) + + +@pytest.mark.parametrize("n_classes", [2, 3]) +@pytest.mark.parametrize("calibrate", [False, True]) +def test_decision_rule_round_trips_through_the_runtime( + tmp_path: Path, + n_classes: int, + calibrate: bool, +) -> None: + frame, features = _imbalanced_frame(n_classes) + predictor = Predictor( + TABULAR_CLASSIFICATION_TASK, + config=_decision_config(calibrate=calibrate), + ).fit(frame, features=features, target="target") + + assert predictor._learner is not None + weights = predictor._learner.decision_weights_ + assert weights is not None + assert len(weights) == n_classes + assert weights != (1.0,) * n_classes + + inputs = frame[features] + predictions = predictor.predict(inputs) + probabilities = predictor.predict_proba(inputs) + + artifact_path = tmp_path / "decision.fnnx" + bundle = predictor.save(artifact_path) + graph = extract_fnnx_graph(bundle) + assert_fnnx_conforms(graph) + decision_ops = { + node.op_type + for node in graph.model.graph.node + if "falcon_decision" in node.name + } + assert decision_ops == {"ArgMax", "Mul"} + + runtime = Runtime(str(artifact_path)) + np.testing.assert_array_equal(runtime.predict(inputs), predictions) + np.testing.assert_allclose( + runtime.predict_proba(inputs), + probabilities, + rtol=1e-5, + atol=1e-6, + ) + + +def test_the_rule_moves_labels_without_moving_probabilities() -> None: + frame, features = _imbalanced_frame(2) + inputs = frame[features] + tuned = Predictor( + TABULAR_CLASSIFICATION_TASK, + config=_decision_config(), + ).fit(frame, features=features, target="target") + plain = Predictor( + TABULAR_CLASSIFICATION_TASK, + config=_decision_config(decision_metric=None), + ).fit(frame, features=features, target="target") + + assert plain._learner is not None + assert plain._learner.decision_weights_ is None + np.testing.assert_allclose( + tuned.predict_proba(inputs), + plain.predict_proba(inputs), + rtol=1e-5, + atol=1e-6, + ) + assert not np.array_equal(tuned.predict(inputs), plain.predict(inputs)) + + assert plain.classes_ is not None + plain_labels = np.asarray(plain.classes_)[ + np.argmax(plain.predict_proba(inputs), axis=1) + ] + np.testing.assert_array_equal(plain.predict(inputs), plain_labels) + + +def test_reported_cross_validation_score_uses_the_deployed_rule() -> None: + frame, features = _imbalanced_frame(2) + predictor = Predictor( + TABULAR_CLASSIFICATION_TASK, + config=_decision_config(eval_strategy="cv"), + ).fit(frame, features=features, target="target") + + assert predictor._learner is not None + learner = predictor._learner + assert learner.decision_weights_ is not None + oof_result = learner.oof_predictions() + assert oof_result is not None + _, predictions = oof_result + weighted_result = learner._weighted_oof_predictions() + assert weighted_result is not None + _, probabilities = weighted_result + + np.testing.assert_array_equal( + predictions, + np.argmax( + probabilities * np.asarray(learner.decision_weights_, dtype=np.float32), + axis=1, + ), + ) + assert not np.array_equal(predictions, np.argmax(probabilities, axis=1)) + + +def test_classification_single_candidate_runs_an_oof_round_for_the_rule( + monkeypatch: pytest.MonkeyPatch, +) -> None: + frame, features = _imbalanced_frame(2, n_samples=400) + X = frame[features].to_numpy(dtype=np.float64) + y = (frame["target"].to_numpy() == "class_1").astype(np.int64) + fit_calls: list[int] = [] + original_fit = SklearnModel.fit + + def counting_fit(self: SklearnModel, *args: Any, **kwargs: Any) -> None: + fit_calls.append(1) + original_fit(self, *args, **kwargs) + + monkeypatch.setattr(SklearnModel, "fit", counting_fit) + config = _decision_config() + learner = CandidateLearner(TABULAR_CLASSIFICATION_TASK, X.shape, config) + + learner.fit(X, y, _schema(TABULAR_CLASSIFICATION_TASK, *X.shape)) + + assert len(fit_calls) == config.oof_folds + 1 + assert learner._evaluation_run is not None + + +def test_a_rare_class_below_the_floor_exports_without_a_decision_rule( + tmp_path: Path, +) -> None: + frame, features = _imbalanced_frame(2, n_samples=400) + inputs = frame[features] + tuned = Predictor( + TABULAR_CLASSIFICATION_TASK, + config=_decision_config(), + ).fit(frame, features=features, target="target") + plain = Predictor( + TABULAR_CLASSIFICATION_TASK, + config=_decision_config(decision_metric=None), + ).fit(frame, features=features, target="target") + + assert tuned._learner is not None + assert tuned._learner.decision_weights_ is None + + artifact_path = tmp_path / "floor.fnnx" + bundle = tuned.save(artifact_path) + graph = extract_fnnx_graph(bundle) + assert not any("falcon_decision" in node.name for node in graph.model.graph.node) + + np.testing.assert_array_equal(tuned.predict(inputs), plain.predict(inputs)) + np.testing.assert_array_equal( + Runtime(str(artifact_path)).predict(inputs), + tuned.predict(inputs), + ) + + +def test_regression_rejects_an_explicit_decision_metric() -> None: + rng = np.random.default_rng(4) + X = rng.normal(size=(60, 2)) + y = 2.0 * X[:, 0] - X[:, 1] + config = _decision_config(decision_metric="f1") + learner = CandidateLearner(TABULAR_REGRESSION_TASK, X.shape, config) + + with pytest.raises(ValueError, match="only available for classification"): + learner.fit(X, y, _schema(TABULAR_REGRESSION_TASK, *X.shape)) + + +def test_regression_ignores_the_untouched_decision_metric_default() -> None: + rng = np.random.default_rng(4) + X = rng.normal(size=(60, 2)) + y = 2.0 * X[:, 0] - X[:, 1] + config = RunConfig( + candidate_sources=( + PortfolioSource(specs=(EstimatorSpec("ridge", "linear", {"alpha": 1.0}),)), + ), + ensemble_enabled=False, + plateau_enabled=False, + oof_folds=3, + eval_strategy=None, + ) + learner = CandidateLearner(TABULAR_REGRESSION_TASK, X.shape, config) + + learner.fit(X, y, _schema(TABULAR_REGRESSION_TASK, *X.shape)) + + assert learner.decision_weights_ is None diff --git a/tests/test_export_conformance.py b/tests/test_export_conformance.py new file mode 100644 index 0000000..9b6318e --- /dev/null +++ b/tests/test_export_conformance.py @@ -0,0 +1,511 @@ +from __future__ import annotations + +import json +import tarfile +from collections.abc import Callable +from dataclasses import dataclass +from io import BytesIO +from pathlib import Path +from typing import Any + +import numpy as np +import onnx +import pandas as pd +import pytest +from numpy import typing as npt +from onnx import TensorProto, helper + +from falcon import Predictor +from falcon.config import ( + ONNX_IR_VERSION, + ONNX_OPSET_VERSION, + PortfolioSource, + RunConfig, +) +from falcon.constants import DEFAULT_PRODUCER_NAME +from falcon.runtime import Runtime +from falcon.serialization import FNNXSerializer, SerializedModelRepr +from falcon.tabular.candidates import EstimatorSpec +from falcon.types import ColumnTypes +from tests.fnnx_conformance import ( + ExtractedFNNXGraph, + assert_fnnx_conforms, + assert_onnx_is_valid, + assert_standard_node_domains, + assert_standard_opset_declarations, + extract_fnnx_graph, + read_bundle_member, +) + +_TASKS = ("tabular_classification", "tabular_regression") +_COLUMN_TYPES = { + "numeric": ColumnTypes.NUMERIC_REGULAR, + "categorical_low": ColumnTypes.CAT_LOW_CARD, + "categorical_high": ColumnTypes.CAT_HIGH_CARD, + "text": ColumnTypes.TEXT_UTF8, + "date": ColumnTypes.DATE_YMD_ISO8601, + "datetime": ColumnTypes.DATETIME_YMDHMS_ISO8601, +} +_ALL_CASES = [ + pytest.param(task, column_name, id=f"{task}-{column_name}") + for task in _TASKS + for column_name in _COLUMN_TYPES +] +_NODE_DOMAIN_CASES = _ALL_CASES +_ROUND_TRIP_CASES = _ALL_CASES +_BRANCHING_OPS = frozenset({"Where", "IsNaN", "Equal", "Or", "If", "Loop", "Scan"}) + + +@dataclass(frozen=True) +class RoundTripArtifact: + graph: ExtractedFNNXGraph + native_predictions: npt.NDArray[Any] + runtime_predictions: npt.NDArray[Any] + native_probabilities: npt.NDArray[Any] | None + runtime_probabilities: npt.NDArray[Any] | None + + +ArtifactFactory = Callable[[str, str], RoundTripArtifact] + + +def _conformance_config(task: str) -> RunConfig: + parameters: dict[str, object] + if task == "tabular_classification": + parameters = {"max_iter": 200} + else: + parameters = {"alpha": 1.0} + return RunConfig( + candidate_sources=( + PortfolioSource( + specs=( + EstimatorSpec( + "conformance-test", + "linear", + parameters, + ), + ) + ), + ), + ensemble_enabled=False, + eval_strategy=None, + ) + + +def _make_training_frame(task: str, column_name: str) -> pd.DataFrame: + sample_count = 128 + sample_indices = np.arange(sample_count) + if column_name == "numeric": + feature = np.linspace(-4.0, 7.0, sample_count) + elif column_name == "categorical_low": + feature = np.asarray([f"group-{index % 4}" for index in sample_indices]) + elif column_name == "categorical_high": + feature = np.asarray([f"category-{index:03d}" for index in sample_indices]) + elif column_name == "text": + feature = np.asarray( + [ + "falcon document sample " + f"{index} contains several useful words about " + f"{'alpha' if index % 2 else 'beta'}" + for index in sample_indices + ] + ) + elif column_name == "date": + feature = ( + pd.Timestamp("2020-01-01") + pd.to_timedelta(sample_indices, unit="D") + ).strftime("%Y-%m-%d") + elif column_name == "datetime": + feature = ( + pd.Timestamp("2020-01-01") + pd.to_timedelta(sample_indices, unit="h") + ).strftime("%Y-%m-%dT%H:%M:%SZ") + else: + raise ValueError(f"Unknown column case: {column_name}") + + if task == "tabular_classification": + target = np.where(sample_indices % 2, "positive", "negative") + elif task == "tabular_regression": + target = 0.4 * sample_indices + np.sin(sample_indices) + else: + raise ValueError(f"Unknown task: {task}") + return pd.DataFrame({column_name: feature, "target": target}) + + +def _build_artifact( + task: str, column_name: str, artifact_directory: Path +) -> RoundTripArtifact: + frame = _make_training_frame(task, column_name) + predictor = Predictor(task, config=_conformance_config(task)).fit( + frame, + features=[column_name], + target="target", + ) + assert predictor._training_data is not None + assert predictor._training_data.schema.column_types == (_COLUMN_TYPES[column_name],) + + inputs = frame[[column_name]].copy() + if column_name == "categorical_high": + inputs.iloc[0, 0] = "Z" + native_predictions = np.asarray(predictor.predict(inputs)).reshape(-1) + native_probabilities = ( + predictor.predict_proba(inputs) if task == "tabular_classification" else None + ) + + artifact_path = artifact_directory / f"{task}-{column_name}.fnnx" + bundle = predictor.save(artifact_path) + runtime = Runtime(str(artifact_path)) + runtime_predictions = np.asarray(runtime.predict(inputs)).reshape(-1) + runtime_probabilities = ( + np.asarray(runtime.predict_proba(inputs)) + if task == "tabular_classification" + else None + ) + return RoundTripArtifact( + graph=extract_fnnx_graph(bundle), + native_predictions=native_predictions, + runtime_predictions=runtime_predictions, + native_probabilities=native_probabilities, + runtime_probabilities=runtime_probabilities, + ) + + +@pytest.fixture(scope="module") +def artifact_factory(tmp_path_factory: pytest.TempPathFactory) -> ArtifactFactory: + artifact_directory = tmp_path_factory.mktemp("export-conformance") + cache: dict[tuple[str, str], RoundTripArtifact] = {} + + def get_artifact(task: str, column_name: str) -> RoundTripArtifact: + key = (task, column_name) + if key not in cache: + cache[key] = _build_artifact(task, column_name, artifact_directory) + return cache[key] + + return get_artifact + + +def _make_identity_model() -> onnx.ModelProto: + input_info = helper.make_tensor_value_info("input", TensorProto.FLOAT, [None, 1]) + output_info = helper.make_tensor_value_info("output", TensorProto.FLOAT, [None, 1]) + graph = helper.make_graph( + [helper.make_node("Identity", ["input"], ["output"], name="identity")], + "identity_graph", + [input_info], + [output_info], + ) + return helper.make_model( + graph, + opset_imports=[helper.make_opsetid("", ONNX_OPSET_VERSION)], + ) + + +def test_fnnx_declares_only_opset_domains_used_by_the_graph() -> None: + component = SerializedModelRepr( + _make_identity_model(), + n_inputs=1, + n_outputs=1, + initial_types=["FLOAT32"], + initial_shapes=[[None, 1]], + ) + + bundle = FNNXSerializer([component], task="tabular_regression").serialize() + graph = extract_fnnx_graph(bundle) + + assert { + "ai.onnx" if opset.domain in {"", "ai.onnx"} else opset.domain + for opset in graph.model.opset_import + } == {"ai.onnx"} + assert graph.fnnx_opset_domains == ("ai.onnx",) + + +def test_conformance_scanner_reports_forbidden_node() -> None: + model = _make_identity_model() + model.graph.node[0].domain = "com.microsoft" + model.graph.node[0].name = "forbidden_identity" + del model.opset_import[:] + model.opset_import.extend( + [ + helper.make_opsetid("", ONNX_OPSET_VERSION), + helper.make_opsetid("com.microsoft", 1), + ] + ) + + graph = ExtractedFNNXGraph( + model=model, + fnnx_opset_domains=("ai.onnx", "com.microsoft"), + declared_ir_version=ONNX_IR_VERSION, + ) + with pytest.raises(AssertionError) as error: + assert_fnnx_conforms(graph) + + message = str(error.value) + assert "forbidden_identity" in message + assert "com.microsoft" in message + + +def test_node_domain_scanner_reports_forbidden_node_in_subgraph() -> None: + nested_output = helper.make_tensor_value_info( + "nested_output", TensorProto.FLOAT, [None, 1] + ) + nested_graph = helper.make_graph( + [ + helper.make_node( + "Identity", + ["input"], + ["nested_output"], + name="nested_forbidden_identity", + domain="com.microsoft", + ) + ], + "nested_graph", + [], + [nested_output], + ) + model = _make_identity_model() + model.graph.node[0].attribute.extend( + [helper.make_attribute("nested_graph", nested_graph)] + ) + + with pytest.raises(AssertionError) as error: + assert_standard_node_domains(model) + + message = str(error.value) + assert "nested_forbidden_identity" in message + assert "com.microsoft" in message + + +def test_declared_opset_scanner_reports_model_and_bundle_sources() -> None: + model = _make_identity_model() + model.opset_import.extend([helper.make_opsetid("com.microsoft", 1)]) + graph = ExtractedFNNXGraph( + model=model, + fnnx_opset_domains=("ai.onnx", "com.microsoft"), + declared_ir_version=ONNX_IR_VERSION, + ) + + with pytest.raises(AssertionError) as error: + assert_standard_opset_declarations(graph) + + message = str(error.value) + assert "model imports com.microsoft" in message + assert "FNNX ops.json declares com.microsoft" in message + + +@pytest.mark.parametrize(("task", "column_name"), _ALL_CASES) +def test_exported_model_passes_onnx_checker( + artifact_factory: ArtifactFactory, task: str, column_name: str +) -> None: + artifact = artifact_factory(task, column_name) + assert_onnx_is_valid(artifact.graph.model) + + +def _collect_bundle_tags(bundle: bytes) -> tuple[list[str], list[str]]: + """Returns every producer identity and every tag the bundle declares.""" + with tarfile.open(fileobj=BytesIO(bundle), mode="r:") as archive: + manifest = json.loads(read_bundle_member(archive, "manifest.json")) + metadata = json.loads(read_bundle_member(archive, "meta.json")) + + producers = [manifest["producer_name"]] + tags = list(manifest["producer_tags"]) + for io_ in [*manifest["inputs"], *manifest["outputs"]]: + tags.extend(io_.get("tags", [])) + for container in metadata: + producers.append(container["producer"]) + tags.extend(container["producer_tags"]) + return producers, tags + + +@pytest.mark.parametrize("task", _TASKS) +def test_every_bundle_tag_is_namespaced_under_the_producer( + task: str, tmp_path: Path +) -> None: + frame = _make_training_frame(task, "numeric") + predictor = Predictor(task, config=_conformance_config(task)).fit( + frame, features=["numeric"], target="target" + ) + bundle = predictor.save(tmp_path / f"{task}.fnnx") + + producers, tags = _collect_bundle_tags(bundle) + + assert DEFAULT_PRODUCER_NAME == "falcon.fnnx.ai" + assert producers and set(producers) == {DEFAULT_PRODUCER_NAME} + assert tags + assert [ + tag for tag in tags if not tag.startswith(f"{DEFAULT_PRODUCER_NAME}::") + ] == [] + + +@pytest.mark.parametrize(("task", "column_name"), _ALL_CASES) +def test_exported_model_pins_the_ir_version_runtimes_accept( + artifact_factory: ArtifactFactory, task: str, column_name: str +) -> None: + """Guards against inheriting `onnx`'s newest IR version. + + A runtime refuses to load any model whose IR is newer than it knows, and the two + versions move independently: Python 3.10 resolves onnx 1.22 (IR 13) against + onnxruntime 1.23 (max IR 11), which rejected every exported model. + """ + artifact = artifact_factory(task, column_name) + + assert artifact.graph.model.ir_version == ONNX_IR_VERSION + assert artifact.graph.declared_ir_version == ONNX_IR_VERSION + + +@pytest.mark.parametrize(("task", "column_name"), _NODE_DOMAIN_CASES) +def test_exported_nodes_use_standard_domains( + artifact_factory: ArtifactFactory, task: str, column_name: str +) -> None: + artifact = artifact_factory(task, column_name) + assert_standard_node_domains(artifact.graph.model) + + +@pytest.mark.parametrize(("task", "column_name"), _ALL_CASES) +def test_exported_opset_declarations_are_standard( + artifact_factory: ArtifactFactory, task: str, column_name: str +) -> None: + artifact = artifact_factory(task, column_name) + assert_standard_opset_declarations(artifact.graph) + + +@pytest.mark.parametrize(("task", "column_name"), _ROUND_TRIP_CASES) +def test_fnnx_round_trip_matches_native_predictions( + artifact_factory: ArtifactFactory, task: str, column_name: str +) -> None: + artifact = artifact_factory(task, column_name) + if task == "tabular_classification": + np.testing.assert_array_equal( + artifact.runtime_predictions, artifact.native_predictions + ) + assert artifact.native_probabilities is not None + assert artifact.runtime_probabilities is not None + np.testing.assert_allclose( + artifact.runtime_probabilities, + artifact.native_probabilities, + rtol=1e-5, + atol=1e-6, + ) + np.testing.assert_allclose( + artifact.runtime_probabilities.sum(axis=1), 1.0, atol=1e-6 + ) + else: + np.testing.assert_allclose( + artifact.runtime_predictions, + artifact.native_predictions, + rtol=1e-5, + atol=1e-5, + ) + + +def _make_missing_training_frame(task: str) -> pd.DataFrame: + sample_indices = np.arange(128) + features: dict[str, npt.NDArray[Any]] = { + "numeric": np.linspace(-4.0, 7.0, sample_indices.size), + "categorical_low": np.asarray( + [f"group-{index % 4}" for index in sample_indices] + ), + "categorical_high": np.asarray( + [f"category-{index:03d}" for index in sample_indices] + ), + "date": np.asarray( + ( + pd.Timestamp("2020-01-01") + pd.to_timedelta(sample_indices, unit="D") + ).strftime("%Y-%m-%d") + ), + "datetime": np.asarray( + ( + pd.Timestamp("2020-01-01") + pd.to_timedelta(sample_indices, unit="h") + ).strftime("%Y-%m-%dT%H:%M:%SZ") + ), + "text": np.where( + sample_indices % 2, + "falcons fly quickly above the quiet mountain valley", + "hawks circle slowly over the broad forest canopy", + ), + } + for name, values in features.items(): + features[name] = np.asarray(values, dtype=np.object_) + features[name][[5, 37]] = np.nan + + if task == "tabular_classification": + target: npt.NDArray[Any] = np.where(sample_indices % 2, "positive", "negative") + else: + target = 0.4 * sample_indices + np.sin(sample_indices) + return pd.DataFrame({**features, "target": target}) + + +@pytest.mark.parametrize("task", _TASKS) +def test_missing_feature_rows_round_trip_without_being_dropped( + task: str, tmp_path: Path +) -> None: + frame = _make_missing_training_frame(task) + feature_names = list(_COLUMN_TYPES) + predictor = Predictor(task, config=_conformance_config(task)).fit( + frame, + features=feature_names, + target="target", + ) + assert predictor._training_data is not None + assert predictor._training_data.X.shape[0] == frame.shape[0] + assert pd.isna(predictor._training_data.X).any() + assert predictor._training_data.schema.column_types == tuple(_COLUMN_TYPES.values()) + + missing_inputs = frame.loc[[5, 37], feature_names] + native_predictions = np.asarray(predictor.predict(missing_inputs)).reshape(-1) + artifact_path = tmp_path / f"missing-{task}.fnnx" + bundle = predictor.save(artifact_path) + runtime = Runtime(str(artifact_path)) + runtime_predictions = np.asarray(runtime.predict(missing_inputs)).reshape(-1) + + assert_fnnx_conforms(extract_fnnx_graph(bundle)) + if task == "tabular_classification": + np.testing.assert_array_equal(runtime_predictions, native_predictions) + np.testing.assert_allclose( + runtime.predict_proba(missing_inputs), + predictor.predict_proba(missing_inputs), + rtol=1e-5, + atol=1e-6, + ) + else: + np.testing.assert_allclose( + runtime_predictions, native_predictions, rtol=1e-5, atol=1e-5 + ) + + +@pytest.mark.parametrize("task", _TASKS) +def test_export_without_imputation_is_free_of_data_dependent_branching( + task: str, tmp_path: Path +) -> None: + sample_indices = np.arange(128) + frame = pd.DataFrame( + { + "numeric": np.linspace(-4.0, 7.0, sample_indices.size), + "categorical_low": [f"group-{index % 4}" for index in sample_indices], + "categorical_high": [f"category-{index:03d}" for index in sample_indices], + "target": ( + np.where(sample_indices % 2, "positive", "negative") + if task == "tabular_classification" + else 0.4 * sample_indices + np.sin(sample_indices) + ), + } + ) + feature_names = ["numeric", "categorical_low", "categorical_high"] + config = _conformance_config(task).replaced(impute_missing=False) + predictor = Predictor(task, config=config).fit( + frame, features=feature_names, target="target" + ) + + artifact_path = tmp_path / f"branch-free-{task}.fnnx" + bundle = predictor.save(artifact_path) + graph = extract_fnnx_graph(bundle) + assert_fnnx_conforms(graph) + + emitted_ops = {node.op_type for node in graph.model.graph.node} + assert not emitted_ops & _BRANCHING_OPS + + inputs = frame.loc[[0, 63], feature_names] + runtime = Runtime(str(artifact_path)) + native_predictions = np.asarray(predictor.predict(inputs)).reshape(-1) + runtime_predictions = np.asarray(runtime.predict(inputs)).reshape(-1) + if task == "tabular_classification": + np.testing.assert_array_equal(runtime_predictions, native_predictions) + else: + np.testing.assert_allclose( + runtime_predictions, native_predictions, rtol=1e-5, atol=1e-5 + ) diff --git a/tests/test_gbdt_models.py b/tests/test_gbdt_models.py new file mode 100644 index 0000000..3f0e635 --- /dev/null +++ b/tests/test_gbdt_models.py @@ -0,0 +1,194 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any + +import numpy as np +import onnx +import onnxruntime as ort +import pandas as pd +import pytest +from sklearn.datasets import make_classification, make_regression + +from falcon import Predictor +from falcon.config import PortfolioSource, RunConfig +from falcon.constants import TABULAR_CLASSIFICATION_TASK, TABULAR_REGRESSION_TASK +from falcon.runtime import Runtime +from falcon.serialization import serialize_to_onnx +from falcon.tabular.candidates import EstimatorSpec +from falcon.tabular.models.gbdt import get_gbdt_model_classes +from tests.fnnx_conformance import assert_standard_node_domains + +_FAMILIES = ("lightgbm", "xgboost", "catboost") + + +def _model_parameters(family: str) -> dict[str, Any]: + if family == "catboost": + return {"iterations": 8, "depth": 3} + return {"n_estimators": 8, "max_depth": 3} + + +def _model_class(family: str, task: str) -> type[Any]: + classes = get_gbdt_model_classes(task, n_classes=2) + if family not in classes: + pytest.skip(f"{family} is not installed") + return classes[family] + + +@pytest.mark.parametrize("family", _FAMILIES) +def test_gbdt_binary_classifier_onnx_parity(family: str) -> None: + X, y = make_classification( + n_samples=96, + n_features=5, + n_informative=4, + n_redundant=0, + random_state=12, + ) + X = X.astype(np.float32) + model = _model_class(family, TABULAR_CLASSIFICATION_TASK)( + random_state=17, + **_model_parameters(family), + ) + model.fit(X, y) + + graph = serialize_to_onnx( + [model.serialize()], + task=TABULAR_CLASSIFICATION_TASK, + ) + onnx.checker.check_model(graph) + assert_standard_node_domains(graph) + assert any(node.op_type == "TreeEnsembleClassifier" for node in graph.graph.node) + assert not graph.graph.output[0].type.tensor_type.shape.dim[0].HasField("dim_value") + + session = ort.InferenceSession( + graph.SerializeToString(), providers=["CPUExecutionProvider"] + ) + labels, probabilities = session.run( + None, + {session.get_inputs()[0].name: X}, + ) + + assert np.array_equal(np.asarray(labels).reshape(-1), model.predict(X)) + np.testing.assert_allclose( + probabilities, + model.predict_proba(X), + rtol=1e-5, + atol=1e-6, + ) + + +@pytest.mark.parametrize("family", _FAMILIES) +def test_gbdt_regressor_onnx_parity_and_output_shape(family: str) -> None: + X, y = make_regression( + n_samples=96, + n_features=5, + n_informative=4, + random_state=12, + ) + X = X.astype(np.float32) + y = y.astype(np.float32) + model = _model_class(family, TABULAR_REGRESSION_TASK)( + random_state=17, + **_model_parameters(family), + ) + model.fit(X, y) + + graph = serialize_to_onnx([model.serialize()], task=TABULAR_REGRESSION_TASK) + onnx.checker.check_model(graph) + assert_standard_node_domains(graph) + assert any(node.op_type == "TreeEnsembleRegressor" for node in graph.graph.node) + + session = ort.InferenceSession( + graph.SerializeToString(), providers=["CPUExecutionProvider"] + ) + (predictions,) = session.run( + None, + {session.get_inputs()[0].name: X}, + ) + + assert np.asarray(predictions).shape == (len(X),) + np.testing.assert_allclose( + predictions, + model.predict(X), + rtol=1e-5, + atol=1e-4, + ) + + +def test_xgboost_regressor_fnnx_parity_after_numeric_preprocessing( + tmp_path: Path, +) -> None: + X, y = make_regression( + n_samples=64, + n_features=4, + n_informative=3, + random_state=4, + ) + frame = pd.DataFrame(X, columns=["a", "b", "c", "d"]) + frame["target"] = y + _model_class("xgboost", TABULAR_REGRESSION_TASK) + config = RunConfig( + candidate_sources=( + PortfolioSource( + specs=( + EstimatorSpec( + "xgboost-test", + "xgboost", + {"n_estimators": 4, "max_depth": 2}, + ), + ) + ), + ), + ensemble_enabled=False, + eval_strategy=None, + ) + predictor = Predictor(TABULAR_REGRESSION_TASK, config=config).fit(frame) + inputs = frame.drop(columns="target") + expected = predictor.predict(inputs) + artifact_path = tmp_path / "xgboost-regression.fnnx" + + predictor.save(artifact_path) + actual = Runtime(str(artifact_path)).predict(inputs) + + np.testing.assert_allclose(actual, expected, rtol=1e-5, atol=1e-4) + + +def test_catboost_is_excluded_from_multiclass_with_log( + caplog: pytest.LogCaptureFixture, +) -> None: + if "catboost" not in get_gbdt_model_classes( + TABULAR_CLASSIFICATION_TASK, n_classes=2 + ): + pytest.skip("catboost is not installed") + + with caplog.at_level("INFO", logger="falcon"): + classes = get_gbdt_model_classes( + TABULAR_CLASSIFICATION_TASK, + n_classes=3, + ) + + assert "catboost" not in classes + assert "CatBoost" in caplog.text + assert "multiclass" in caplog.text + + +def test_each_optional_family_registers_independently( + monkeypatch: pytest.MonkeyPatch, +) -> None: + from falcon.tabular.models import gbdt + + available_modules = {"catboost"} + monkeypatch.setattr( + gbdt, + "_module_is_available", + lambda module_name: module_name in available_modules, + ) + + registry = gbdt._discover_gbdt_families() + + assert set(registry) == {"catboost"} + + +def test_unknown_task_is_rejected() -> None: + with pytest.raises(ValueError, match="Unknown task"): + get_gbdt_model_classes("forecasting") diff --git a/tests/test_hpo.py b/tests/test_hpo.py new file mode 100644 index 0000000..160a0a6 --- /dev/null +++ b/tests/test_hpo.py @@ -0,0 +1,315 @@ +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path +from typing import TYPE_CHECKING, Any + +import numpy as np +import pandas as pd +import pytest +from numpy import typing as npt + +from falcon import Predictor +from falcon.config import HPOSource, PortfolioSource, RunConfig +from falcon.runtime import Runtime +from falcon.tabular import hpo +from falcon.tabular.candidates import EstimatorSpec, GreedyWeightedEnsemble + +optuna = pytest.importorskip("optuna") + +if TYPE_CHECKING: + from optuna.study import Study + from optuna.trial import Trial + + +def _regression_frame(n_rows: int = 60) -> pd.DataFrame: + values = np.linspace(-3.0, 3.0, n_rows) + return pd.DataFrame( + { + "first": values, + "second": np.square(values), + "target": 2.5 * values - 0.4 * np.square(values), + } + ) + + +def test_hpo_source_uses_grouped_cv_default_trial_pruner_and_budget( + monkeypatch: pytest.MonkeyPatch, +) -> None: + X = np.column_stack( + ( + np.repeat(np.arange(12), 2), + np.tile(np.asarray([0.0, 1.0]), 12), + ) + ).astype(np.float32) + y = (1.5 * X[:, 0] - X[:, 1]).astype(np.float32) + groups = np.repeat(np.arange(12), 2) + observed_splits: list[tuple[npt.NDArray[np.int64], npt.NDArray[np.int64]]] = [] + studies: list[Study] = [] + optimize_timeouts: list[float | None] = [] + original_splitter = hpo.out_of_fold_indices + original_create_study = optuna.create_study + original_optimize = optuna.study.Study.optimize + + def capture_splits( + features: npt.NDArray[Any], + targets: npt.NDArray[Any], + task: str, + group_values: npt.ArrayLike | None = None, + *, + n_splits: int = 5, + random_state: int = 42, + ) -> list[tuple[npt.NDArray[np.int64], npt.NDArray[np.int64]]]: + splits = original_splitter( + features, + targets, + task, + group_values, + n_splits=n_splits, + random_state=random_state, + ) + observed_splits.extend(splits) + return splits + + def capture_study(**kwargs: Any) -> Study: + study = original_create_study(**kwargs) + studies.append(study) + return study + + def capture_optimize( + study: Study, + objective: Callable[[Trial], float], + *args: Any, + **kwargs: Any, + ) -> None: + timeout = kwargs.get("timeout") + optimize_timeouts.append(None if timeout is None else float(timeout)) + original_optimize(study, objective, *args, **kwargs) + + monkeypatch.setattr(hpo, "out_of_fold_indices", capture_splits) + monkeypatch.setattr(optuna, "create_study", capture_study) + monkeypatch.setattr(optuna.study.Study, "optimize", capture_optimize) + + specs = HPOSource( + family="linear", + n_trials=2, + top_n=2, + time_budget_fraction=0.4, + ).get_candidates( + "tabular_regression", + X=X, + y=y, + groups=groups, + n_splits=4, + time_limit=10.0, + random_state=17, + ) + + assert len(observed_splits) == 4 + for train_indices, eval_indices in observed_splits: + assert set(groups[train_indices]).isdisjoint(groups[eval_indices]) + assert optimize_timeouts == [4.0] + assert isinstance(studies[0].pruner, optuna.pruners.MedianPruner) + assert studies[0].trials[0].params == {"alpha": 1.0} + assert len(specs) == 2 + assert len({tuple(spec.parameters.items()) for spec in specs}) == 2 + assert all(spec.name.startswith("hpo_linear_trial_") for spec in specs) + + +def test_hpo_candidates_join_portfolio_oof_ensemble_and_export( + tmp_path: Path, +) -> None: + frame = _regression_frame() + config = RunConfig( + candidate_sources=( + PortfolioSource( + specs=(EstimatorSpec("portfolio-ridge", "linear", {"alpha": 20.0}),) + ), + HPOSource(family="linear", n_trials=2), + ), + ensemble_enabled=True, + ensemble_max_iterations=5, + plateau_enabled=False, + oof_folds=3, + eval_strategy=None, + ) + predictor = Predictor("tabular_regression", config=config).fit( + frame, + features=["first", "second"], + target="target", + ) + leaderboard = predictor.leaderboard() + + assert list(leaderboard["candidate"])[0] == "portfolio-ridge" + assert any( + name.startswith("hpo_linear_trial_") for name in leaderboard["candidate"] + ) + assert predictor._learner is not None + assert isinstance(predictor._learner.model, GreedyWeightedEnsemble) + assert predictor._learner.model.score >= float(leaderboard["score"].max()) + + artifact_path = tmp_path / "hpo-ensemble.fnnx" + predictor.save(artifact_path) + inputs = frame[["first", "second"]].iloc[:8] + np.testing.assert_allclose( + Runtime(str(artifact_path)).predict(inputs), + predictor.predict(inputs), + rtol=1e-5, + atol=1e-5, + ) + + +def test_hpo_only_source_trains_without_ensembling() -> None: + frame = _regression_frame(40) + config = RunConfig( + candidate_sources=(HPOSource(family="linear", n_trials=2, top_n=2),), + ensemble_enabled=False, + plateau_enabled=False, + oof_folds=2, + eval_strategy=None, + ) + + predictor = Predictor("tabular_regression", config=config).fit( + frame, + features=["first", "second"], + target="target", + ) + + leaderboard = predictor.leaderboard() + assert len(leaderboard) == 2 + assert all( + name.startswith("hpo_linear_trial_") for name in leaderboard["candidate"] + ) + assert sorted(leaderboard["weight"]) == [0.0, 1.0] + assert predictor.predict(frame[["first", "second"]].iloc[:5]).shape == (5,) + assert predictor.save() + + +def test_hpo_classification_candidate_produces_probabilities() -> None: + values = np.linspace(-2.0, 2.0, 48) + frame = pd.DataFrame( + { + "first": values, + "second": np.sin(values), + "target": np.where(values > 0, "positive", "negative"), + } + ) + config = RunConfig( + candidate_sources=(HPOSource(family="linear", n_trials=1),), + ensemble_enabled=False, + plateau_enabled=False, + oof_folds=2, + eval_strategy=None, + ) + + predictor = Predictor("tabular_classification", config=config).fit( + frame, + features=["first", "second"], + target="target", + ) + probabilities = predictor.predict_proba(frame[["first", "second"]].iloc[:6]) + + assert probabilities.shape == (6, 2) + np.testing.assert_allclose(probabilities.sum(axis=1), 1.0, atol=1e-6) + + +def test_hpo_source_forwards_the_run_config_weighting_and_scoring_rule( + monkeypatch: pytest.MonkeyPatch, +) -> None: + captured: list[dict[str, Any]] = [] + + def capture(*args: Any, **kwargs: Any) -> tuple[EstimatorSpec, ...]: + captured.append(kwargs) + return (EstimatorSpec("hpo_stub", "linear", {"alpha": 1.0}),) + + monkeypatch.setattr(hpo, "generate_hpo_candidates", capture) + frame = _regression_frame(20) + X = frame[["first", "second"]].to_numpy(dtype=np.float64) + y = frame["target"].to_numpy(dtype=np.float64) + source = HPOSource(family="linear", n_trials=1) + + source.get_candidates("tabular_regression", X=X, y=y) + source.get_candidates( + "tabular_regression", + X=X, + y=y, + config=RunConfig(class_weight="balanced"), + ) + + assert captured[0]["class_weight"] == "none" + assert captured[0]["prior_correct"] is True + assert captured[1]["class_weight"] == "balanced" + assert captured[1]["prior_correct"] is False + + +def test_hpo_trials_train_with_the_configured_class_weighting( + monkeypatch: pytest.MonkeyPatch, +) -> None: + from falcon.tabular import candidates + + weighted_calls: list[int] = [] + original = candidates.compute_sample_weight + + def counting_compute_sample_weight(**kwargs: Any) -> Any: + weighted_calls.append(1) + return original(**kwargs) + + monkeypatch.setattr( + candidates, + "compute_sample_weight", + counting_compute_sample_weight, + ) + values = np.linspace(-2.0, 2.0, 40) + frame = pd.DataFrame( + { + "first": values, + "second": np.sin(values), + "target": np.where(values > 0.6, "positive", "negative"), + } + ) + config = RunConfig( + candidate_sources=(HPOSource(family="linear", n_trials=1),), + ensemble_enabled=False, + plateau_enabled=False, + oof_folds=2, + eval_strategy=None, + ) + + Predictor("tabular_classification", config=config).fit( + frame, + features=["first", "second"], + target="target", + ) + assert not weighted_calls + + Predictor( + "tabular_classification", + config=RunConfig( + candidate_sources=config.candidate_sources, + ensemble_enabled=False, + plateau_enabled=False, + oof_folds=2, + eval_strategy=None, + class_weight="balanced", + ), + ).fit(frame, features=["first", "second"], target="target") + assert weighted_calls + + +@pytest.mark.parametrize( + ("options", "message"), + [ + ({"family": ""}, "family"), + ({"family": "linear", "n_trials": 0}, "n_trials"), + ({"family": "linear", "top_n": 0}, "top_n"), + ({"family": "linear", "n_trials": 1, "top_n": 2}, "top_n"), + ({"family": "linear", "time_budget_fraction": 0.0}, "time_budget_fraction"), + ({"family": "linear", "time_budget_fraction": 1.0}, "time_budget_fraction"), + ], +) +def test_hpo_source_rejects_invalid_configuration( + options: dict[str, object], + message: str, +) -> None: + with pytest.raises(ValueError, match=message): + HPOSource(**options) # type: ignore[arg-type] diff --git a/tests/test_hpo_optional.py b/tests/test_hpo_optional.py new file mode 100644 index 0000000..df748a2 --- /dev/null +++ b/tests/test_hpo_optional.py @@ -0,0 +1,26 @@ +from typing import Any + +import numpy as np +import pytest + +from falcon.config import HPOSource +from falcon.tabular import hpo + + +def test_hpo_source_without_optuna_has_install_hint( + monkeypatch: pytest.MonkeyPatch, +) -> None: + def unavailable(module_name: str) -> Any: + if module_name == "optuna": + raise ModuleNotFoundError("No module named 'optuna'", name="optuna") + raise AssertionError(f"Unexpected import: {module_name}") + + monkeypatch.setattr(hpo, "import_module", unavailable) + + with pytest.raises(ImportError, match=r"pip install falcon-ml\[hpo\]"): + HPOSource(family="linear", n_trials=1).get_candidates( + "tabular_regression", + X=np.arange(16, dtype=np.float32).reshape(8, 2), + y=np.arange(8, dtype=np.float32), + groups=np.arange(8), + ) diff --git a/tests/test_ingestion.py b/tests/test_ingestion.py new file mode 100644 index 0000000..bb0064d --- /dev/null +++ b/tests/test_ingestion.py @@ -0,0 +1,162 @@ +from __future__ import annotations + +import json +import logging +import tarfile +from io import BytesIO +from pathlib import Path + +import numpy as np +import pandas as pd +import pytest + +from falcon import Predictor +from falcon.config import PortfolioSource, RunConfig +from falcon.tabular.candidates import EstimatorSpec +from falcon.tabular.ingestion import ingest_data +from falcon.type_guessing import determine_column_types +from falcon.types import ColumnTypes +from tests.fnnx_conformance import extract_fnnx_graph + + +def _training_frame() -> pd.DataFrame: + return pd.DataFrame( + { + "amount": np.arange(12, dtype=np.float64), + "category": [f"group-{index % 3}" for index in range(12)], + "outcome": [index % 2 for index in range(12)], + } + ) + + +def _assert_named_schema(data: pd.DataFrame | str) -> None: + X, y, schema = ingest_data(data, task="tabular_classification") + + assert X.shape == (12, 2) + assert y.shape == (12,) + assert schema.column_names == ("amount", "category") + assert schema.column_types == ( + ColumnTypes.NUMERIC_REGULAR, + ColumnTypes.CAT_LOW_CARD, + ) + assert schema.target_name == "outcome" + assert schema.target_kind == "classification" + assert schema.dimensions == (12, 2) + + +def test_ingestion_produces_schema_for_dataframe_and_file_inputs( + tmp_path: Path, +) -> None: + frame = _training_frame() + csv_path = tmp_path / "training.csv" + parquet_path = tmp_path / "training.parquet" + frame.to_csv(csv_path, index=False) + frame.to_parquet(parquet_path, index=False) + + _assert_named_schema(frame) + _assert_named_schema(str(csv_path)) + _assert_named_schema(str(parquet_path)) + + +def test_ingestion_produces_schema_for_array_and_tuple_inputs() -> None: + frame = _training_frame() + combined = frame.to_numpy() + dataframe_tuple = (frame[["amount", "category"]], frame["outcome"]) + array_tuple = (combined[:, :2], combined[:, 2]) + + X, y, array_schema = ingest_data(combined, task="tabular_classification") + dataframe_X, dataframe_y, dataframe_schema = ingest_data( + dataframe_tuple, task="tabular_classification" + ) + tuple_X, tuple_y, tuple_schema = ingest_data( + array_tuple, task="tabular_classification" + ) + + assert X.shape == dataframe_X.shape == tuple_X.shape == (12, 2) + assert y.shape == dataframe_y.shape == tuple_y.shape == (12,) + assert array_schema.column_names == ("feature_0", "feature_1") + assert tuple_schema.column_names == ("feature_0", "feature_1") + assert dataframe_schema.column_names == ("amount", "category") + assert dataframe_schema.target_name == "outcome" + assert array_schema.column_types == dataframe_schema.column_types + assert tuple_schema.column_types == dataframe_schema.column_types + + +def test_type_guessing_ignores_missing_values() -> None: + numeric = np.asarray([[*range(11), np.nan]], dtype=np.float64).T + dates = np.asarray([["2024-01-01"], [None], ["2024-03-15"]], dtype=np.object_) + + assert determine_column_types(numeric) == [ColumnTypes.NUMERIC_REGULAR] + assert determine_column_types(dates) == [ColumnTypes.DATE_YMD_ISO8601] + + +def test_ingestion_drops_missing_targets_and_keeps_missing_feature_rows( + caplog: pytest.LogCaptureFixture, +) -> None: + frame = pd.DataFrame( + { + "numeric": [*range(12), np.nan, 13], + "date": ["2024-01-01"] * 14, + "target": [*[index % 2 for index in range(13)], np.nan], + } + ) + + with caplog.at_level(logging.INFO, logger="falcon"): + X, y, schema = ingest_data(frame, task="tabular_classification") + + assert X.shape == (13, 2) + assert y.shape == (13,) + assert schema.dimensions == (13, 2) + assert schema.column_types == ( + ColumnTypes.NUMERIC_REGULAR, + ColumnTypes.DATE_YMD_ISO8601, + ) + assert pd.isna(X[:, 0]).sum() == 1 + assert "Dropped 1 row with a missing target" in caplog.text + + +def test_saved_artifact_embeds_schema_and_uses_sanitized_unique_input_names() -> None: + sample_indices = np.arange(24, dtype=np.float64) + frame = pd.DataFrame( + { + "feature-a": sample_indices, + "feature a": sample_indices**2, + "target value": sample_indices * 0.5, + } + ) + config = RunConfig( + candidate_sources=( + PortfolioSource( + specs=( + EstimatorSpec( + "schema-test", + "hist_gradient_boosting", + {"max_iter": 8, "min_samples_leaf": 2}, + ), + ) + ), + ), + ensemble_enabled=False, + eval_strategy=None, + ) + predictor = Predictor("tabular_regression", config=config).fit(frame) + + bundle = predictor.save() + graph = extract_fnnx_graph(bundle) + with tarfile.open(fileobj=BytesIO(bundle), mode="r:") as archive: + manifest_member = archive.extractfile("manifest.json") + assert manifest_member is not None + manifest = json.load(manifest_member) + + assert [input_.name for input_ in graph.model.graph.input] == [ + "feature_a", + "feature_a_1", + ] + assert manifest["schema"] == { + "columns": [ + {"name": "feature-a", "type": "NUMERIC_REGULAR"}, + {"name": "feature a", "type": "NUMERIC_REGULAR"}, + ], + "target": {"name": "target value", "kind": "regression"}, + "dimensions": {"rows": 24, "features": 2}, + } diff --git a/tests/test_logging.py b/tests/test_logging.py new file mode 100644 index 0000000..7f8549b --- /dev/null +++ b/tests/test_logging.py @@ -0,0 +1,54 @@ +import logging +import os +import subprocess +import sys +from pathlib import Path + +import pytest + +from falcon.utils import logger, set_verbosity_level + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] + + +def test_verbosity_controls_falcon_logger_without_environment_state( + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture +) -> None: + monkeypatch.delenv("FALCON_VERBOSITY_LEVEL", raising=False) + original_level = logger.level + + try: + set_verbosity_level(1) + with caplog.at_level(logging.INFO, logger="falcon"): + logger.info("falcon-visible-info") + + assert logger.level == logging.INFO + assert "falcon-visible-info" in caplog.text + assert "FALCON_VERBOSITY_LEVEL" not in os.environ + + set_verbosity_level(0) + assert logger.level == logging.WARNING + finally: + logger.setLevel(original_level) + + +def test_import_does_not_suppress_python_warnings() -> None: + env = os.environ.copy() + env.pop("PYTHONWARNINGS", None) + code = ( + "import os, warnings; import falcon; " + "warnings.warn('falcon-visible-warning'); " + "print(os.environ.get('PYTHONWARNINGS', ''))" + ) + + result = subprocess.run( + [sys.executable, "-c", code], + cwd=REPOSITORY_ROOT, + env=env, + capture_output=True, + check=True, + text=True, + ) + + assert "falcon-visible-warning" in result.stderr + assert result.stdout.strip() == "" diff --git a/tests/test_main.py b/tests/test_main.py index 8916f4f..a8d3fe8 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,16 +1,53 @@ -from falcon import initialize -from falcon.tabular.tabular_manager import TabularTaskManager +import subprocess +import sys +from importlib import import_module + import pytest +import falcon +import falcon.tabular -def test_initialize(): - manager1 = initialize( - data="tests/extra_files/iris.csv", task="tabular_classification" - ) - manager2 = initialize( - data="tests/extra_files/prices.csv", task="tabular_regression" + +def test_initialize_is_not_public_api() -> None: + assert not hasattr(falcon, "initialize") + + +def test_public_import_does_not_load_the_legacy_training_path() -> None: + code = ( + "import sys; import falcon; " + "blocked = ('super_learner', 'optuna_learner', 'tabular_manager', " + "'task_configurations', 'tabular.configurations', 'models.stacking', " + "'abstract.optuna'); " + "assert not any(any(part in name for part in blocked) for name in sys.modules)" ) - assert isinstance(manager1, TabularTaskManager) - assert isinstance(manager2, TabularTaskManager) - with pytest.raises(ValueError, match="Unknown task"): - manager1 = initialize(data="tests/extra_files/iris.csv", task="unknown_task") + + subprocess.run([sys.executable, "-c", code], check=True) + + +def test_tabular_task_manager_is_not_public_api() -> None: + assert not hasattr(falcon.tabular, "TabularTaskManager") + + +@pytest.mark.parametrize( + "module_name", + [ + "falcon.abstract.optuna", + "falcon.abstract.task_manager", + "falcon.addons.sklearn.ensemble.balanced_stacking", + "falcon.addons.sklearn.model_selection.balanced_strat_kfold", + "falcon.tabular.configurations", + "falcon.tabular.learners.optuna_learner", + "falcon.tabular.learners.plain_learner", + "falcon.tabular.learners.super_learner", + "falcon.tabular.models.hist_gbt", + "falcon.tabular.models.stacking", + "falcon.tabular.reporting", + "falcon.tabular.tabular_manager", + "falcon.tabular.utils", + "falcon.tabular.wrappers", + "falcon.task_configurations", + ], +) +def test_legacy_modules_are_removed(module_name: str) -> None: + with pytest.raises(ModuleNotFoundError): + import_module(module_name) diff --git a/tests/test_pipeline_steps.py b/tests/test_pipeline_steps.py new file mode 100644 index 0000000..f836e70 --- /dev/null +++ b/tests/test_pipeline_steps.py @@ -0,0 +1,116 @@ +from typing import Any, cast + +import numpy as np +import pytest +from numpy import typing as npt + +import falcon.abstract as abstract +from falcon.abstract import Pipeline, PipelineStep +from falcon.config import RunConfig +from falcon.serialization import SerializedModelRepr +from falcon.tabular.processors.label_decoder import LabelDecoder +from falcon.tabular.processors.scaler_and_encoder import ScalerAndEncoder +from falcon.tabular.training import CandidateLearner +from falcon.types import ColumnTypes, DatasetSchema + + +class _RecordingStep: + def __init__(self, input_type: object, output_type: object, offset: float) -> None: + self.input_type = input_type + self.output_type = output_type + self.offset = offset + self.fit_input: npt.NDArray[Any] | None = None + self.fit_schema: DatasetSchema | None = None + self.fit_groups: npt.NDArray[Any] | None = None + + def fit( + self, + X: npt.NDArray[Any], + y: npt.NDArray[Any], + schema: DatasetSchema, + *, + groups: npt.ArrayLike | None = None, + ) -> None: + self.fit_input = X.copy() + self.fit_schema = schema + self.fit_groups = None if groups is None else np.asarray(groups).copy() + + def transform(self, X: npt.NDArray[Any]) -> npt.NDArray[Any]: + return X + self.offset + + def serialize(self) -> SerializedModelRepr: + raise NotImplementedError + + def get_input_type(self) -> object: + return self.input_type + + def get_output_type(self) -> object: + return self.output_type + + +class _IncompleteStep: + def transform(self, X: npt.NDArray[Any]) -> npt.NDArray[Any]: + return X + + +def _schema() -> DatasetSchema: + return DatasetSchema( + column_names=("value",), + column_types=(ColumnTypes.NUMERIC_REGULAR,), + target_name="target", + target_kind="regression", + dimensions=(3, 1), + ) + + +def test_pipeline_fits_steps_with_schema_and_composes_transforms() -> None: + schema = _schema() + X = np.asarray([[1.0], [2.0], [3.0]]) + y = np.asarray([1.0, 2.0, 3.0]) + groups = np.asarray([0, 0, 1]) + first = _RecordingStep("raw", "encoded", 2.0) + second = _RecordingStep("encoded", "prediction", 3.0) + pipeline = Pipeline("tabular_regression", X.shape, schema) + pipeline.add_step(first) + pipeline.add_step(second) + + pipeline.fit(X, y, schema, groups=groups) + + assert first.fit_schema is schema + assert second.fit_schema is schema + assert first.fit_input is not None + assert second.fit_input is not None + assert first.fit_groups is not None + assert second.fit_groups is not None + assert np.array_equal(first.fit_input, X) + assert np.array_equal(second.fit_input, X + 2.0) + assert np.array_equal(first.fit_groups, groups) + assert np.array_equal(second.fit_groups, groups) + assert np.array_equal(pipeline.predict(X), X + 5.0) + + +def test_pipeline_rejects_objects_that_do_not_implement_step_protocol() -> None: + pipeline = Pipeline("tabular_regression", (3, 1), _schema()) + + with pytest.raises(TypeError, match="Pipeline steps must implement"): + pipeline.add_step(cast(Any, _IncompleteStep())) + + +@pytest.mark.parametrize( + "step", + [ + ScalerAndEncoder(), + CandidateLearner("tabular_regression", (3, 1), RunConfig()), + LabelDecoder(), + ], +) +def test_existing_steps_use_the_single_protocol_without_legacy_aliases( + step: PipelineStep, +) -> None: + assert isinstance(step, PipelineStep) + assert not hasattr(step, "fit_pipe") + assert not hasattr(step, "forward") + assert not hasattr(step, "to_onnx") + assert not hasattr(abstract, "Model") + assert not hasattr(abstract, "Learner") + assert not hasattr(abstract, "Processor") diff --git a/tests/test_predictor.py b/tests/test_predictor.py new file mode 100644 index 0000000..8a1e230 --- /dev/null +++ b/tests/test_predictor.py @@ -0,0 +1,469 @@ +from __future__ import annotations + +import time +from pathlib import Path +from typing import Any + +import numpy as np +import pandas as pd +import pytest +from numpy import typing as npt + +import falcon +from falcon import AutoML, Predictor +from falcon.config import PortfolioSource, RunConfig +from falcon.runtime import Runtime +from falcon.tabular.candidates import EstimatorSpec +from falcon.types import ColumnTypes +from tests.fnnx_conformance import assert_fnnx_conforms, extract_fnnx_graph + + +def _linear_config( + *, + task: str, + eval_strategy: str | None = None, + ensemble: bool = False, +) -> RunConfig: + parameters: dict[str, object] + if task == "tabular_classification": + parameters = {"C": 1.0, "max_iter": 200} + else: + parameters = {"alpha": 1.0} + specs = [EstimatorSpec("linear-primary", "linear", parameters)] + if ensemble: + secondary = {**parameters, "C": 0.5} if "C" in parameters else {"alpha": 2.0} + specs.append(EstimatorSpec("linear-secondary", "linear", secondary)) + return RunConfig( + candidate_sources=(PortfolioSource(specs=tuple(specs)),), + ensemble_enabled=ensemble, + ensemble_max_iterations=5, + plateau_enabled=False, + oof_folds=3, + eval_strategy=eval_strategy, + ) + + +def _classification_frame(n_rows: int = 90) -> pd.DataFrame: + rng = np.random.default_rng(42) + first = rng.normal(size=n_rows) + second = rng.normal(size=n_rows) + return pd.DataFrame( + { + "first": first, + "second": second, + "target": np.where(first + 0.2 * second > 0, "yes", "no"), + } + ) + + +def _mixed_frame(task: str) -> tuple[pd.DataFrame, list[str]]: + sample_count = 128 + sample_indices = np.arange(sample_count) + features: dict[str, npt.NDArray[Any]] = { + "numeric": np.linspace(-4.0, 7.0, sample_count).astype(np.object_), + "categorical_low": np.asarray( + [f"group-{index % 4}" for index in sample_indices], dtype=np.object_ + ), + "categorical_high": np.asarray( + [f"category-{index:03d}" for index in sample_indices], dtype=np.object_ + ), + "date": np.asarray( + ( + pd.Timestamp("2020-01-01") + pd.to_timedelta(sample_indices, unit="D") + ).strftime("%Y-%m-%d"), + dtype=np.object_, + ), + "datetime": np.asarray( + ( + pd.Timestamp("2020-01-01") + pd.to_timedelta(sample_indices, unit="h") + ).strftime("%Y-%m-%dT%H:%M:%SZ"), + dtype=np.object_, + ), + "text": np.asarray( + [ + "falcon document sample " + f"{index} contains several useful words about " + f"{'alpha' if index % 2 else 'beta'}" + for index in sample_indices + ], + dtype=np.object_, + ), + } + for values in features.values(): + values[[5, 37]] = np.nan + if task == "tabular_classification": + target: npt.NDArray[Any] = np.where( + sample_indices % 2, "positive", "negative" + ).astype(np.object_) + else: + target = (0.4 * sample_indices + np.sin(sample_indices)).astype(np.object_) + target[-1] = np.nan + return pd.DataFrame({**features, "target": target}), list(features) + + +def test_predictor_is_the_public_api_and_holdout_does_not_mutate_training_data() -> ( + None +): + frame = _classification_frame() + predictor = Predictor( + "tabular_classification", + config=_linear_config(task="tabular_classification"), + eval_strategy="holdout", + random_state=7, + ) + + result = predictor.fit(frame, features=["first", "second"], target="target") + + assert result is predictor + assert falcon.Predictor is Predictor + assert predictor._training_data is not None + assert predictor._fit_indices is not None + assert predictor._eval_indices is not None + assert predictor._training_data.X.shape == (len(frame), 2) + assert len(predictor._fit_indices) + len(predictor._eval_indices) == len(frame) + assert set(predictor._training_data.groups[predictor._fit_indices]).isdisjoint( + predictor._training_data.groups[predictor._eval_indices] + ) + assert set(predictor._performance_metrics) == {"train", "eval"} + + +@pytest.mark.parametrize( + ("task", "preset"), + [ + pytest.param("tabular_classification", "balanced", id="classification"), + pytest.param("tabular_regression", "fast", id="regression"), + ], +) +def test_mixed_type_predictor_round_trip( + task: str, + preset: str, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + frame, features = _mixed_frame(task) + with caplog.at_level("INFO", logger="falcon"): + predictor = Predictor( + task, + preset=preset, + eval_strategy=None, + random_state=17, + ).fit(frame, features=features, target="target") + assert predictor._training_data is not None + assert predictor._training_data.X.shape == (len(frame) - 1, len(features)) + assert pd.isna(predictor._training_data.X).any() + assert predictor._training_data.schema.column_types == ( + ColumnTypes.NUMERIC_REGULAR, + ColumnTypes.CAT_LOW_CARD, + ColumnTypes.CAT_HIGH_CARD, + ColumnTypes.DATE_YMD_ISO8601, + ColumnTypes.DATETIME_YMDHMS_ISO8601, + ColumnTypes.TEXT_UTF8, + ) + assert "Dropped 1 row with a missing target" in caplog.text + + inputs = frame.loc[[5, 37, 0], features].copy() + inputs.loc[0, "categorical_low"] = "Z" + inputs.loc[0, "categorical_high"] = "Z" + artifact_path = tmp_path / f"mixed-{task}.fnnx" + bundle = predictor.save(artifact_path) + runtime = Runtime(str(artifact_path)) + + assert bundle == artifact_path.read_bytes() + assert predictor.evaluate(frame)["N_SAMPLES"] == len(frame) - 1 + assert_fnnx_conforms(extract_fnnx_graph(bundle)) + if task == "tabular_classification": + np.testing.assert_array_equal( + runtime.predict(inputs), predictor.predict(inputs) + ) + native_probabilities = predictor.predict_proba(inputs) + np.testing.assert_allclose( + runtime.predict_proba(inputs), + native_probabilities, + rtol=1e-5, + atol=1e-6, + ) + np.testing.assert_allclose( + native_probabilities.sum(axis=1), + 1.0, + atol=1e-6, + ) + else: + assert len(predictor.leaderboard()) == 1 + np.testing.assert_allclose( + runtime.predict(inputs), + predictor.predict(inputs), + rtol=1e-5, + atol=1e-4, + ) + + +def test_predictor_classification_methods_and_artifact_probabilities_match( + tmp_path: Path, +) -> None: + frame = _classification_frame() + predictor = Predictor( + "tabular_classification", + config=_linear_config( + task="tabular_classification", + ensemble=True, + ), + ).fit(frame, features=["first", "second"], target="target") + inputs = frame[["first", "second"]].iloc[:12] + + native_predictions = predictor.predict(inputs) + native_probabilities = predictor.predict_proba(inputs) + evaluation = predictor.evaluate(frame) + leaderboard = predictor.leaderboard() + artifact_path = tmp_path / "predictor.fnnx" + bundle = predictor.save(artifact_path) + runtime = Runtime(str(artifact_path)) + + assert evaluation["N_SAMPLES"] == len(frame) + assert list(leaderboard["candidate"]) == [ + "linear-primary", + "linear-secondary", + ] + assert len(predictor.feature_importance(n_repeats=2)) == 2 + assert bundle == artifact_path.read_bytes() + np.testing.assert_array_equal(runtime.predict(inputs), native_predictions) + np.testing.assert_allclose( + runtime.predict_proba(inputs), + native_probabilities, + rtol=1e-5, + atol=1e-6, + ) + np.testing.assert_allclose(native_probabilities.sum(axis=1), 1.0, atol=1e-6) + + +def test_time_budget_keeps_the_first_candidate_usable( + caplog: pytest.LogCaptureFixture, +) -> None: + X = np.arange(120, dtype=np.float64).reshape(60, 2) + y = 3 * X[:, 0] - X[:, 1] + config = _linear_config( + task="tabular_regression", + eval_strategy=None, + ensemble=True, + ) + time_limit = float(np.finfo(float).eps) + + started_at = time.monotonic() + with caplog.at_level("INFO", logger="falcon"): + predictor = Predictor( + "tabular_regression", + config=config, + time_limit=time_limit, + ).fit((X, y)) + elapsed = time.monotonic() - started_at + leaderboard = predictor.leaderboard() + + assert len(leaderboard) == 1 + assert elapsed <= float(leaderboard.iloc[0]["fit_time"]) + time_limit + 2.0 + assert "time limit is insufficient for the first candidate" in caplog.text + assert predictor.predict(X[:4]).shape == (4,) + assert predictor.save() + + +def test_balanced_preset_stops_on_a_reproducible_plateau( + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, +) -> None: + from falcon.tabular import candidates + + specs = tuple( + EstimatorSpec(f"linear-{index}", "linear", {"alpha": float(index + 1)}) + for index in range(4) + ) + monkeypatch.setattr( + candidates, + "default_portfolio", + lambda task, n_classes=None: specs, + ) + X = np.arange(120, dtype=np.float64).reshape(60, 2) + y = np.zeros(len(X), dtype=np.float64) + + def fit_predictor() -> Predictor: + return Predictor( + "tabular_regression", + preset="balanced", + eval_strategy=None, + random_state=13, + ).fit((X, y)) + + with caplog.at_level("INFO", logger="falcon"): + first = fit_predictor() + second = fit_predictor() + + first_candidates = list(first.leaderboard()["candidate"]) + second_candidates = list(second.leaderboard()["candidate"]) + assert first_candidates == second_candidates + assert len(first_candidates) < len(specs) + assert "OOF score plateau" in caplog.text + np.testing.assert_array_equal(first.predict(X), second.predict(X)) + assert first.save() + + +def test_predictor_is_reproducible_under_a_fixed_seed() -> None: + frame = _classification_frame() + config = _linear_config( + task="tabular_classification", + eval_strategy=None, + ensemble=True, + ) + + predictions = [ + Predictor( + "tabular_classification", + config=config, + random_state=19, + ) + .fit(frame, features=["first", "second"], target="target") + .predict(frame[["first", "second"]]) + for _ in range(2) + ] + + np.testing.assert_array_equal(predictions[0], predictions[1]) + + +def test_named_groups_cover_holdout_and_oof_splits_and_remain_features( + monkeypatch: pytest.MonkeyPatch, +) -> None: + from falcon.tabular import candidates + + spec = EstimatorSpec("linear", "linear", {"max_iter": 200}) + monkeypatch.setattr( + candidates, + "default_portfolio", + lambda task, n_classes=None: (spec,), + ) + original_out_of_fold_indices = candidates.out_of_fold_indices + captured_splits: list[ + tuple[ + npt.NDArray[np.int64], + list[tuple[npt.NDArray[np.int64], npt.NDArray[np.int64]]], + ] + ] = [] + + def recording_out_of_fold_indices( + X: npt.NDArray[Any], + y: npt.NDArray[Any], + task: str, + groups: npt.ArrayLike | None = None, + *, + n_splits: int = 5, + random_state: int = 42, + ) -> list[tuple[npt.NDArray[np.int64], npt.NDArray[np.int64]]]: + splits = original_out_of_fold_indices( + X, + y, + task, + groups, + n_splits=n_splits, + random_state=random_state, + ) + assert groups is not None + captured_splits.append((np.asarray(groups).copy(), splits)) + return splits + + monkeypatch.setattr( + candidates, + "out_of_fold_indices", + recording_out_of_fold_indices, + ) + account_ids = np.repeat(np.arange(12), 3) + frame = pd.DataFrame( + { + "account": [f"account-{account_id}" for account_id in account_ids], + "value": np.arange(len(account_ids), dtype=np.float64), + "target": np.where(account_ids % 2, "positive", "negative"), + } + ) + + predictor = Predictor( + "tabular_classification", + preset="balanced", + eval_strategy="holdout", + ).fit(frame, target="target", group_by="account") + + assert predictor._training_data is not None + assert predictor._fit_indices is not None + assert predictor._eval_indices is not None + training_data = predictor._training_data + assert training_data.schema.column_names == ("account", "value") + assert set(training_data.groups[predictor._fit_indices]).isdisjoint( + training_data.groups[predictor._eval_indices] + ) + assert len(captured_splits) == 1 + oof_groups, splits = captured_splits[0] + for train_indices, eval_indices in splits: + assert set(oof_groups[train_indices]).isdisjoint(oof_groups[eval_indices]) + assert predictor.predict(frame[["account", "value"]].iloc[:3]).shape == (3,) + + +def test_no_evaluation_strategy_uses_all_rows_and_still_predicts_and_saves() -> None: + X = np.arange(120, dtype=np.float64).reshape(60, 2) + y = 3 * X[:, 0] - X[:, 1] + predictor = Predictor( + "tabular_regression", + config=_linear_config(task="tabular_regression", eval_strategy=None), + ).fit((X, y)) + + assert predictor._fit_indices is not None + assert len(predictor._fit_indices) == len(X) + assert set(predictor._performance_metrics) == {"train"} + assert predictor.predict(X[:4]).shape == (4,) + assert predictor.save() + assert len(predictor.feature_importance(n_repeats=2)) == X.shape[1] + with pytest.raises(RuntimeError, match="classification"): + predictor.predict_proba(X[:4]) + + +def test_automl_dynamic_evaluation_uses_test_data_and_returns_predictor() -> None: + frame = _classification_frame() + train = frame.iloc[:70].copy() + test = frame.iloc[70:].copy() + + predictor = AutoML( + task="tabular_classification", + train_data=train, + test_data=test, + features=["first", "second"], + target="target", + config=_linear_config( + task="tabular_classification", + eval_strategy="holdout", + ), + save_model=False, + ) + + assert isinstance(predictor, Predictor) + assert predictor.config.eval_strategy is None + assert set(predictor._performance_metrics) == {"train", "test"} + + +def test_removed_automl_kwargs_and_legacy_presets_have_migration_errors() -> None: + frame = _classification_frame(30) + with pytest.raises(TypeError) as error: + AutoML( + task="tabular_classification", + train_data=frame, + manager_configuration={}, + save_model=False, + ) + + message = str(error.value) + assert "manager_configuration" in message + assert "removed" in message + assert "RunConfig" in message + + with pytest.raises(TypeError, match="manager_configuration.*removed"): + Predictor( + "tabular_classification", + manager_configuration={}, + ) + + with pytest.raises(ValueError) as preset_error: + Predictor("tabular_classification", preset="SuperLearner") + assert all( + preset in str(preset_error.value) for preset in ("fast", "balanced", "best") + ) diff --git a/tests/test_repository_configuration.py b/tests/test_repository_configuration.py new file mode 100644 index 0000000..cab207b --- /dev/null +++ b/tests/test_repository_configuration.py @@ -0,0 +1,309 @@ +import importlib.util +import json +import subprocess +import sys +from pathlib import Path, PurePosixPath +from typing import Any + +if sys.version_info >= (3, 11): + import tomllib +else: + import tomli as tomllib + +import falcon + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] + + +def _matrix_entries() -> list[dict[str, object]]: + payload = json.loads( + (REPOSITORY_ROOT / "ci/matrix.json").read_text(encoding="utf-8") + ) + return payload["include"] + + +def test_ci_runs_supported_python_versions_with_runtime() -> None: + entries = _matrix_entries() + locked = { + entry["python"] + for entry in entries + if entry["resolution"] == "locked" and entry.get("extras") == ["runtime"] + } + + assert locked == {"3.10", "3.11", "3.12", "3.13"} + assert all( + entry["resolution"] in {"locked", "lowest-direct", "highest"} + for entry in entries + ) + + +def test_ci_matrix_covers_the_dependency_floor_and_ceiling() -> None: + """Both bounds have to be exercised, not just the pinned middle. + + Every job used to run `uv sync --locked`, so the whole matrix tested one dependency + set. That is how a protobuf release broke installs while CI stayed green. + """ + resolutions = {entry["name"]: entry["resolution"] for entry in _matrix_entries()} + + assert "lowest-direct" in resolutions.values() + assert "highest" in resolutions.values() + + ceiling = next( + entry for entry in _matrix_entries() if entry["resolution"] == "highest" + ) + assert ceiling.get("allow_failure") is True + + +def test_ci_workflow_runs_the_matrix_through_the_local_entry_point() -> None: + """CI and `python scripts/run_matrix.py` must not be able to drift apart.""" + workflow = (REPOSITORY_ROOT / ".github/workflows/tests.yml").read_text( + encoding="utf-8" + ) + + assert "jq -c '.include' ci/matrix.json" in workflow + assert "python scripts/run_matrix.py ${{ matrix.name }}" in workflow + assert (REPOSITORY_ROOT / "scripts/run_matrix.py").is_file() + + +def _load_matrix_runner() -> Any: + spec = importlib.util.spec_from_file_location( + "falcon_run_matrix", REPOSITORY_ROOT / "scripts/run_matrix.py" + ) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + # dataclasses resolves field types through sys.modules[cls.__module__]. + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +def test_every_entry_lints_and_typechecks_in_its_own_environment() -> None: + """mypy's verdict depends on the installed stubs, not just on the source. + + numpy 2.2 types `np.arange` as strictly 1-D where 2.5 does not, so one shared + environment cannot speak for the whole support range: the checks belong to the + entries. A standalone job would report green while an entry was broken. + """ + runner = _load_matrix_runner() + workflow = (REPOSITORY_ROOT / ".github/workflows/tests.yml").read_text( + encoding="utf-8" + ) + venv = Path("/tmp/venv") + + for item in _matrix_entries(): + entry = runner.Entry( + name=str(item["name"]), + python=str(item["python"]), + resolution=str(item["resolution"]), + extras=tuple(item.get("extras", ())), # type: ignore[arg-type] + ) + stages = dict(entry.quality_commands(venv)) + + assert set(stages) == {"format", "lint", "mypy"} + assert stages["mypy"][1:] == ["-m", "mypy", "falcon"] + assert stages["lint"][1:] == ["-m", "ruff", "check", "falcon", "tests"] + if entry.resolution != "locked": + installed = " ".join(sum(entry.install_commands(venv), [])) + assert "mypy" in installed and "ruff" in installed + + steps = [line for line in workflow.splitlines() if not line.strip().startswith("#")] + assert [line for line in steps if "ruff" in line or "mypy" in line] == [] + + +def test_runtime_extra_pins_string_op_compatible_onnxruntime() -> None: + configuration = tomllib.loads( + (REPOSITORY_ROOT / "pyproject.toml").read_text(encoding="utf-8") + ) + + requirements = configuration["project"]["optional-dependencies"]["runtime"] + assert any( + requirement.startswith("onnxruntime>=1.18.1") for requirement in requirements + ) + + +def test_gbdt_extra_and_ci_parity_job_are_configured() -> None: + configuration = tomllib.loads( + (REPOSITORY_ROOT / "pyproject.toml").read_text(encoding="utf-8") + ) + requirements = configuration["project"]["optional-dependencies"]["gbdt"] + dependency_names = { + requirement.split(">", maxsplit=1)[0].split("=", maxsplit=1)[0] + for requirement in requirements + } + + assert dependency_names == {"lightgbm", "xgboost", "catboost", "onnxmltools"} + assert any( + entry.get("extras") == ["runtime", "gbdt"] for entry in _matrix_entries() + ) + + +def test_hpo_extra_contains_optuna_and_tqdm() -> None: + configuration = tomllib.loads( + (REPOSITORY_ROOT / "pyproject.toml").read_text(encoding="utf-8") + ) + requirements = configuration["project"]["optional-dependencies"]["hpo"] + dependency_names = { + requirement.split(">", maxsplit=1)[0].split("=", maxsplit=1)[0] + for requirement in requirements + } + + assert dependency_names == {"optuna", "tqdm"} + assert any(entry.get("extras") == ["runtime", "hpo"] for entry in _matrix_entries()) + + +def test_release_metadata_and_slimmed_core_dependencies() -> None: + configuration = tomllib.loads( + (REPOSITORY_ROOT / "pyproject.toml").read_text(encoding="utf-8") + ) + dependency_names = { + requirement.split(">", maxsplit=1)[0] + .split("<", maxsplit=1)[0] + .split("=", maxsplit=1)[0] + for requirement in configuration["project"]["dependencies"] + } + + assert configuration["project"]["version"] == "1.0.0" + assert falcon.__version__ == "1.0.0" + assert dependency_names == { + "numpy", + "onnx", + "pandas", + "protobuf", + "pyarrow", + "scikit-learn", + "scipy", + "skl2onnx", + } + assert set(configuration["project"]["optional-dependencies"]) == { + "gbdt", + "hpo", + "runtime", + } + + +def test_the_lockfile_records_the_released_version() -> None: + """A stale lock breaks every `uv sync --locked` CI leg, not just packaging. + + `uv.lock` pins the project's own version, so bumping `pyproject.toml` without + re-running `uv lock` fails the install stage before a single test runs. + """ + configuration = tomllib.loads( + (REPOSITORY_ROOT / "pyproject.toml").read_text(encoding="utf-8") + ) + lock = tomllib.loads((REPOSITORY_ROOT / "uv.lock").read_text(encoding="utf-8")) + + locked = next( + package for package in lock["package"] if package["name"] == "falcon-ml" + ) + + assert locked["version"] == configuration["project"]["version"] + + +def test_protobuf_is_capped_below_the_bool_attribute_rejection() -> None: + """Guards the pin that keeps tree export working. + + skl2onnx emits Python bools into `nodes_missing_value_tracks_true`, an int64 + attribute. protobuf 7.34 turned that from a warning into a `TypeError`, which fails + every tree model export, so the cap has to hold until skl2onnx stops doing it. + """ + configuration = tomllib.loads( + (REPOSITORY_ROOT / "pyproject.toml").read_text(encoding="utf-8") + ) + requirement = next( + entry + for entry in configuration["project"]["dependencies"] + if entry.startswith("protobuf") + ) + assert requirement == "protobuf>=4.25.1,<7.34" + + from google.protobuf import __version__ as protobuf_version + + major, minor = (int(part) for part in protobuf_version.split(".")[:2]) + assert (major, minor) < (7, 34) + + +def test_core_skl2onnx_version_imports_without_optuna_transitive_dependencies() -> None: + configuration = tomllib.loads( + (REPOSITORY_ROOT / "pyproject.toml").read_text(encoding="utf-8") + ) + requirement = next( + requirement + for requirement in configuration["project"]["dependencies"] + if requirement.startswith("skl2onnx") + ) + + assert requirement.startswith("skl2onnx>=1.20.0") + + subprocess.run( + [ + sys.executable, + "-c", + """ +import importlib.abc +import sys + + +class PackagingImportBlocker(importlib.abc.MetaPathFinder): + def find_spec(self, fullname, path=None, target=None): + if fullname == "packaging" or fullname.startswith("packaging."): + raise ModuleNotFoundError("packaging is unavailable") + return None + + +sys.meta_path.insert(0, PackagingImportBlocker()) +import skl2onnx +""", + ], + check=True, + ) + + +def test_benchmarks_are_excluded_from_distribution_and_ci() -> None: + configuration = tomllib.loads( + (REPOSITORY_ROOT / "pyproject.toml").read_text(encoding="utf-8") + ) + package_finder = configuration["tool"]["setuptools"]["packages"]["find"] + + assert package_finder["include"] == ["falcon*"] + assert "benchmarks*" in package_finder["exclude"] + + workflow = (REPOSITORY_ROOT / ".github/workflows/tests.yml").read_text( + encoding="utf-8" + ) + assert "benchmarks/run.py" not in workflow + + +def test_local_artifacts_are_untracked_and_ignored() -> None: + tracked_result = subprocess.run( + ["git", "ls-files"], + cwd=REPOSITORY_ROOT, + capture_output=True, + check=True, + text=True, + ) + tracked_paths = [ + PurePosixPath(raw_path) for raw_path in tracked_result.stdout.splitlines() + ] + tracked_artifacts = { + path + for path in tracked_paths + if ( + path.suffix == ".fnnx" + or path.name.startswith("tmp.") + or any(part in {"build", "dist"} for part in path.parts[:-1]) + ) + } + + assert not tracked_artifacts + + artifacts = ("build/package", "dist/package", "tmp.cache", "model.fnnx") + result = subprocess.run( + ["git", "check-ignore", "--stdin"], + cwd=REPOSITORY_ROOT, + input="\n".join(artifacts), + capture_output=True, + check=True, + text=True, + ) + + assert set(result.stdout.splitlines()) == set(artifacts) diff --git a/tests/test_run_config.py b/tests/test_run_config.py new file mode 100644 index 0000000..644e4c6 --- /dev/null +++ b/tests/test_run_config.py @@ -0,0 +1,240 @@ +from __future__ import annotations + +import sys +from dataclasses import FrozenInstanceError +from types import ModuleType + +import pytest + +import falcon +from falcon.config import ( + DATASET_AWARE_ORDERING_DEFAULT, + PortfolioSource, + RunConfig, +) +from falcon.constants import TABULAR_CLASSIFICATION_TASK, TABULAR_REGRESSION_TASK +from falcon.presets import PresetRegistry, resolve_run_config +from falcon.tabular.candidates import EstimatorSpec + + +def _spec(name: str) -> EstimatorSpec: + return EstimatorSpec(name=name, family="hist_gradient_boosting") + + +def test_run_config_is_public_package_api() -> None: + assert falcon.RunConfig is RunConfig + assert RunConfig().dataset_aware_ordering is DATASET_AWARE_ORDERING_DEFAULT + + +def test_run_config_is_frozen_and_normalizes_sequences() -> None: + source = PortfolioSource(specs=[_spec("first"), _spec("second")]) # type: ignore[arg-type] + config = RunConfig(candidate_sources=[source]) + + assert isinstance(source.specs, tuple) + assert isinstance(config.candidate_sources, tuple) + with pytest.raises(FrozenInstanceError): + config.random_state = 7 # type: ignore[misc] + + +@pytest.mark.parametrize( + ("options", "message"), + [ + ({"candidate_sources": []}, "candidate_sources"), + ({"ensemble_max_iterations": 0}, "ensemble_max_iterations"), + ({"plateau_patience": 0}, "plateau_patience"), + ({"plateau_tolerance": -0.1}, "plateau_tolerance"), + ({"plateau_tolerance": False}, "plateau_tolerance"), + ({"oof_folds": 1}, "oof_folds"), + ({"eval_strategy": "invalid"}, "eval_strategy"), + ({"time_limit": 0.0}, "time_limit"), + ({"time_limit": float("inf")}, "time_limit"), + ({"random_state": -1}, "random_state"), + ({"dataset_aware_ordering": 1}, "dataset_aware_ordering"), + ({"calibrate": 1}, "calibrate"), + ({"impute_missing": 1}, "impute_missing"), + ({"conformal_alpha": 0.0}, "conformal_alpha"), + ({"conformal_alpha": 1.0}, "conformal_alpha"), + ({"class_weight": "auto"}, "class_weight"), + ({"decision_metric": "accuracy"}, "decision_metric"), + ], +) +def test_run_config_rejects_invalid_settings( + options: dict[str, object], message: str +) -> None: + with pytest.raises(ValueError, match=message): + RunConfig(**options) # type: ignore[arg-type] + + +def test_imbalance_settings_default_to_opt_in_weighting_and_a_tuned_rule() -> None: + config = RunConfig() + + assert config.class_weight == "none" + assert config.decision_metric == "balanced_accuracy" + assert config.prior_correct + + +@pytest.mark.parametrize( + ("options", "expected"), + [ + ({}, True), + ({"class_weight": "balanced"}, False), + ({"decision_metric": None}, False), + ({"class_weight": "balanced", "decision_metric": None}, False), + ], +) +def test_prior_correction_needs_unweighted_training_and_a_tuned_rule( + options: dict[str, object], + expected: bool, +) -> None: + assert RunConfig(**options).prior_correct is expected # type: ignore[arg-type] + + +def test_replaced_keeps_untouched_defaults_out_of_the_provided_fields() -> None: + config = RunConfig(oof_folds=3).replaced(random_state=7) + + assert config._provided_fields == frozenset({"oof_folds", "random_state"}) + assert config.oof_folds == 3 + assert config.random_state == 7 + + +def test_portfolio_source_resolves_for_the_requested_task( + monkeypatch: pytest.MonkeyPatch, +) -> None: + calls: list[tuple[str, int | None]] = [] + specs = (_spec("first"), _spec("second"), _spec("third")) + + def portfolio( + task: str, *, n_classes: int | None = None + ) -> tuple[EstimatorSpec, ...]: + calls.append((task, n_classes)) + return specs + + monkeypatch.setattr("falcon.tabular.candidates.default_portfolio", portfolio) + + resolved = PortfolioSource(max_candidates=2).get_candidates( + TABULAR_CLASSIFICATION_TASK, + n_classes=3, + ) + + assert resolved == specs[:2] + assert calls == [(TABULAR_CLASSIFICATION_TASK, 3)] + + +@pytest.mark.parametrize("task", [TABULAR_CLASSIFICATION_TASK, TABULAR_REGRESSION_TASK]) +def test_builtin_presets_have_the_expected_training_profiles(task: str) -> None: + fast = PresetRegistry.get_preset(task, "fast") + balanced = PresetRegistry.get_preset(task, "balanced") + best = PresetRegistry.get_preset(task, "best") + + def portfolio(config: RunConfig) -> PortfolioSource: + source = config.candidate_sources[0] + assert isinstance(source, PortfolioSource) + return source + + assert portfolio(fast).max_candidates == 1 + assert not fast.ensemble_enabled + assert not fast.plateau_enabled + + assert balanced.ensemble_enabled + assert balanced.plateau_enabled + assert portfolio(balanced).max_candidates is not None + + assert best.ensemble_enabled + assert not best.plateau_enabled + assert portfolio(best).max_candidates is None + assert best.oof_folds > balanced.oof_folds + assert best.ensemble_max_iterations > balanced.ensemble_max_iterations + + +def test_run_config_resolution_applies_documented_precedence() -> None: + custom = RunConfig( + eval_strategy=None, + random_state=11, + time_limit=30.0, + ) + + resolved = resolve_run_config( + TABULAR_REGRESSION_TASK, + "fast", + config=custom, + eval_strategy="holdout", + random_state=19, + time_limit=5.0, + ) + + assert not resolved.ensemble_enabled + assert resolved.ensemble_max_iterations == 1 + assert resolved.eval_strategy == "holdout" + assert resolved.random_state == 19 + assert resolved.time_limit == 5.0 + + enabled = resolve_run_config( + TABULAR_REGRESSION_TASK, + "fast", + config=RunConfig(ensemble_enabled=True), + ) + assert enabled.ensemble_enabled + + +def test_explicit_none_overrides_config_values() -> None: + resolved = resolve_run_config( + TABULAR_REGRESSION_TASK, + config=RunConfig(eval_strategy="cv", time_limit=30.0), + eval_strategy=None, + time_limit=None, + ) + + assert resolved.eval_strategy is None + assert resolved.time_limit is None + + +@pytest.mark.parametrize( + "legacy_name", + ["SuperLearner", "SuperLearner.mini", "OptunaLearner.hgbt", "PlainLearner"], +) +def test_new_registry_rejects_legacy_presets_with_migration_guidance( + legacy_name: str, +) -> None: + with pytest.raises(ValueError) as error: + PresetRegistry.get_preset(TABULAR_CLASSIFICATION_TASK, legacy_name) + + message = str(error.value) + assert "removed in 0.9" in message + assert all(name in message for name in ("fast", "balanced", "best")) + assert "RunConfig" in message + + +def test_extension_preset_is_discovered_and_registered( + monkeypatch: pytest.MonkeyPatch, +) -> None: + module_name = "falcon_ml_example" + preset_name = "EXAMPLE::tiny" + module = ModuleType(module_name) + extension_config = RunConfig(ensemble_enabled=False) + + def self_register() -> None: + PresetRegistry.register_presets( + TABULAR_REGRESSION_TASK, + {preset_name: extension_config}, + silent=True, + ) + + module.self_register = self_register # type: ignore[attr-defined] + monkeypatch.setitem(sys.modules, module_name, module) + monkeypatch.delenv("FALCON_PREVENT_EXTENSION_AUTO_LOAD", raising=False) + + try: + assert ( + PresetRegistry.get_preset(TABULAR_REGRESSION_TASK, preset_name) + == extension_config + ) + finally: + PresetRegistry._PRESETS[TABULAR_REGRESSION_TASK].pop(preset_name, None) + + +def test_unknown_preset_lists_available_names() -> None: + with pytest.raises(ValueError) as error: + PresetRegistry.get_preset(TABULAR_REGRESSION_TASK, "unknown") + + message = str(error.value) + assert all(name in message for name in ("fast", "balanced", "best")) diff --git a/tests/test_sklapi.py b/tests/test_sklapi.py index 3bce880..f23f9b5 100644 --- a/tests/test_sklapi.py +++ b/tests/test_sklapi.py @@ -1,137 +1,57 @@ -from sklearn.utils.estimator_checks import check_estimator -from sklearn.tree import DecisionTreeClassifier, DecisionTreeRegressor -from sklearn.linear_model import LinearRegression, LogisticRegression +from __future__ import annotations + import numpy as np +import pytest + +from falcon.config import PortfolioSource, RunConfig from falcon.sklapi import ( - FalconRegressor, - FalconTabularRegressor, FalconClassifier, + FalconRegressor, FalconTabularClassifier, + FalconTabularRegressor, ) -from falcon.abstract import Model, ONNXConvertible, Pipeline -from falcon.tabular.pipelines import SimpleTabularPipeline -from falcon.tabular.learners import PlainLearner -from unittest.case import SkipTest - - -class DummyTestPipeline(Pipeline): - def __init__(self, task, learner, learner_kwargs, dataset_size, **kwargs): - mask = kwargs.get("mask", []) - super().__init__(task=task, dataset_size=dataset_size, mask = mask) - self.add_element(learner(task=task, **learner_kwargs)) - - def fit(self, X, y) -> None: - if self.task == "tabular_classification": - y = y.astype(np.str_) - for p in self._pipeline: - p.fit_pipe(X, y) - X = p.forward(X) - - def predict(self, X, *args, **kwargs): - for p in self._pipeline: - X = p.forward(X) - return X - - -class FalconSklModelWrapper(Model, ONNXConvertible): - def __init__(self, model, **kwargs): - self._model = model - self._kwargs = kwargs - # kwargs['random_state'] = 42 - - def fit(self, X, y): - self.model_ = self._model(**self._kwargs) - self.model_.fit(X, y) - - def predict(self, X): - return self.model_.predict(X) - - def to_onnx(self): - pass - - -class FalconR(FalconSklModelWrapper): - def __init__(self, **kwargs): - super().__init__(model=LinearRegression, **kwargs) - - def fit(self, X, y): - self.model_ = self._model(**self._kwargs) - self.model_.fit(X, y) - - def predict(self, X): - return self.model_.predict(X) +from falcon.tabular.candidates import EstimatorSpec - def to_onnx(self): - pass +def _linear_config() -> RunConfig: + return RunConfig( + candidate_sources=( + PortfolioSource(specs=(EstimatorSpec("linear", "linear"),)), + ), + ensemble_enabled=False, + eval_strategy=None, + ) -class FalconC(FalconSklModelWrapper): - def __init__(self, **kwargs): - super().__init__(model=LogisticRegression) +def test_sklapi_regressor_uses_predictor_for_the_regression_task() -> None: + X = np.arange(120, dtype=np.float64).reshape(60, 2) + y = 2 * X[:, 0] - X[:, 1] + estimator = FalconTabularRegressor(preset=_linear_config()) -def _test_regr(est): - tests = check_estimator(est, generate_only=True) - for t in tests: - if t[1].func.__name__ in [ - "check_no_attributes_set_in_init", - "check_fit_score_takes_y", - "check_estimators_fit_returns_self", - "check_estimator_get_tags_default_keys", - "check_regressors_train", - "check_estimators_unfitted", - "check_set_params", - "check_dont_overwrite_parameters", - "check_n_features_in" - ]: - print(t) - try: - t[1](t[0]) - except SkipTest: - pass + result = estimator.fit(X, y) + assert result is estimator + assert estimator.predictor_.task == "tabular_regression" + assert estimator.n_features_in_ == 2 + assert estimator.predict(X[:4]).shape == (4,) + assert FalconRegressor is FalconTabularRegressor -def _test_clf(est): - tests = check_estimator(est, generate_only=True) - for t in tests: - if t[1].func.__name__ in [ - "check_no_attributes_set_in_init", - "check_fit_score_takes_y", - "check_estimators_fit_returns_self", - "check_estimator_get_tags_default_keys", - "check_classification_train", - "check_estimators_unfitted", - "check_set_params", - "check_dont_overwrite_parameters", - "check_n_features_in" - ]: - print(t) - try: - t[1](t[0]) - except SkipTest: - pass +def test_sklapi_classifier_exposes_native_probabilities() -> None: + rng = np.random.default_rng(42) + X = rng.normal(size=(80, 3)) + y = np.where(X[:, 0] > 0, "positive", "negative") + estimator = FalconTabularClassifier(preset=_linear_config()) -def test_skl_regr(): - config = { - "pipeline": DummyTestPipeline, - "extra_pipeline_options": { - "learner": PlainLearner, - "learner_kwargs": {"model_class": FalconR}, - }, - } + estimator.fit(X, y) + probabilities = estimator.predict_proba(X[:5]) - _test_regr(FalconRegressor(config=config, eval_strategy='auto')) - _test_regr(FalconTabularRegressor(config=config, eval_strategy='auto')) + assert estimator.predictor_.task == "tabular_classification" + assert probabilities.shape == (5, 2) + np.testing.assert_allclose(probabilities.sum(axis=1), 1.0, atol=1e-6) + assert FalconClassifier is FalconTabularClassifier -def test_skl_clf(): - config = { - "pipeline": DummyTestPipeline, - "extra_pipeline_options": { - "learner": PlainLearner, - "learner_kwargs": {"model_class": FalconC}, - }, - } - _test_clf(FalconClassifier(config=config, eval_strategy='auto')) - _test_clf(FalconTabularClassifier(config=config, eval_strategy='auto')) +def test_sklapi_replaces_config_with_preset() -> None: + with pytest.raises(TypeError, match="unexpected keyword argument 'config'"): + FalconRegressor(config="PlainLearner") # type: ignore[call-arg] diff --git a/tests/test_type_guessing.py b/tests/test_type_guessing.py index 4e5f5e3..863e827 100644 --- a/tests/test_type_guessing.py +++ b/tests/test_type_guessing.py @@ -1,10 +1,10 @@ -import pandas as pd import numpy as np + from falcon.type_guessing import determine_column_types from falcon.types import ColumnTypes -def test_type_guessing_0(): +def test_type_guessing_0() -> None: data = np.asarray([[1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11]]) type_ = determine_column_types(data)[0] @@ -12,7 +12,7 @@ def test_type_guessing_0(): assert type_ == ColumnTypes.NUMERIC_REGULAR -def test_type_guessing_1_from_num(): +def test_type_guessing_1_from_num() -> None: data = np.asarray([[1], [2], [3], [4], [5], [6], [7], [8], [9], [10]]) type_ = determine_column_types(data)[0] @@ -20,7 +20,7 @@ def test_type_guessing_1_from_num(): assert type_ == ColumnTypes.CAT_LOW_CARD -def test_type_guessing_1(): +def test_type_guessing_1() -> None: a = [[f"cat_{i}"] for i in range(40)] a.append(["cat_1"]) data = np.asarray(a) @@ -30,7 +30,7 @@ def test_type_guessing_1(): assert type_ == ColumnTypes.CAT_LOW_CARD -def test_type_guessing_2(): +def test_type_guessing_2() -> None: a = [[f"cat_{i}"] for i in range(400)] a.append(["cat_1"]) data = np.asarray(a) @@ -40,7 +40,7 @@ def test_type_guessing_2(): assert type_ == ColumnTypes.CAT_HIGH_CARD -def test_type_guessing_3(): +def test_type_guessing_3() -> None: corpus = [ "Removed demands expense account in outward tedious do.", "Particular way thoroughly unaffected projection favourable mrs can projecting own.", @@ -50,20 +50,20 @@ def test_type_guessing_3(): type_ = determine_column_types(np.array(corpus))[0] - assert type_ == ColumnTypes.TEXT_UTF8, 'corpus not detected' + assert type_ == ColumnTypes.TEXT_UTF8, "corpus not detected" not_corpus = [ "Removed demands expense", "Particular way thoroughly unaffected", - "Thirty it matter enable become" - "Drawings offended yet answered jennings.", + "Thirty it matter enable becomeDrawings offended yet answered jennings.", ] type_ = determine_column_types(np.array(not_corpus))[0] - assert type_ != ColumnTypes.TEXT_UTF8, 'not_corpus wrongly detected as text' + assert type_ != ColumnTypes.TEXT_UTF8, "not_corpus wrongly detected as text" + -def test_type_guessing_100(): +def test_type_guessing_100() -> None: data = np.asarray([["2022-02-02"], ["2022-02-25"], ["2022-05-02"]]) type_ = determine_column_types(data)[0] @@ -71,20 +71,20 @@ def test_type_guessing_100(): assert type_ == ColumnTypes.DATE_YMD_ISO8601 -def test_type_guessing_101(): +def test_type_guessing_101() -> None: data = np.asarray( [["2022-02-02T12:13:14Z"], ["2022-02-25T15:16:17Z"], ["2022-05-02T18:19:20Z"]] ) type_ = determine_column_types(data)[0] - assert ( - type_ == ColumnTypes.DATETIME_YMDHMS_ISO8601 - ), r"%Y-%m-%dT%H:%M:%SZ format was not detected" + assert type_ == ColumnTypes.DATETIME_YMDHMS_ISO8601, ( + r"%Y-%m-%dT%H:%M:%SZ format was not detected" + ) data = np.asarray( [["2022-02-02 12:13:14"], ["2022-02-25 15:16:17"], ["2022-05-02 18:19:20"]] ) type_ = determine_column_types(data)[0] - assert ( - type_ == ColumnTypes.DATETIME_YMDHMS_ISO8601 - ), r"%Y-%m-%d %H:%M:%S format was not detected" + assert type_ == ColumnTypes.DATETIME_YMDHMS_ISO8601, ( + r"%Y-%m-%d %H:%M:%S format was not detected" + ) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..96af52b --- /dev/null +++ b/uv.lock @@ -0,0 +1,2584 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version >= '3.13' and python_full_version < '3.15'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", + "python_full_version < '3.11'", +] + +[[package]] +name = "alembic" +version = "1.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/01/a48dab7827ac4421272399f7ed9a2ec17edd12c8bcde4417bd7b6821b71a/alembic-1.19.0.tar.gz", hash = "sha256:6487c612fc719dcfa22b17d2dd5b2b458929641e6aa2f0b65b135727f5e6d501", size = 2069906, upload-time = "2026-08-04T18:57:04.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/79/59ab6f1fe72eee229de91aa393225389a85df12a0fbbbfa264efcf6d7872/alembic-1.19.0-py3-none-any.whl", hash = "sha256:cf839d3849116aab3cc047e09c6968b9bd6b2fde61b6bb7c1e97352fe5503580", size = 265738, upload-time = "2026-08-04T18:57:06.219Z" }, +] + +[[package]] +name = "ast-serialize" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/a9/11851c3e02a3fea2ddc9932d1fdc7d2edaeecc0d2e11bc5f2a7fde2b0934/ast_serialize-0.8.0.tar.gz", hash = "sha256:6c37c43e4004dfb42d321ddedc569dc17ff4259296f3af577c9ea46a809bc010", size = 845638, upload-time = "2026-08-07T11:29:02.152Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/16/6e520b57cd8c75914b38c670ad4593d13c22911e4306cc7165dab8b0789b/ast_serialize-0.8.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3d822605fa7bb326ef868d25fafced7fc660fa46d9b90c02ea86d5e2f5d325f7", size = 863924, upload-time = "2026-08-07T11:27:34.579Z" }, + { url = "https://files.pythonhosted.org/packages/03/e1/48802de9b22a2bcad42ec80601a17e3f69172fe4f590e6311bcc2b323aeb/ast_serialize-0.8.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:2efa40b068197d5efb62655b43baadb842ed71c4958cccd3e8b86a35726f0119", size = 1177662, upload-time = "2026-08-07T11:27:36.196Z" }, + { url = "https://files.pythonhosted.org/packages/38/d4/323438db76bded3a1f3523a3167b8325916b2ddceb2107a330c6ec9fcf4d/ast_serialize-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:db1b957291bca08c7e72f43a12357b2948e20775d970e3fc3dac0aa3160ab725", size = 1167072, upload-time = "2026-08-07T11:27:37.646Z" }, + { url = "https://files.pythonhosted.org/packages/77/82/53c5400b54144b56de8ed7f957fd1ccd97e42482009292ab46121d15f8dd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdc0d5b18ff8fb364e87923e47c0a91d0d69dbcaeaa274591f7fd26892cc3a3a", size = 1225497, upload-time = "2026-08-07T11:27:39.225Z" }, + { url = "https://files.pythonhosted.org/packages/44/5f/36c07327a8b91303fbf1382c7c3e8a2902072dbe1b9546138a5288e75ff0/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9da7330f3e235bf7da89b8d39205c6350fc0c08a85379743f2df9fff87d6d980", size = 1227101, upload-time = "2026-08-07T11:27:40.799Z" }, + { url = "https://files.pythonhosted.org/packages/9d/48/5adf5c67addc7ddb328122208c6d375a84cf154984f412b4087330a157bd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3186969ee66a9863b00acc6523ace44c56974eecb348a7ea4b228d9f0b80e19", size = 1424001, upload-time = "2026-08-07T11:27:42.708Z" }, + { url = "https://files.pythonhosted.org/packages/38/a1/70074dd3869d2b0e934f91891d8d6b734361cd3b80f85ca7ece2e668ecdd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40a57b73731be45da4fa41430c4d5dc94a24b3a4faba7b9e069978c0402064ea", size = 1245545, upload-time = "2026-08-07T11:27:44.4Z" }, + { url = "https://files.pythonhosted.org/packages/e3/be/53b9c0a8a6399950c2e3546bdfab96d2b299d5b114b47eb94fd3c49c4054/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5075b9da3ef807eda752502446dfecea3b381c4900b7e27a5d5f4f899eb39951", size = 1248961, upload-time = "2026-08-07T11:27:45.781Z" }, + { url = "https://files.pythonhosted.org/packages/eb/13/3651d3812548a2bda15e26e5dd51aadb48cf682d0865370255fcf0e367dd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:293cc1c5bfa741f8e3fbe8175b9c07beee487c9a6fdbb25a5acad9f1df2d30a9", size = 1243877, upload-time = "2026-08-07T11:27:47.325Z" }, + { url = "https://files.pythonhosted.org/packages/21/a0/521f0bf000f675e9312a4aae2c8ba7a992405d072a85c485e08fd59433b9/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0910c3442a75216dde0f102d854ba2aaa71d2482e0ee213630b9bf29584fba3", size = 1293903, upload-time = "2026-08-07T11:27:49.264Z" }, + { url = "https://files.pythonhosted.org/packages/b1/7e/402fc902568aa2ee65865a3e151f000db0153da8ce6b1be4c9c349025f8d/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43dd6d596879bb1cb8a12cc9dae7bb10090a39a35883026c24f82488a195619a", size = 1401070, upload-time = "2026-08-07T11:27:50.947Z" }, + { url = "https://files.pythonhosted.org/packages/ff/7c/97d4b66c057f1706fc8be6dd532cc77c988794357c8f4ffdb6adabb39562/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8c9d537f59e936392cfd3597789d1390304dd659efc3c486ce7f40fb6b8a9f53", size = 1502602, upload-time = "2026-08-07T11:27:52.364Z" }, + { url = "https://files.pythonhosted.org/packages/89/6f/72cc3b71562001bba46e898ccfbf1844f7939b3e28912736206102f2e5a8/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f0190a33d7f97c65e9069f7a7f40499eea6b5cbe260c558378109caf20ce934b", size = 1495848, upload-time = "2026-08-07T11:27:53.803Z" }, + { url = "https://files.pythonhosted.org/packages/a0/53/d6f629d1e49308b2f363dae028baa213ec222c9106fa1f7f0d1f7b41499a/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:77308ae6c5cf5264cc0f01a7c556ec77a9e68eb1f61b093534d698139fdc3b14", size = 1556556, upload-time = "2026-08-07T11:27:55.342Z" }, + { url = "https://files.pythonhosted.org/packages/ee/22/340f35dd8dfc6d412d53dc20699ca014b8d228db923e8ed4759c512b162c/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8d53a23f27e1ed3a36b2d26fd2a1a6228c8e85a1ed62ff7cdb44bd610769f20a", size = 1417822, upload-time = "2026-08-07T11:27:56.712Z" }, + { url = "https://files.pythonhosted.org/packages/11/29/6dde5c13fbebc051d3a6df4ec0a6fd1d5359333cc1193f7f609f3410b4d8/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ffa5e7cb08f96fed9121f77b224151e41caf88feab9d652bb46c78202b6fbeda", size = 1445153, upload-time = "2026-08-07T11:27:58.275Z" }, + { url = "https://files.pythonhosted.org/packages/62/c5/f473a8ed030f7a0ca24b9849cca184677a50c053867a7b808c2e1289bbd3/ast_serialize-0.8.0-cp314-cp314t-win32.whl", hash = "sha256:fa70ed4dea0bb18b30a1789c77baa701d0ef30c474f2ccabdea61e25623a8827", size = 1063711, upload-time = "2026-08-07T11:27:59.793Z" }, + { url = "https://files.pythonhosted.org/packages/23/63/39e171fcd38ca057c2e1979d5ee81ac7a3502784abe3d83df7454f7a0978/ast_serialize-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d8b3c8eee4c1baef9d4e84d2a59a805501617127be42615cb48970b15b0892b6", size = 1103740, upload-time = "2026-08-07T11:28:01.405Z" }, + { url = "https://files.pythonhosted.org/packages/21/1c/d00762b399e7726d68d0a088cc946e3a4c60f1c6176f557608f672f627f3/ast_serialize-0.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:ac4f0a83c55a9b782f79ad55a5247b7db123c1db405959791c2ef886e9710c9f", size = 1076021, upload-time = "2026-08-07T11:28:02.947Z" }, + { url = "https://files.pythonhosted.org/packages/4c/11/911210c3c78923273a9211a2b6cfc4c8aa723b30dab3e1c8d19afb983b40/ast_serialize-0.8.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:86b8a1e6d90467345356098b040150e82fbc26d24a7a202224b13dc1f6264ca0", size = 1177715, upload-time = "2026-08-07T11:28:04.654Z" }, + { url = "https://files.pythonhosted.org/packages/77/89/6282881c8587606638db153cbe21e1e0c4d1f3970dee1aa0610a1c62a026/ast_serialize-0.8.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:39e92ff8e8cb45947fe9007174b2950e1fb098e6abd00266a13cd3bcf6675068", size = 1169347, upload-time = "2026-08-07T11:28:06.1Z" }, + { url = "https://files.pythonhosted.org/packages/97/78/a9f846a03a340ff3728c915f23338ca742742f3292700559cdb3ad999b1e/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c85d8d18db5b2dfcb3b7e38a4d600ca35504c0ed8a6f75cd1c811e4ffe248a15", size = 1225916, upload-time = "2026-08-07T11:28:07.654Z" }, + { url = "https://files.pythonhosted.org/packages/c0/15/aba6ef8a988a6eceb6f0359589aac509e29ae2dba67fd9bfd5af0c3f13e7/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9830ff7e764f74d9eefb01170c61a9f0fd2c027dac5fcb72e064decd57d56371", size = 1227135, upload-time = "2026-08-07T11:28:09.504Z" }, + { url = "https://files.pythonhosted.org/packages/94/29/3f63d696ea7c5b8abadcecc3505be51bd900daaccc522ed8322fa5b05a93/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6479d9722a4cd21b578f5478074c41e6169f04811996ec881655560f703a5bba", size = 1425040, upload-time = "2026-08-07T11:28:11.044Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5d/0aac338604ff59df5774d4304307898982252f325ff7cafe31d52fedcb65/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a63bed264e818cd83eec11feed0f50aa162542b91132ef58afebc857182763a5", size = 1246278, upload-time = "2026-08-07T11:28:12.519Z" }, + { url = "https://files.pythonhosted.org/packages/23/ca/9f1ef795bb724719532bd86dbec11e5b66857d3fbe9b6772baec0191a6ed/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d187197d234aa45d6cfa2b096be5f666e8cc2e7eb3722d0ab8926293cf5720c", size = 1250029, upload-time = "2026-08-07T11:28:13.896Z" }, + { url = "https://files.pythonhosted.org/packages/dc/25/5e061372d2ed953b9ba3b9c4f73de3b8e9234cda3f6c088db4686801d0e1/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:2d39a56282cfcc0d8eeea37267c754be59c98d48505c23b1dae5c6011f3813dd", size = 1243575, upload-time = "2026-08-07T11:28:15.37Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c1/ae7da218053120635a4ca802366c69f707203641af95372eeb83f70dfd52/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7cc5f10386994c0f4844f1e6d6a97127e9b478660eb6dec2b257644f0acab64", size = 1294396, upload-time = "2026-08-07T11:28:16.813Z" }, + { url = "https://files.pythonhosted.org/packages/2e/89/271d1f49c5269fcddcc789ea3f25be401f6723fc1138aeda539f4d05516d/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6102f2f985c2e542be85cd857678ec9356fefa792b93cadfadd31139f5696f27", size = 1401987, upload-time = "2026-08-07T11:28:18.333Z" }, + { url = "https://files.pythonhosted.org/packages/55/be/4e7d77fcf571ac7cb5cf7115a20c36642bd7d29473b45dfaaefeb9618f90/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:3a8660fe66667b76a6e9dccd1d33e66b229fde3b308db991c041609226c005b6", size = 1502904, upload-time = "2026-08-07T11:28:20.039Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ae/ed1de2db7e019d4236fbc164ffa5ef9a6022a300a342bbf142d21b7c141e/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:e7266307e5fba39836edb79def8608887af48820508bff3c5f2941e1e04d1534", size = 1496967, upload-time = "2026-08-07T11:28:21.734Z" }, + { url = "https://files.pythonhosted.org/packages/92/89/5fea507fae5c5f18b7dc7f95e5c00956574b8c717b8fd2049c504fab0b18/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca7e6fd1ad845d1cc649dc2ecd499db2f8f46af5bf8da7b70dd858774cc038b", size = 1559041, upload-time = "2026-08-07T11:28:23.194Z" }, + { url = "https://files.pythonhosted.org/packages/42/71/478d69df21b64e064554a68134c94be304270316ca676a94e63c389a636a/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:2880350b13d3eae69a0d70bc1fb6c9bfaca4dbd0e20ba8cd1aa483080b56ff06", size = 1417367, upload-time = "2026-08-07T11:28:24.601Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2d/8962dc8d5b3a9dc27b36f9db199afa25264c741505469d9ec10ffbfd2ba7/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:ab0f9a59f7d63d0d441b56b9a818b273705264352d5115cfee12e940e816d958", size = 1446178, upload-time = "2026-08-07T11:28:26.152Z" }, + { url = "https://files.pythonhosted.org/packages/4f/22/14d2ad4fd1d1bcd0dc687ca268e0630069f45162496260c0efb70ee0ea72/ast_serialize-0.8.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:0485a25ef519c62e749ee3c1ad8070e591b380d67226349eb5a70b228dc1ac4a", size = 1063811, upload-time = "2026-08-07T11:28:27.864Z" }, + { url = "https://files.pythonhosted.org/packages/18/1d/84a327c0202a41aa5fdba3ade33904d6d8f3b9e6806fa83568d835395850/ast_serialize-0.8.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:bd84d60bca7079e741be4ac5dbe237751a59d7f6f9f0126b11880d63822cbe16", size = 1105518, upload-time = "2026-08-07T11:28:29.691Z" }, + { url = "https://files.pythonhosted.org/packages/8c/92/74556dec52fde85a2ad84ed159991b916241043788609c15d8b77e14570b/ast_serialize-0.8.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:057769b5921336eb2d9124f2a731b42ed05ffdac559b840dbdf6f3937cf153dc", size = 1076319, upload-time = "2026-08-07T11:28:31.282Z" }, + { url = "https://files.pythonhosted.org/packages/d1/5d/c650b1f2cc1e75193358da95a080261422e8cd10b66d7370b1688c9915c5/ast_serialize-0.8.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:a02cbed7d8bfdcdee88edaac12bd50d53d9953aaa2e1852ef078625be5f1c0b5", size = 852914, upload-time = "2026-08-07T11:28:32.929Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e3/6142e920fec6ef7bccabd8c24ed8ed99f8bdc6cb8b065e1df7c6a3b2d667/ast_serialize-0.8.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e1bd223df0f6c96b396975fa604cb33bce53d9b4a0185490be4c4a289f7c9c87", size = 1184007, upload-time = "2026-08-07T11:28:34.654Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e9/6e8be8df02b35d85e2b8809f7f1cfa290bdf5882b55127a539d049482db0/ast_serialize-0.8.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ddd3b61f45c132da66c5476b281891e08c1fd87fbdabe8a6973e1622efc85f06", size = 1177588, upload-time = "2026-08-07T11:28:36.318Z" }, + { url = "https://files.pythonhosted.org/packages/8c/80/7e0fd2e2e2aba257820db4a8657c4c356844d36b914b20a4af294bcfb902/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f9caa63fad8241257ae401b5ff0a64026c6adb36b8e86cbe8782d9ea505daf6", size = 1234575, upload-time = "2026-08-07T11:28:37.772Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/3bae0af06f9b1bae3001c44d64215f5b567877e7aae9ffd45db11c3a7647/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3926fa117b5e65019853a2969966d11c7175af377a3425991f3fe73784412405", size = 1236015, upload-time = "2026-08-07T11:28:39.14Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c4/ce2d41a1bc22508e82618901f7e10f2a5e2f9556553fea90624daf9875e2/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:485f1113af805e9e170b95ef993ca3fbd4f89c04bab25c58b4fc632d854801ab", size = 1432808, upload-time = "2026-08-07T11:28:40.664Z" }, + { url = "https://files.pythonhosted.org/packages/1a/90/f5058f209756dd70e958b7538aaa82d25d24944baf9ec8ae6f27b06fcacc/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccebbed24f1281062d5852353c72c47502955926cfcb8345ffb3a44d87ff3d3", size = 1256251, upload-time = "2026-08-07T11:28:42.223Z" }, + { url = "https://files.pythonhosted.org/packages/bf/32/7f77ea87fa0836daab706ed5cb7f903bb25fa26a77439011aee626af11d8/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:252f883290d1cdb728eb7fe1d9a7221b88af5a329aae0bc91ddee4dafb820331", size = 1258574, upload-time = "2026-08-07T11:28:43.751Z" }, + { url = "https://files.pythonhosted.org/packages/eb/5a/75b82ad2725b5e8e8c742732f9e76c6738a292d0709e1f60d10a973730b4/ast_serialize-0.8.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:96abc072ad29db8d02194afd47d68987322622787daceae82398d7b69f3ba2e6", size = 1254075, upload-time = "2026-08-07T11:28:45.28Z" }, + { url = "https://files.pythonhosted.org/packages/4e/54/8c20ed4eea805516a3fd23dd4a721ce28c64f50f0e4b359969f60a8c97a6/ast_serialize-0.8.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9118ad3e369727060b2696fc4078f250ecffca4248ba87f537f55cea9f9dce06", size = 1301018, upload-time = "2026-08-07T11:28:46.851Z" }, + { url = "https://files.pythonhosted.org/packages/cb/5b/9f14430f12fe830b656fb38f8e2e05ee13b02a88967660bef46af0ab22a8/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f359df4bd921918af8bebd142a376c77511d7151cc8ba852760b587b5a4a54f3", size = 1409951, upload-time = "2026-08-07T11:28:48.312Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3d/084882eca93c842bd4262591a071ec7f825340644035e51501208cc5a8d4/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:e94f9121d13fa36cbf21314783c77d05ae3a0868decd18cf5233fdcc6de49ac8", size = 1509544, upload-time = "2026-08-07T11:28:49.847Z" }, + { url = "https://files.pythonhosted.org/packages/ce/73/ea84852096c2036c61cc0b2f97b90242207419f534dc671060ee1c8e05cb/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:54f95b486018d262bcb387a9afd96f0da74508b442762b80c769454a6fbb3ee3", size = 1505671, upload-time = "2026-08-07T11:28:51.239Z" }, + { url = "https://files.pythonhosted.org/packages/cb/88/287b9a5300c1f2f651d259f670931b63110adc265b7613c885b44c5bc53d/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c38b915511e32bc718c49dbce98ff9af36bac0ad6a604f58000cd5e3aecdba7", size = 1563685, upload-time = "2026-08-07T11:28:53.112Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f3/1bc3a79afcf0c2a8d2c37182d0d659d1545a9d7f7f6dc9cf3e63d6c17135/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:9a2ef9cf12f2de4f1028c42c1dd7d775255e0fb3e5bb48896c97e35ef52366fe", size = 1427977, upload-time = "2026-08-07T11:28:54.418Z" }, + { url = "https://files.pythonhosted.org/packages/5c/cd/440c798957e14e31776bfeb024d8fafe0bb1d5b89c51c2f067e69938f7b0/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f18048fe9f6dd266bd577cdec48bdcecb74faaa01fe941324435483b013ed2a", size = 1454335, upload-time = "2026-08-07T11:28:55.968Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4a/587eb36dcc240a54c8660f599464516b469ecad96f0dbdb6bccbedb50745/ast_serialize-0.8.0-cp39-abi3-win32.whl", hash = "sha256:31883542dd6c94d178f5db3d32fbd69c5eb88b3a7c018e7ac8cc0c45195ddbed", size = 1068858, upload-time = "2026-08-07T11:28:57.541Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a4/3e887bbd92164e183cb6e412c6a3e9198ddd446d7fe405958293ef5ef49c/ast_serialize-0.8.0-cp39-abi3-win_amd64.whl", hash = "sha256:861794565b06337005c1447ef23103a3d5a627d08bdc827870d00d0b28ef5f51", size = 1111839, upload-time = "2026-08-07T11:28:59Z" }, + { url = "https://files.pythonhosted.org/packages/25/6c/b400476d3ceba681ab929787edc9554f6d88fcc69435eb681b00fc0457a5/ast_serialize-0.8.0-cp39-abi3-win_arm64.whl", hash = "sha256:b2a5978662fd4db463dfb4b974d2b10ac6430b98f5333aabc7051909df3561d0", size = 1083655, upload-time = "2026-08-07T11:29:00.349Z" }, +] + +[[package]] +name = "catboost" +version = "1.2.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "graphviz" }, + { name = "matplotlib", version = "3.10.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "matplotlib", version = "3.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "pandas" }, + { name = "plotly" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/0e/09e8fa0858570fda88090bc3f441b69c18ea3d6f4a02fd41aa5426c157bf/catboost-1.2.10.tar.gz", hash = "sha256:26ae6d423acaf0e9d8160f2477a990431057ed04522d993c2f42dac62743b4f7", size = 39925863, upload-time = "2026-02-18T16:13:29.092Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/5b/9086b4183bc3ad17daf4d38489c0c9d4e7e89cd327978e7379aedcd918eb/catboost-1.2.10-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:cf54c216f6b3b102e06a5fc42deeb7a2497d622e6bc2e222f586e7e357a942f1", size = 28849868, upload-time = "2026-02-18T16:11:29.502Z" }, + { url = "https://files.pythonhosted.org/packages/37/5e/4fe404306a4839358e4d196a834765a137be163d2a29b316d01233c3a1e2/catboost-1.2.10-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:25c9b0dd9afb464efe7ccabf7567241aa566f70e7f77893218cb9fa21663e5d5", size = 96702284, upload-time = "2026-02-18T16:11:34.511Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c4/2db4b19e21b0620ba8cb706120aeb2649694f96f4b4de7b4678f07a79873/catboost-1.2.10-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:5319c7f9a7764d7dba04c218fd28383b7267553f83232e8ce8737d6b8d38534d", size = 97152239, upload-time = "2026-02-18T16:11:40.027Z" }, + { url = "https://files.pythonhosted.org/packages/e6/4f/7134bf2cfdfe46bbb059fac4ac562ce91586a4eb31ca33cb1b4a3ca298bd/catboost-1.2.10-cp310-cp310-win_amd64.whl", hash = "sha256:19de3cb267be3ddb8fd667a87f9e7d3c9ee31783c61ea9e6e6f036f666bddcc3", size = 100245782, upload-time = "2026-02-18T16:11:44.756Z" }, + { url = "https://files.pythonhosted.org/packages/90/52/b961328a61a31a474c61fdcc1a19a086f7e672fd1becc0f70697a0ccccdb/catboost-1.2.10-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:ab2e84237308d62bae236b1ecba2e3867697f96bdbaf0ca68dafc2c886946406", size = 28850304, upload-time = "2026-02-18T16:11:49.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5e/4cb6a2f896b34aaa4afe70491c595f77bef7f9b948d719eda99678847d3d/catboost-1.2.10-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:5ffe85f53092219cf65c73c2946426a289ef6f62c119c2bfda52815250d9bcef", size = 96708112, upload-time = "2026-02-18T16:11:53.607Z" }, + { url = "https://files.pythonhosted.org/packages/7c/62/5839abf95f9ee4bc2beb1be4a45f1a912859362477a4eb0f4c9d81298f53/catboost-1.2.10-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:5819a880af6b314f4980e6c26ad0f7552eafcf247d521bc884fe726347fdd87d", size = 97159502, upload-time = "2026-02-18T16:11:59.55Z" }, + { url = "https://files.pythonhosted.org/packages/a9/af/36048fdd08eca7876716176c30acf0e7ff1dfb1f53d0b93a021537e26601/catboost-1.2.10-cp311-cp311-win_amd64.whl", hash = "sha256:41bbe16cab0695978c325a20fa300f92831ed78e9cc8c5fe8047538b4055e98e", size = 100244500, upload-time = "2026-02-18T16:12:04.065Z" }, + { url = "https://files.pythonhosted.org/packages/bb/52/f5cd568800c87576012d715481730da93bcc34e609c5c204550a9ad0c067/catboost-1.2.10-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b27115d5b443048f710001c8ac666892dfe03498492310b00466203c91cc30a5", size = 28884278, upload-time = "2026-02-18T16:12:07.659Z" }, + { url = "https://files.pythonhosted.org/packages/33/ae/d33a8feba68fa810b30d70c660e4a2c62299472c2e1aa34406ccce306d13/catboost-1.2.10-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:39234b3692b6c9002b4a2ac529025fc210dd72feb9b621b27d17c65b7d3e9f92", size = 96704178, upload-time = "2026-02-18T16:12:11.229Z" }, + { url = "https://files.pythonhosted.org/packages/15/6c/08eabe522ac5cefc605ef81f273d77602130739ec7bcdc0ef192aa0a1f07/catboost-1.2.10-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:b28f763776e62f50da90dddf73b36399583295032667a7e46fc5c1f2593eb80f", size = 97136498, upload-time = "2026-02-18T16:12:15.339Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/f467a133b37eef2b3d8697d46a6e7f0da24bd3643f5475817c473ffc41dc/catboost-1.2.10-cp312-cp312-win_amd64.whl", hash = "sha256:6b8a7ef11d7a89fc547760cfafeee895011a4b92cc1f60d00235ef80a71158ed", size = 100214655, upload-time = "2026-02-18T16:12:20.046Z" }, + { url = "https://files.pythonhosted.org/packages/2d/02/3c5f08a7c7969eaa2509d804461db26752fe1c7ecb8ad8510cab51a95fd2/catboost-1.2.10-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:bd3d3b344894f61b5f70124658f302148bb9a51c41d0d5b6c453a72e9dfefc49", size = 28829400, upload-time = "2026-02-18T16:12:23.682Z" }, + { url = "https://files.pythonhosted.org/packages/98/fd/63be2ff7aa9f6a7d63e342f42948259a028bfa50203d5ff687c84804ffb7/catboost-1.2.10-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:59aa166f075f0a5ea57b0ba46e5060bd6a22e849e91e4142f16c2df11295b184", size = 96680675, upload-time = "2026-02-18T16:12:28.407Z" }, + { url = "https://files.pythonhosted.org/packages/fe/2c/fa0479bd79226f037b495a30696b70741beb198f65227c975005e213aa8e/catboost-1.2.10-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:42c1b6c7ae5c18cdbe00c8b9493987cc13338fe328baaf1a0b98ddaf58db96a2", size = 97111368, upload-time = "2026-02-18T16:12:32.456Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/a9e9a06418832fbea9d7cefda585d53395358d498537b6bdd3cf7364cd29/catboost-1.2.10-cp313-cp313-win_amd64.whl", hash = "sha256:5ede858e634d6d0f521bf6dd6fad9374f23d37049ee48e0779ccd2a372632cb1", size = 100201430, upload-time = "2026-02-18T16:12:36.731Z" }, + { url = "https://files.pythonhosted.org/packages/56/58/f370f6c64db5e7da92e3b88ab62e2df72f113cf5a1eee35b48f69d54accd/catboost-1.2.10-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:3efc5e4d414b7c13bff6dd0d6c938cf09bb1445097283c7790e54b8ee461820b", size = 28840256, upload-time = "2026-02-18T16:12:40.153Z" }, + { url = "https://files.pythonhosted.org/packages/9d/74/18597f0b2923e3660cd44f942fe9e7cddaa99afc252bc745c48f79566330/catboost-1.2.10-cp314-cp314-manylinux2014_aarch64.whl", hash = "sha256:bad9a70890cdc591080a908d54a3cd70002ab1e48b2017adff84726da0b3e16d", size = 96688527, upload-time = "2026-02-18T16:12:43.534Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ac/7effae0e47fd9586e46a796f5af61b730c572570cedee333ee9ba8db85a8/catboost-1.2.10-cp314-cp314-manylinux2014_x86_64.whl", hash = "sha256:7b8cc4ea3a6ac4a8d05f3a79c8ee5454360a0a710fa12444963865ad3f0ddfec", size = 97119495, upload-time = "2026-02-18T16:12:47.557Z" }, + { url = "https://files.pythonhosted.org/packages/da/b7/8f9e284a9cdd034f01f017dc5dab0da03dc3eac171a2be205745da3becb6/catboost-1.2.10-cp314-cp314-win_amd64.whl", hash = "sha256:951c5bdf27b8edb6ca624f41134888c666ae68275488803d3c91ce83e154f0c5", size = 101749687, upload-time = "2026-02-18T16:12:51.736Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coloredlogs" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "humanfriendly", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520, upload-time = "2021-06-11T10:22:45.202Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload-time = "2021-06-11T10:22:42.561Z" }, +] + +[[package]] +name = "colorlog" +version = "6.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/55/ba79756cb90c8d69d599d57785398ac87bba7b19c80e87f4e8a562197c93/colorlog-6.12.0.tar.gz", hash = "sha256:2a7924c1dadf18b22a0eb8b06d1c7b01d5341707ec1641eb6fcc4fde0c3e8e5f", size = 18151, upload-time = "2026-07-23T13:40:40.71Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/19/0b6647bf5e331521e55d2b63bfbdc210bd9cd605189273f03614a05f702d/colorlog-6.12.0-py3-none-any.whl", hash = "sha256:30d392604e9110045a2c2aeefc27d7a017abbab63f3a8aee594eac0801df784e", size = 12239, upload-time = "2026-07-23T13:40:39.562Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" }, + { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" }, + { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" }, + { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" }, + { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" }, + { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" }, + { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" }, + { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" }, + { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" }, + { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" }, + { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" }, + { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, + { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" }, + { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" }, + { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" }, + { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" }, + { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" }, + { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" }, + { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" }, + { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" }, + { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" }, + { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" }, + { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" }, + { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" }, + { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" }, + { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" }, + { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version >= '3.13' and python_full_version < '3.15'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { 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/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "falcon-ml" +version = "1.0.0" +source = { editable = "." } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "onnx" }, + { name = "pandas" }, + { name = "protobuf" }, + { name = "pyarrow" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scikit-learn", version = "1.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "skl2onnx" }, +] + +[package.optional-dependencies] +gbdt = [ + { name = "catboost" }, + { name = "lightgbm" }, + { name = "onnxmltools" }, + { name = "xgboost", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "xgboost", version = "3.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +hpo = [ + { name = "optuna" }, + { name = "tqdm" }, +] +runtime = [ + { name = "fnnx", extra = ["core"] }, + { name = "onnxruntime", version = "1.23.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "onnxruntime", version = "1.28.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] + +[package.dev-dependencies] +dev = [ + { name = "mypy" }, + { name = "pytest" }, + { name = "ruff" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] + +[package.metadata] +requires-dist = [ + { name = "catboost", marker = "extra == 'gbdt'", specifier = ">=1.2.10" }, + { name = "fnnx", extras = ["core"], marker = "extra == 'runtime'", specifier = ">=0.0.3,<0.1.0" }, + { name = "lightgbm", marker = "extra == 'gbdt'", specifier = ">=4.7.0" }, + { name = "numpy", specifier = ">=1.23.0,<3.0.0" }, + { name = "onnx", specifier = ">=1.16.0,<2.0.0" }, + { name = "onnxmltools", marker = "extra == 'gbdt'", specifier = ">=1.16.0" }, + { name = "onnxruntime", marker = "python_full_version < '3.11' and extra == 'runtime'", specifier = "<1.24.0" }, + { name = "onnxruntime", marker = "extra == 'runtime'", specifier = ">=1.18.1" }, + { name = "optuna", marker = "extra == 'hpo'", specifier = ">=3.0.0,<5.0.0" }, + { name = "pandas", specifier = ">=2.0.0,<3.0.0" }, + { name = "protobuf", specifier = ">=4.25.1,<7.34" }, + { name = "pyarrow", specifier = ">=12.0.0" }, + { name = "scikit-learn", specifier = ">=1.5.0,<1.10.0" }, + { name = "scipy", specifier = ">=1.9.0" }, + { name = "skl2onnx", specifier = ">=1.20.0,<1.21.0" }, + { name = "tqdm", marker = "extra == 'hpo'", specifier = ">=4.0.0" }, + { name = "xgboost", marker = "extra == 'gbdt'", specifier = ">=3.2.0" }, +] +provides-extras = ["runtime", "gbdt", "hpo"] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy", specifier = ">=2.3.0" }, + { name = "pytest", specifier = ">=9.1.1" }, + { name = "ruff", specifier = ">=0.16.1" }, + { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=2.0.0" }, +] + +[[package]] +name = "flatbuffers" +version = "25.12.19" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, +] + +[[package]] +name = "fnnx" +version = "0.0.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/99/e7/a5845c7c471949e494beb6f8aa6837f7f3e4cf31ab51dfa571b23fa004a0/fnnx-0.0.12.tar.gz", hash = "sha256:ef6369a209e579686d402617a91b3760d90025541a86f901d9f6ae422bdffcbc", size = 67594, upload-time = "2026-06-28T18:20:32.849Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/f1/42c8ac5bf17f0300079ec1b6a4a96610929b0a8e92c0c52445e543b7e7dc/fnnx-0.0.12-py3-none-any.whl", hash = "sha256:6e1c46ced2416c1dafb520ed90d1ad83ec293a0e0ce3393715ba57e97db31d0b", size = 51052, upload-time = "2026-06-28T18:20:31.599Z" }, +] + +[package.optional-dependencies] +core = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "onnxruntime", version = "1.23.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "onnxruntime", version = "1.28.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] + +[[package]] +name = "fonttools" +version = "4.63.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/84/69/c97f2c18e0db87d2c7b15da1974dace76ae938f1cfa22e2727a648b7ed43/fonttools-4.63.0.tar.gz", hash = "sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0", size = 3597189, upload-time = "2026-05-14T12:04:30.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/c9/4141c90a90db20f807c7e10bfd689fe53eb8f7f4caff58ee4d4dfe46919f/fonttools-4.63.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e3297a6a4059b4acc3a1e9a8b04741f240a80044eef08ebd32e8b5bcdddce75b", size = 2884632, upload-time = "2026-05-14T12:02:38.56Z" }, + { url = "https://files.pythonhosted.org/packages/b8/46/ad12b5c10eae602d7ef814b02afa08aacbf89da917fed5b071282b7eadc2/fonttools-4.63.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b1cd75a03ad8cb5bc40c90bfde68c0c47de423aa19e5c0f362b43520645eea94", size = 2429441, upload-time = "2026-05-14T12:02:41.162Z" }, + { url = "https://files.pythonhosted.org/packages/90/8f/bdca24a84c81d56fffed052229cdcff368f6e05882e526f4558891481f65/fonttools-4.63.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0425b277a59cff3d80ca42162a8de360f318438a2ac83570842a678d826d579", size = 4946346, upload-time = "2026-05-14T12:02:43.41Z" }, + { url = "https://files.pythonhosted.org/packages/04/59/a639c0e136441ee91a65b56fdf89e5d075927e7a09c559d1b0f5276577db/fonttools-4.63.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d7e5c9973aa04c95650c96e5f5ad865fbf42d62079163ecfab1e01cbc2504c22", size = 4903184, upload-time = "2026-05-14T12:02:45.742Z" }, + { url = "https://files.pythonhosted.org/packages/e6/53/91b7e0cb45b536f3da1b29ba8cbab89f27e8b986809e0b1982303a3f4eca/fonttools-4.63.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cb014d58140a38135f16064c74c652ed57aa0b75cbf8bb59cac821f7edb5334e", size = 4922967, upload-time = "2026-05-14T12:02:48.386Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b7/87439bf44e6b97c5538cd29d0b7e366a5b8ce2cc132a4134fb67fa3f2fa2/fonttools-4.63.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:032038247a96c1690f9f31e377c389383c902531b085aa4e4dabd6f57f870e69", size = 5042799, upload-time = "2026-05-14T12:02:50.424Z" }, + { url = "https://files.pythonhosted.org/packages/ad/7c/8b96c3263b89ef99cded544c0f0636686f85dbd3c211c4dceef0231fca23/fonttools-4.63.0-cp310-cp310-win32.whl", hash = "sha256:a8b33a82979e0a6a34ff435cc81317be1f95ec1ebb7a3a2d1c8a6a54f02ae44e", size = 1519704, upload-time = "2026-05-14T12:02:52.523Z" }, + { url = "https://files.pythonhosted.org/packages/e5/4d/2c2f0069970b6907de8fb5b05c5c0193cc22f717df151d1c7aef1c738f58/fonttools-4.63.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c18358a155d75034911c5ee397a5b44cd19dd325dbb8b35fb60bf421d6a72ac", size = 1568666, upload-time = "2026-05-14T12:02:54.917Z" }, + { url = "https://files.pythonhosted.org/packages/75/2b/a7f1545bdf5da69c4bda0cea2a5781f0ad2a6623e0277267672db43c5fe6/fonttools-4.63.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b8ae05d9eacf6081414d759c0a352769ac28ce31280d6bb8e77b03f9e3c449f", size = 2881793, upload-time = "2026-05-14T12:02:56.645Z" }, + { url = "https://files.pythonhosted.org/packages/49/50/965308c703f085f225db2886813b27e015b8b3438c350b22dd65b52c2a2c/fonttools-4.63.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79cdc9f567aec74a72918fd060283911406750cbc9fd28c1316023deb6ce31a9", size = 2428130, upload-time = "2026-05-14T12:02:58.891Z" }, + { url = "https://files.pythonhosted.org/packages/d8/38/6937fbd7f2dc3a6b48725851bc2c15ec949b9af14d9bbcb5fe83cdf9bdf9/fonttools-4.63.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c14b4fd138c4bafcca294765c547914e1aa431ae1ca94ab99d8db08c958bd3b", size = 5111952, upload-time = "2026-05-14T12:03:01.263Z" }, + { url = "https://files.pythonhosted.org/packages/0b/43/a81f20050a3115b57d62c8e781446949512eac36690dc384ccea65ff4cc1/fonttools-4.63.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76ac49f929aecaf82d83250b8347e099d7aecba0f4726c1d9b6df3b8bb5fe18", size = 5082308, upload-time = "2026-05-14T12:03:03.211Z" }, + { url = "https://files.pythonhosted.org/packages/67/00/cdd9d4944ca6ae280d01e69cc37bde3bf663630b837a6fc6d2cd65d80e0e/fonttools-4.63.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dcf076a4474fe0d7367e5bbf5b052c7284fa1feca729c04176ce513521afd8a0", size = 5087932, upload-time = "2026-05-14T12:03:05.147Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f1/0aa0dbea778c75adbef223c42019fd47d22262b905974d62d829545d485f/fonttools-4.63.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7dd683fef0663e9f0f45cf541d788d24caa3ec9db50796b588e1757d8b3bc007", size = 5213271, upload-time = "2026-05-14T12:03:07.238Z" }, + { url = "https://files.pythonhosted.org/packages/a8/99/253e4056e1f0e67b9390125a154b73b5eb73ad521bece95c004858fdeec2/fonttools-4.63.0-cp311-cp311-win32.whl", hash = "sha256:afefc1ed0a59785a7fb06ea7e1678e849c193e1e387db783579bc7b3056fcfcb", size = 2304473, upload-time = "2026-05-14T12:03:09.271Z" }, + { url = "https://files.pythonhosted.org/packages/08/60/defa5e69641db890a63be281f41345f4c33b157824eaf0b9fad3e08b0dcb/fonttools-4.63.0-cp311-cp311-win_amd64.whl", hash = "sha256:063e08bd17bd5a90127a14123de0d6a952dbc847695fd98b63c043d58057f90c", size = 2356389, upload-time = "2026-05-14T12:03:11.53Z" }, + { url = "https://files.pythonhosted.org/packages/08/ef/b3c6b9b5be2f82416d73fe2ed2e96e2793cd80e7510bd6a17ca79cdd88ec/fonttools-4.63.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:37dd23e621e3b0aef1baa70a303b80aaf38449632cfc8fd2a55fb285bbccfc02", size = 2881131, upload-time = "2026-05-14T12:03:13.386Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/c815bea63117fa63e4e1c01f8a1110d2112fa003f838e6467094ec2432ce/fonttools-4.63.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9faff9e0c1f76f9fd55899d2ce785832efebab37eb8ae13995853aef178bef0", size = 2426704, upload-time = "2026-05-14T12:03:15.801Z" }, + { url = "https://files.pythonhosted.org/packages/44/04/0b91d8e916e92ad1fac9e4624760baf0fd5ff2ead614c2f68fb21373f03f/fonttools-4.63.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef3048ef05dbb552b89817713d9cac912e00d0fde4a3105c00d29e52e10c89af", size = 5044298, upload-time = "2026-05-14T12:03:18.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/c7/2342da9830e3e9d4870305ca5d2091d2a83284f2953079b7bdd3b5e029d8/fonttools-4.63.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58dc6bb86a78d782f00f9190ca02c119cf5bbe2807536e361e18d42019f877d8", size = 4999800, upload-time = "2026-05-14T12:03:20.161Z" }, + { url = "https://files.pythonhosted.org/packages/e6/6d/67fe16c48d7ce050979b33f47e0d28a318f02da030602e944c34f7a16ef3/fonttools-4.63.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee08ebfa58f6e1aeff5697ab9582105bb620008c1caafb681e4c557e7483027b", size = 4982666, upload-time = "2026-05-14T12:03:22.87Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/3bbab338c07c71fa56269953845e92c951a61457bbbb0f1022551ea266d9/fonttools-4.63.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:27fdc65af8da6f88b9c6121c47a464cbe359fcfff7ff6fc2d37a1f395d755b78", size = 5133598, upload-time = "2026-05-14T12:03:25.168Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/aa27c7f98db5b064883dadcc5283947e81e034de42e22a33675878d98b54/fonttools-4.63.0-cp312-cp312-win32.whl", hash = "sha256:af2fd1664d00a397d75f806985ddb36282091c2131a73a6485c23b4a34722263", size = 2292575, upload-time = "2026-05-14T12:03:27.496Z" }, + { url = "https://files.pythonhosted.org/packages/87/36/cccb9bc2a6ab63d1b2980374f0dca72ce95ae267c9b4cfe77455bb70d0d4/fonttools-4.63.0-cp312-cp312-win_amd64.whl", hash = "sha256:59ac449f8cca9b4ffa08d2e7bbadad87ce710d69d1eda5c3c1ce579baa987272", size = 2343211, upload-time = "2026-05-14T12:03:30.057Z" }, + { url = "https://files.pythonhosted.org/packages/0f/8d/d8fec3dcde2963f8c908fb315e5ff2cd0ac34f82394bbbf73a2aa5145ce3/fonttools-4.63.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd7e9857e5e63738b9d9fd707bc1f59c8b09e5177726d23664db393c59bb08bd", size = 2876062, upload-time = "2026-05-14T12:03:32.554Z" }, + { url = "https://files.pythonhosted.org/packages/ef/71/d935dc54e4ff121bfdd11e08702db63a7e6f25af21d8a3d7b7212df53641/fonttools-4.63.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c2a2a42198b696a6f48fad91709afb55176e66a5e566131219dba372fb7f8c59", size = 2424594, upload-time = "2026-05-14T12:03:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/8e/40/e76320afa1df918e146155ef239b1719ee266092e96f5423bfd075affba1/fonttools-4.63.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e874792a8212b44583ea02189d9e693906b2f78b261f372f95d6c563210ac1d", size = 5024840, upload-time = "2026-05-14T12:03:36.745Z" }, + { url = "https://files.pythonhosted.org/packages/ce/36/0b805d8c485f872f65a509cbe3b58a5d0d17bee855333b54a150c79d3061/fonttools-4.63.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22135da48a348785c5e2d5d2d9d6bec5ed44adacbaeb9db12d9493bf6c6bfa68", size = 4975801, upload-time = "2026-05-14T12:03:38.833Z" }, + { url = "https://files.pythonhosted.org/packages/c8/26/2cee03d0aa083ab022da5c07aff9ed3f689da1defb81ad6917c9627896da/fonttools-4.63.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ccf41f2efdf56994d22d73bef4ced1052161958169428d06ba9724ea9e9a64be", size = 4965009, upload-time = "2026-05-14T12:03:41.494Z" }, + { url = "https://files.pythonhosted.org/packages/7e/48/cc4b66d9058c0d0982c833fad10127c4b0e9324606aafa41382295ca4102/fonttools-4.63.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9ced0bd02ac751dd6319b0da88aaef24414e3b0dbc32bb4f24944821a3741a27", size = 5105892, upload-time = "2026-05-14T12:03:43.525Z" }, + { url = "https://files.pythonhosted.org/packages/d8/1f/a98a30a814b9ddef3a2e706025f90b9e0bc94890e6cb15254bc86547d11a/fonttools-4.63.0-cp313-cp313-win32.whl", hash = "sha256:85be818f5506e8a7753153def2c9550178f0ecae6a47b5e0e8dbb23f7cc90380", size = 2291313, upload-time = "2026-05-14T12:03:45.594Z" }, + { url = "https://files.pythonhosted.org/packages/92/46/5177b01f3b4abfdd4409f31cca4ab279c9343a26efbe9ec78c97fc612e02/fonttools-4.63.0-cp313-cp313-win_amd64.whl", hash = "sha256:ba04cb5891d4c0c21b6da95eda8d7b090021508a294fff33464fc7d241e0856b", size = 2342299, upload-time = "2026-05-14T12:03:47.414Z" }, + { url = "https://files.pythonhosted.org/packages/27/d2/23d25e3f247b328be58d04a4c9f894178a0d1eda7d42867cfb388adaf416/fonttools-4.63.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fd1e3094f42d806d3d7c79162fc59e5910fcbe3a7360c385b8da969bc4493745", size = 2875338, upload-time = "2026-05-14T12:03:50.052Z" }, + { url = "https://files.pythonhosted.org/packages/cd/58/7dfa0c761cb3b2964e2a84c4dc986c926a87de0cb9fb60d5b28ded3f2914/fonttools-4.63.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6e528da43bc3791085f8cb6141b1d13e459226790240340fcbb4625649238b03", size = 2422661, upload-time = "2026-05-14T12:03:52.154Z" }, + { url = "https://files.pythonhosted.org/packages/dd/87/64cfa18a7a1621d17b7f4502b2b0ed8a135a90c3db51ea590ee99043e76b/fonttools-4.63.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b2248c5decb223562f7902ff6325077a073f608ee8e33e88ad88db734eb9f49", size = 5010526, upload-time = "2026-05-14T12:03:54.647Z" }, + { url = "https://files.pythonhosted.org/packages/36/e1/a8933a72c45a87177fbde2696e0d0755c8c9062f8c077a961c6215fa27b1/fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:308f957cdeaf8abe4e5f2f124902ef405448af92c90f80e302a3b771c2e6116b", size = 4923946, upload-time = "2026-05-14T12:03:56.984Z" }, + { url = "https://files.pythonhosted.org/packages/27/60/872e6e233b8c5e8b41413796ff18b7fe479661bd40147e071b450dfad7a1/fonttools-4.63.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bf00f21eb5fb721dbaf73d1e9da6d02a1af7768f2ebcf9798be98beab8ba90f6", size = 4962489, upload-time = "2026-05-14T12:03:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/30/c4/83c24f2ec38b90cfda84bf4b1a1f49df80e84a1db4e7ac6e0d41bf23bc39/fonttools-4.63.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c1aaa4b9c75798400ac043ce04d74e7830376c85095a5a6ed7cba2f17a266bf4", size = 5071870, upload-time = "2026-05-14T12:04:02.122Z" }, + { url = "https://files.pythonhosted.org/packages/de/40/3ae22b60ff1d41ce0bd044b31238cdc72cef99f28b976f1e128ebd618c9b/fonttools-4.63.0-cp314-cp314-win32.whl", hash = "sha256:22693918177bd9ceabec4736d338045f357769416fc6b0b2508eefef75b08616", size = 2295026, upload-time = "2026-05-14T12:04:04.47Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d4/98078064ccc76b45cb0f6c002452011e93c4bd26f6850344f0951cc1fe89/fonttools-4.63.0-cp314-cp314-win_amd64.whl", hash = "sha256:7d782fac32985914c351556f68ac0855391572bcd87de50e05970d3cd4c96fc5", size = 2347454, upload-time = "2026-05-14T12:04:06.752Z" }, + { url = "https://files.pythonhosted.org/packages/49/4e/652d1580c5f4e39f7d103b0c793e4773129ad633dce4addd0cf4dfebde02/fonttools-4.63.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6db5140a60a5d731d21ec076745b40a310607731b0a565b50776393188649001", size = 2958152, upload-time = "2026-05-14T12:04:08.706Z" }, + { url = "https://files.pythonhosted.org/packages/0e/55/ad864c9a9b219f552eb46b32cd7906c466e5a578ba0c3abfcc0fe7413eb6/fonttools-4.63.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d76edbff9014094dbf03bd2d074709dfa6ec7aba13d838c937a2b33d2d6a86e", size = 2460809, upload-time = "2026-05-14T12:04:10.783Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2b/0aa8db70f18cf52e49b4ed5ecec68547f981160bf5ded3b5aed6faa0a6f9/fonttools-4.63.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0eac00b9118c3c2f87d272e45341871c5b3066baa3c86897fa634a7c3fb59096", size = 5148649, upload-time = "2026-05-14T12:04:12.747Z" }, + { url = "https://files.pythonhosted.org/packages/7f/63/18e4369c25043096f1048e0c9915951adc4f842bd81c6b18155824d6fa99/fonttools-4.63.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51394295f1a51de8b5f30bdb1e1b9a4231536c7064ef5c6e211eec19fa36036f", size = 4932147, upload-time = "2026-05-14T12:04:14.806Z" }, + { url = "https://files.pythonhosted.org/packages/a1/3f/67f3eac2ffd8a98446c5022f8ed3864eac878a5ff7af8df4c8286dba16cc/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9e12f105d2b6342c559c298afb674006bb2893afc7102dcf8a1b55b0486b4e40", size = 5027237, upload-time = "2026-05-14T12:04:17.675Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ba/4e6214cb38a7b04779e97bb7636de9a5c7f20af7018d03dee0b64c08510a/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:796f27556dbe094c4824f75ca85267e4df776c79036c8441469a4df37038c196", size = 5053933, upload-time = "2026-05-14T12:04:20.818Z" }, + { url = "https://files.pythonhosted.org/packages/34/3b/214dcc19ee31d3d38fb5ad2755c11ef0514e5dc300bbaf41c0b69f393799/fonttools-4.63.0-cp314-cp314t-win32.whl", hash = "sha256:948428a275741f0b64b113c955425a953314f4b9ab9997f73a72c83e68e569c8", size = 2359326, upload-time = "2026-05-14T12:04:24.22Z" }, + { url = "https://files.pythonhosted.org/packages/dd/1e/3ff1a9b523058c2eeb6a9d50f5574e2a738200d0d94107d5bc4105e8da3f/fonttools-4.63.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6d4741eb179121cab9eea4cb2393d24492373a260d7945006358c08cfbf45419", size = 2425829, upload-time = "2026-05-14T12:04:26.829Z" }, + { url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" }, +] + +[[package]] +name = "graphviz" +version = "0.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/b3/3ac91e9be6b761a4b30d66ff165e54439dcd48b83f4e20d644867215f6ca/graphviz-0.21.tar.gz", hash = "sha256:20743e7183be82aaaa8ad6c93f8893c923bd6658a04c32ee115edb3c8a835f78", size = 200434, upload-time = "2025-06-15T09:35:05.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl", hash = "sha256:54f33de9f4f911d7e84e4191749cac8cc5653f815b06738c54db9a15ab8b1e42", size = 47300, upload-time = "2025-06-15T09:35:04.433Z" }, +] + +[[package]] +name = "greenlet" +version = "3.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/74/b13368064b09053253555d3f2839cc2684d22d5aed0d2ccffbf7a6736558/greenlet-3.5.4.tar.gz", hash = "sha256:0232ae1de90a8e07867bb127d7a6ba2301e859145489f25cda8a6096dabe1d20", size = 206538, upload-time = "2026-07-22T12:47:14.468Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/9d/58f80897f4121f5c218bb931cf6d3b6514873f02ad0b729f744352926b9f/greenlet-3.5.4-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:ac5bf81d79d2c8eeb2ef6359b2e1687a1e9ebf46c2b1f970da9a9255df51d190", size = 293072, upload-time = "2026-07-22T11:38:14.299Z" }, + { url = "https://files.pythonhosted.org/packages/dd/9f/b4bc9bbd6a7855cbd8ad8a83c874eeeca56c24de9132b3323f81c03a30ba/greenlet-3.5.4-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89f3738167bab8c1084b94e23023d41d247117ac149fa0fbcb5bd4cf6262b353", size = 609393, upload-time = "2026-07-22T12:26:37.964Z" }, + { url = "https://files.pythonhosted.org/packages/05/0e/744b5e063af127d2e3c74fe0f1aef15573064c83b6066883524f5b258b17/greenlet-3.5.4-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e9a5e3406e3ed8125ae1a3b37c12f3434e2b1f0fa053197c5557895b4fb09606", size = 622750, upload-time = "2026-07-22T12:28:59.546Z" }, + { url = "https://files.pythonhosted.org/packages/d6/6b/d78ea2908e8e08985348f28ac396c2950be7ab66321dfe0054c73bd1f456/greenlet-3.5.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4ab9f0704bccf6d3b38e0d2130b7b33271cff11453690da074fa280c3aa8e8e7", size = 622920, upload-time = "2026-07-22T11:51:06.83Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e4/3ce7009c948920b01527f8d9da29f501a31ac3d98318829e981fd879b850/greenlet-3.5.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2cdaadc3d31445a8f782bde3cd37e49a2c2a9c6da6daf76a3e34c683b271a3c7", size = 1582262, upload-time = "2026-07-22T12:25:01.131Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4c/0408366102a33829f7bdd6a992dad75abbf75e86cc1e76caf19e57311d29/greenlet-3.5.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:70bdfacdc183dac838b2a0aaff2dd6134a457c52fe68a9c6bbab435483d2b9df", size = 1648906, upload-time = "2026-07-22T11:51:08.627Z" }, + { url = "https://files.pythonhosted.org/packages/13/52/ebfe8f6a1aeb8e430540b406c844ecc4e3367072b0192f69dcb85eeeec2b/greenlet-3.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:69173331fbc5d64bfac0065d7e22c39cfcd089e9b18d125bdcd5079363b09616", size = 246036, upload-time = "2026-07-22T11:38:30.073Z" }, + { url = "https://files.pythonhosted.org/packages/61/16/71eefcf68267bbf06a9b6bff57d0b222e49432326e85d74348b67694b8d4/greenlet-3.5.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e883de250e299654b1f1680f72a1a9f9ba62c9bd1bce84099c90657349a8dfbb", size = 294266, upload-time = "2026-07-22T11:37:56.142Z" }, + { url = "https://files.pythonhosted.org/packages/36/ea/a0b19adfc35d07e10acb626e9d22a3893b95f1309c42c4a20161dec16800/greenlet-3.5.4-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32802705c2c1ff25e8237b3bdacf2594fa02be80af8a66703eb7853ea7e68686", size = 613712, upload-time = "2026-07-22T12:26:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/54/76/a121978b3337407d05a1ce5f79b4aa5998a43a9d8422f9726029b90b4471/greenlet-3.5.4-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57aa201b351f7c7c75627c60d29e4d5b97a07d37efeb62b903466fca42c097d7", size = 625582, upload-time = "2026-07-22T12:29:00.814Z" }, + { url = "https://files.pythonhosted.org/packages/34/c2/080f16cf870e929e592f55767f01d6c98d2ee83bfdc36c3b892f2d0459ab/greenlet-3.5.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3fe76c2cac86b4f7a1e92865ac0a54384deb05c92986287c1a7110d9bd53071", size = 624663, upload-time = "2026-07-22T11:51:08.016Z" }, + { url = "https://files.pythonhosted.org/packages/9e/bb/8f3ca88370b817369008faeceeee85970adc16c92a70a3e5fe5fea495a57/greenlet-3.5.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1e1a4a684b16c45ba324e60b32a4386a87722bcb815d2a149d2182f9b401ca72", size = 1585010, upload-time = "2026-07-22T12:25:02.539Z" }, + { url = "https://files.pythonhosted.org/packages/51/c2/45877154689709ebce9a0b83c2235e6ca0f31577889b02af308c8cc5f8fb/greenlet-3.5.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e849e6e139b9671adeac505f72fc05f4af7fd1921faef40295e214fc3b361b59", size = 1651283, upload-time = "2026-07-22T11:51:10.408Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7d/8711a75cb61d85246277c07ff6e1a6504621ba473d808c11ad225ffca43f/greenlet-3.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:dc418cf4c873357964d6624445ed09472e50def990c65dd4e76fc3ba8cd9cef6", size = 246434, upload-time = "2026-07-22T11:43:15.557Z" }, + { url = "https://files.pythonhosted.org/packages/00/62/e290b3bce433da8f0324ac02da0b128d683482229f1a8b789fa47818a4cd/greenlet-3.5.4-cp311-cp311-win_arm64.whl", hash = "sha256:c38c902a0986eba1f6e7ba1ab39ad5195926abde90f3fe080e08212db62176da", size = 244990, upload-time = "2026-07-22T11:39:22.626Z" }, + { url = "https://files.pythonhosted.org/packages/f3/04/81bd731d6d1e3a469d9a4c36f5eb069bcf0cbb2d5d342c9fec22245b91fc/greenlet-3.5.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3d66250e8b09f182ede05490998c818b5961f7a3640332d44c4927caec7bbfe4", size = 295909, upload-time = "2026-07-22T11:38:09.261Z" }, + { url = "https://files.pythonhosted.org/packages/cc/dd/f5f22903a6ae70f5ea328ed0beaec92ad903f0e3b7d2845133b354abc4b8/greenlet-3.5.4-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c90e930c9c192e5b3ee9fb8bcd920ea3926155e2e3ded39fc697323addecee17", size = 612011, upload-time = "2026-07-22T12:26:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/8e/10/92a4a88d12b915d74ea5b6d288e4afefda4771647caa34442c156f7a454f/greenlet-3.5.4-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:791fdfeeb9c6e0c7b10fa151bf110d2a6974866f13dcb5b1c7efae698245893a", size = 624299, upload-time = "2026-07-22T12:29:02.089Z" }, + { url = "https://files.pythonhosted.org/packages/50/6d/0b14bb9db2989f32cd9fe7f76afedea01ee8bee3f87c07e69f24adfe7e63/greenlet-3.5.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f88193799d43dbf8c8a806d6405c9c52fe2af40bf75072a606357b33cc336c7f", size = 621541, upload-time = "2026-07-22T11:51:09.464Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/25e9a2d9eb6b2e8b7ca4e80a3a26cb887cce6c8e0a87c921164f11bc5574/greenlet-3.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b7a5f095767c4493afcd06067f2bb3b8716e3f3f9e92b99c88e7e99f885b3d4d", size = 1581444, upload-time = "2026-07-22T12:25:03.818Z" }, + { url = "https://files.pythonhosted.org/packages/b9/96/4c9bf2e2c408dcc0556edce69efa9f802e82223573c53240136a086821f1/greenlet-3.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:42afdc1ab5f66da8c586c32af9224a74a706b4f0ea0dc3a4188a0860a09c65c9", size = 1645842, upload-time = "2026-07-22T11:51:12.295Z" }, + { url = "https://files.pythonhosted.org/packages/b5/41/303ecb26a3a56122c0f4d4073ee078881847bd6b6f463ae0ec57ec20223b/greenlet-3.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:60149df8f462d1b230038e6590c23c3b4768bb5d6c022b3b6e82532b34b0b8a3", size = 247169, upload-time = "2026-07-22T11:38:19.893Z" }, + { url = "https://files.pythonhosted.org/packages/a4/e3/ef56864b4c35fcb3eb3b41b869f6cc46f4cd3f5e2c68e74acde8ac433951/greenlet-3.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:77d6ce04fed0d9aeed42e0f37923cc43eba9b027bdd9c34546bb4ccd143d0fe0", size = 245565, upload-time = "2026-07-22T11:38:27.061Z" }, + { url = "https://files.pythonhosted.org/packages/c0/9a/e51225dcd58713f16ccbdcc501a8da21098ea14515b7870f1f94459e5ff5/greenlet-3.5.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:24e61b88cb7e1b1d794b32a10cc346ac779681d6d74ff137a3e0a444d2bf1f02", size = 294831, upload-time = "2026-07-22T11:38:53.389Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ea/de50a50fadf979713ab18b46f22ad5ff5f2dcfc637a3ebdecf669801e1a5/greenlet-3.5.4-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:870d730fec833f5a06906a32596cc099b9161594642a92a520b7a88911c95356", size = 614619, upload-time = "2026-07-22T12:26:42.282Z" }, + { url = "https://files.pythonhosted.org/packages/db/c7/2aae27fea41205b8650294c301f042a2a4bb6155eea48c995b890a92f2c1/greenlet-3.5.4-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec5ff0d1878df6af3bf9b638a5a92a7d5693291de77c91bff10fa48519c604ef", size = 627021, upload-time = "2026-07-22T12:29:03.445Z" }, + { url = "https://files.pythonhosted.org/packages/eb/56/79fd826f9ccaae0b84e1b4ef68dabba5e105bb044ffcd448a0b782fcba9a/greenlet-3.5.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d84d993f6e575c950d91a23c1345d18fe1a4310d447bf630849d7809196b52f0", size = 624002, upload-time = "2026-07-22T11:51:11.391Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/27319f97e731298513dcba1a2e91b63e9d8811d9de22130f960b129b1bf1/greenlet-3.5.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:58023945f421093de5e6fa108c0985a8659d43f49e0216da25099369a121bcbd", size = 1581533, upload-time = "2026-07-22T12:25:05.322Z" }, + { url = "https://files.pythonhosted.org/packages/b1/6d/24240bf562e9786dd2799ee0a4a4dadb4ded22510f41b20245099159ac8c/greenlet-3.5.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bae2728e1897aa8df8cb1af38cd48b3a743aefe29372de7b8b7a9f532501e69f", size = 1645781, upload-time = "2026-07-22T11:51:14.805Z" }, + { url = "https://files.pythonhosted.org/packages/c1/5a/442ab1a9ef7ca6bf7210e5397a95972206a91a31033a03c8900866a10039/greenlet-3.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:ca5726c0b08ca35ae873557266a78b2c3f3b2b7d7401aa5ff886c2045dd0111c", size = 247133, upload-time = "2026-07-22T11:39:20.661Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e6/9160210222386b1a378ff94db846b9508ca24a121cf684991561fdb69280/greenlet-3.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:7c1303791d603080cac6fc3b34df51c3b75b723739c282c8029e48a0d241672f", size = 245500, upload-time = "2026-07-22T11:40:22.185Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a7/6ab1d4f9cd548d15ab90da29947f2076100130bb179b0bde59f795a459e3/greenlet-3.5.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:7e8afa5eac028f8140ceafe5ceec66e6aa127ddcb21452d2a564dcd2900b5f22", size = 295410, upload-time = "2026-07-22T11:40:35.747Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7a/422f63b4715cbc0b24385305407adf38b48f6bb68b3e6b04090e994d0f5a/greenlet-3.5.4-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73b37afe369021423ea53dd3123e04bffa7e93ac64429b9f50835b2e4fcae7cf", size = 661286, upload-time = "2026-07-22T12:26:43.8Z" }, + { url = "https://files.pythonhosted.org/packages/d0/31/5a1cac663bf5582190c5a714ef81364f03cde232227f39748f8ae4c11da5/greenlet-3.5.4-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ef964f56dfcb6f9bbef2a190d9126795eac408716aeae47b5e7c73c32aafca9", size = 673517, upload-time = "2026-07-22T12:29:04.815Z" }, + { url = "https://files.pythonhosted.org/packages/15/4a/2a82a1e3f8aaca020853ac8d12211280ca2b231aa08ea39f636f1060c319/greenlet-3.5.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c53ff01a5c53a40f2c16820ebc56d7c61a77f5fbe009dadd96292d5682f80f8", size = 670917, upload-time = "2026-07-22T11:51:13.589Z" }, + { url = "https://files.pythonhosted.org/packages/68/b0/e379a152b17bfdfa95795af4049e37c0fd1b4d81f020d426db104ed07c77/greenlet-3.5.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecca4d80d55a01ad6b23b33262662956149fbb7b2c6be2910f1705921958cbf3", size = 1628478, upload-time = "2026-07-22T12:25:06.678Z" }, + { url = "https://files.pythonhosted.org/packages/5e/43/bffdfa64f7317f954c5c1230b5dd5922676ce198689a68c1ac1ed4b1b1a5/greenlet-3.5.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ffbc533e0eaf8e80d8471411646ab88fe58f641d508c0b02b24494479f4d9ec", size = 1692021, upload-time = "2026-07-22T11:51:17.008Z" }, + { url = "https://files.pythonhosted.org/packages/d0/11/f799f9637e2c6e9b0b716015e339040598b058cf7654dfc0d67468b177ed/greenlet-3.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:305f69e6c4523d7f6979ed001cff4e5853c063e5da04880296603aa0227e544c", size = 248031, upload-time = "2026-07-22T11:40:11.007Z" }, + { url = "https://files.pythonhosted.org/packages/05/75/625bcdd74d5e6b2dca1ecba3c3ac77bcf8a026c21a649a46cef23e421f97/greenlet-3.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:f260930bbbbcf9caee661211235a5111c86dfe5832fdf6ae4570da1e0995320f", size = 246892, upload-time = "2026-07-22T11:40:27.357Z" }, + { url = "https://files.pythonhosted.org/packages/ec/69/35c62ed49c320cb4d98e14698ccca5467d3bfe683984172be9cb564d9ce3/greenlet-3.5.4-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:41ddab54e4b238f4a6c323f39b4e59e176affd5a94d461a9fb7583dac74240a3", size = 305571, upload-time = "2026-07-22T11:40:31.659Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6c/64d60216b3640dcb0b62d913dd9e0d80030c09115bb2e4ba70c95d10ca45/greenlet-3.5.4-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3dabe3e2809013052c68bdf0b7fa5f5f2859c43a80803131ad61af9cabd7867", size = 672568, upload-time = "2026-07-22T12:26:45.298Z" }, + { url = "https://files.pythonhosted.org/packages/5c/de/ba3ab0a96292e53039530333b0d2ae18d9e508f3a325cd7bf15f8172944c/greenlet-3.5.4-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:27d3f00718634d4520a3a150154ac5da36f257869d41321953375b90bfbbc72c", size = 680076, upload-time = "2026-07-22T12:29:06.125Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/aeada79083c6f1c15f45d77a332f9c441af263ee298e3eb17522cd337d22/greenlet-3.5.4-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbd60b5763c6543c1827e48faaf14ea9bfbad245f52b1a4d76a2a2d8884c6c66", size = 676733, upload-time = "2026-07-22T11:51:16.027Z" }, + { url = "https://files.pythonhosted.org/packages/e9/10/2392fc3a98948652ef5fd1e7275c04f861dd13f74b78a2b4309f4ee4d090/greenlet-3.5.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f00f910f0e7b35416c63b23ad78b769aeccfc1775f712b43c4ee525624a2eef7", size = 1637327, upload-time = "2026-07-22T12:25:07.879Z" }, + { url = "https://files.pythonhosted.org/packages/55/c6/e7237a3dfa1f205ed0d9ea1e46d70bd2811b32d516266399fb59d28ab90a/greenlet-3.5.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:91c26423753b92caf41ab3f98fd547d7374d4d9fc2d85be041886c1579d9255e", size = 1697493, upload-time = "2026-07-22T11:51:19.214Z" }, + { url = "https://files.pythonhosted.org/packages/55/e3/4ba8154ba2a3d43729e499f72471b4b5c993f3826d3e24da81d5f06d6572/greenlet-3.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:ee032b91fd8ec29ec6c4cea2b8c561b178435134bd0752c7334b94e9c736c132", size = 251637, upload-time = "2026-07-22T11:40:37.44Z" }, + { url = "https://files.pythonhosted.org/packages/90/03/e3f96dfc100261a29545ddc8270cafe58f9195b6651466910e820910de77/greenlet-3.5.4-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:178111881dd7a6c946471fda85485ec796e1043c2b939f694b096e2ecf986809", size = 296076, upload-time = "2026-07-22T11:39:38.364Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3d/da52d208e5c977bce8667e784729e584e38b5785f4c1ec0f4c836e9a1c42/greenlet-3.5.4-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d92df08dd65fede97fc37aad36c2e9dcda3b31c467f8e0c2c096456cb818e927", size = 666870, upload-time = "2026-07-22T12:26:46.691Z" }, + { url = "https://files.pythonhosted.org/packages/2d/8a/7e6dee25cb8a8cf9b362c8e597cc269593378bd916f16c736c059a52e85a/greenlet-3.5.4-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:99e8f8c4ebc4fd80aa26c1280ae9ad43a0976e786349703a181cf0bae60413e5", size = 677678, upload-time = "2026-07-22T12:29:07.508Z" }, + { url = "https://files.pythonhosted.org/packages/6c/21/5a38699fa45de749e3857d93b8f07e4c20489e77c2d35d915a2e1c456606/greenlet-3.5.4-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:394de08dad5ffcb1f50c2159d93e398d9d2da3ed437645eaa54771fa720db9f0", size = 676067, upload-time = "2026-07-22T11:51:18.163Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ba/863116ab8ff1ca7a729e327800268939d182db47aa433db70e216e7d9194/greenlet-3.5.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:c883d61f2282d72c767a14936641b3efcbde9d82f1080712aaea0b1d3126cb88", size = 1633489, upload-time = "2026-07-22T12:25:09.605Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/7466ced82818d6132462d7f26b3f83c66ea15d2b193a6c0088d558ed7d95/greenlet-3.5.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2a924f15d17957e252a810acefcb5942f5ca712298e8b6fcaed9a307d357522c", size = 1696584, upload-time = "2026-07-22T11:51:21.304Z" }, + { url = "https://files.pythonhosted.org/packages/f9/4d/55b638489260065de9ffce606c8b5d04507bef705de4b212a0c3d6a1a0df/greenlet-3.5.4-cp315-cp315-win_amd64.whl", hash = "sha256:ed17e5f3420360d5b459de8462efb52060399a5326a613d4cde31cef63ef95da", size = 248297, upload-time = "2026-07-22T11:42:04.055Z" }, + { url = "https://files.pythonhosted.org/packages/bb/08/9dd4ae635da93d41dc268bc34bd62a9d711ed8b8825c5d22ac910c7d6e6d/greenlet-3.5.4-cp315-cp315-win_arm64.whl", hash = "sha256:f908898d6fa484ce4b6f447ce70ea99b52c503fee419e53cf74d60a16bc9e667", size = 247423, upload-time = "2026-07-22T11:44:00.764Z" }, + { url = "https://files.pythonhosted.org/packages/19/66/7c87ed9cdbf1d49c2c6cd1c7b9dd4d16c33b24235ca03972293a1876b30c/greenlet-3.5.4-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:1833637f17d5e7472548a48575c394fe39f1b1890d676d162d86593610f44d8c", size = 306487, upload-time = "2026-07-22T11:41:25.118Z" }, + { url = "https://files.pythonhosted.org/packages/5b/05/0a4201e7c0054866eefc05da234f236dd4c950d0fbf9ca0517141f01b269/greenlet-3.5.4-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12cda9122e03341f1cb6b8207a19d7a9d375e52f1b4e9243918375f40fd7b4b9", size = 676479, upload-time = "2026-07-22T12:26:48.129Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c0/4b6b8c5a3aec70f0649cd89662d120fdd6421e2bdc8e3b15c3ab5ec568d8/greenlet-3.5.4-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d83ae0e32d14957ab7170785a20f582635c8474deab1bfbb552b17e769a6ce25", size = 684321, upload-time = "2026-07-22T12:29:08.925Z" }, + { url = "https://files.pythonhosted.org/packages/24/c9/b49c31c9a972eee91e260445770e922244a7efc542697f51a012ec046d0f/greenlet-3.5.4-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f1467de1bb767f75db0aa34c195e3a496d8d1278c796e70c24ce205d3e99cde", size = 681293, upload-time = "2026-07-22T11:51:20.43Z" }, + { url = "https://files.pythonhosted.org/packages/95/6f/7f2d4653770500eee667866016d42d7a68e3d3462f80df6b8e3fcd48a0eb/greenlet-3.5.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0fa53040b78b578120eecdc0265e3f1051487cc425d11a2b7c761daadf4feaa8", size = 1642474, upload-time = "2026-07-22T12:25:10.819Z" }, + { url = "https://files.pythonhosted.org/packages/5a/d8/8cba31036a4caae448087ba5d150660ab03b4a0f54d9150f6495a3be7262/greenlet-3.5.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:60e0bc961d367df506660e9ac0177a76bc6d81305300704b0977d1634f76efe2", size = 1701012, upload-time = "2026-07-22T11:51:23.17Z" }, + { url = "https://files.pythonhosted.org/packages/e3/cd/3f77a4cce3bae631b08eb52f53a82a976669600337e21dfdba811cb50267/greenlet-3.5.4-cp315-cp315t-win_amd64.whl", hash = "sha256:f680e549edb3eaf21eea4e7fe101e15ec180c74b7879ab46adc080f22d4015d2", size = 251977, upload-time = "2026-07-22T11:41:38.125Z" }, + { url = "https://files.pythonhosted.org/packages/93/e8/65e8707d00fe2a49bf12f609a9b2b39ba6dd23c2810eacad877c4fc94bfe/greenlet-3.5.4-cp315-cp315t-win_arm64.whl", hash = "sha256:08fc36de8442d5c3e95b044550dbea9bf144d31ec0cc58e36fb241cb6ef6a994", size = 250538, upload-time = "2026-07-22T11:40:17.985Z" }, +] + +[[package]] +name = "humanfriendly" +version = "10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyreadline3", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702, upload-time = "2021-09-17T21:40:43.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794, upload-time = "2021-09-17T21:40:39.897Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/f8/06549565caa026e540b7e7bab5c5a90eb7ca986015f4c48dace243cd24d9/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32cc0a5365239a6ea0c6ed461e8838d053b57e397443c0ca894dcc8e388d4374", size = 122802, upload-time = "2026-03-09T13:12:37.515Z" }, + { url = "https://files.pythonhosted.org/packages/84/eb/8476a0818850c563ff343ea7c9c05dcdcbd689a38e01aa31657df01f91fa/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cc0b66c1eec9021353a4b4483afb12dfd50e3669ffbb9152d6842eb34c7e29fd", size = 66216, upload-time = "2026-03-09T13:12:38.812Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/f9c8a6b4c21aed4198566e45923512986d6cef530e7263b3a5f823546561/kiwisolver-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86e0287879f75621ae85197b0877ed2f8b7aa57b511c7331dce2eb6f4de7d476", size = 63917, upload-time = "2026-03-09T13:12:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/f1/0e/ba4ae25d03722f64de8b2c13e80d82ab537a06b30fc7065183c6439357e3/kiwisolver-1.5.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:62f59da443c4f4849f73a51a193b1d9d258dcad0c41bc4d1b8fb2bcc04bfeb22", size = 1628776, upload-time = "2026-03-09T13:12:41.976Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e4/3f43a011bc8a0860d1c96f84d32fa87439d3feedf66e672fef03bf5e8bac/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9190426b7aa26c5229501fa297b8d0653cfd3f5a36f7990c264e157cbf886b3b", size = 1228164, upload-time = "2026-03-09T13:12:44.002Z" }, + { url = "https://files.pythonhosted.org/packages/4b/34/3a901559a1e0c218404f9a61a93be82d45cb8f44453ba43088644980f033/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c8277104ded0a51e699c8c3aff63ce2c56d4ed5519a5f73e0fd7057f959a2b9e", size = 1246656, upload-time = "2026-03-09T13:12:45.557Z" }, + { url = "https://files.pythonhosted.org/packages/87/9e/f78c466ea20527822b95ad38f141f2de1dcd7f23fb8716b002b0d91bbe59/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8f9baf6f0a6e7571c45c8863010b45e837c3ee1c2c77fcd6ef423be91b21fedb", size = 1295562, upload-time = "2026-03-09T13:12:47.562Z" }, + { url = "https://files.pythonhosted.org/packages/0a/66/fd0e4a612e3a286c24e6d6f3a5428d11258ed1909bc530ba3b59807fd980/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cff8e5383db4989311f99e814feeb90c4723eb4edca425b9d5d9c3fefcdd9537", size = 2178473, upload-time = "2026-03-09T13:12:50.254Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8e/6cac929e0049539e5ee25c1ee937556f379ba5204840d03008363ced662d/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ebae99ed6764f2b5771c522477b311be313e8841d2e0376db2b10922daebbba4", size = 2274035, upload-time = "2026-03-09T13:12:51.785Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d3/9d0c18f1b52ea8074b792452cf17f1f5a56bd0302a85191f405cfbf9da16/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d5cd5189fc2b6a538b75ae45433140c4823463918f7b1617c31e68b085c0022c", size = 2443217, upload-time = "2026-03-09T13:12:53.329Z" }, + { url = "https://files.pythonhosted.org/packages/45/2a/6e19368803a038b2a90857bf4ee9e3c7b667216d045866bf22d3439fd75e/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f42c23db5d1521218a3276bb08666dcb662896a0be7347cba864eca45ff64ede", size = 2249196, upload-time = "2026-03-09T13:12:55.057Z" }, + { url = "https://files.pythonhosted.org/packages/75/2b/3f641dfcbe72e222175d626bacf2f72c3b34312afec949dd1c50afa400f5/kiwisolver-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:94eff26096eb5395136634622515b234ecb6c9979824c1f5004c6e3c3c85ccd2", size = 73389, upload-time = "2026-03-09T13:12:56.496Z" }, + { url = "https://files.pythonhosted.org/packages/da/88/299b137b9e0025d8982e03d2d52c123b0a2b159e84b0ef1501ef446339cf/kiwisolver-1.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:dd952e03bfbb096cfe2dd35cd9e00f269969b67536cb4370994afc20ff2d0875", size = 64782, upload-time = "2026-03-09T13:12:57.609Z" }, + { url = "https://files.pythonhosted.org/packages/12/dd/a495a9c104be1c476f0386e714252caf2b7eca883915422a64c50b88c6f5/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9eed0f7edbb274413b6ee781cca50541c8c0facd3d6fd289779e494340a2b85c", size = 122798, upload-time = "2026-03-09T13:12:58.963Z" }, + { url = "https://files.pythonhosted.org/packages/11/60/37b4047a2af0cf5ef6d8b4b26e91829ae6fc6a2d1f74524bcb0e7cd28a32/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c4923e404d6bcd91b6779c009542e5647fef32e4a5d75e115e3bbac6f2335eb", size = 66216, upload-time = "2026-03-09T13:13:00.155Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/510dc933d87767584abfe03efa445889996c70c2990f6f87c3ebaa0a18c5/kiwisolver-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac", size = 63911, upload-time = "2026-03-09T13:13:01.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/46/bddc13df6c2a40741e0cc7865bb1c9ed4796b6760bd04ce5fae3928ef917/kiwisolver-1.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27", size = 1438209, upload-time = "2026-03-09T13:13:03.385Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d6/76621246f5165e5372f02f5e6f3f48ea336a8f9e96e43997d45b240ed8cd/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff710414307fefa903e0d9bdf300972f892c23477829f49504e59834f4195398", size = 1248888, upload-time = "2026-03-09T13:13:05.231Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c1/31559ec6fb39a5b48035ce29bb63ade628f321785f38c384dee3e2c08bc1/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6176c1811d9d5a04fa391c490cc44f451e240697a16977f11c6f722efb9041db", size = 1266304, upload-time = "2026-03-09T13:13:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ef/1cb8276f2d29cc6a41e0a042f27946ca347d3a4a75acf85d0a16aa6dcc82/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50847dca5d197fcbd389c805aa1a1cf32f25d2e7273dc47ab181a517666b68cc", size = 1319650, upload-time = "2026-03-09T13:13:08.607Z" }, + { url = "https://files.pythonhosted.org/packages/4c/e4/5ba3cecd7ce6236ae4a80f67e5d5531287337d0e1f076ca87a5abe4cd5d0/kiwisolver-1.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:01808c6d15f4c3e8559595d6d1fe6411c68e4a3822b4b9972b44473b24f4e679", size = 970949, upload-time = "2026-03-09T13:13:10.299Z" }, + { url = "https://files.pythonhosted.org/packages/5a/69/dc61f7ae9a2f071f26004ced87f078235b5507ab6e5acd78f40365655034/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1f9f4121ec58628c96baa3de1a55a4e3a333c5102c8e94b64e23bf7b2083309", size = 2199125, upload-time = "2026-03-09T13:13:11.841Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7b/abbe0f1b5afa85f8d084b73e90e5f801c0939eba16ac2e49af7c61a6c28d/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b7d335370ae48a780c6e6a6bbfa97342f563744c39c35562f3f367665f5c1de2", size = 2293783, upload-time = "2026-03-09T13:13:14.399Z" }, + { url = "https://files.pythonhosted.org/packages/8a/80/5908ae149d96d81580d604c7f8aefd0e98f4fd728cf172f477e9f2a81744/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:800ee55980c18545af444d93fdd60c56b580db5cc54867d8cbf8a1dc0829938c", size = 1960726, upload-time = "2026-03-09T13:13:16.047Z" }, + { url = "https://files.pythonhosted.org/packages/84/08/a78cb776f8c085b7143142ce479859cfec086bd09ee638a317040b6ef420/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c438f6ca858697c9ab67eb28246c92508af972e114cac34e57a6d4ba17a3ac08", size = 2464738, upload-time = "2026-03-09T13:13:17.897Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e1/65584da5356ed6cb12c63791a10b208860ac40a83de165cb6a6751a686e3/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c63c91f95173f9c2a67c7c526b2cea976828a0e7fced9cdcead2802dc10f8a4", size = 2270718, upload-time = "2026-03-09T13:13:19.421Z" }, + { url = "https://files.pythonhosted.org/packages/be/6c/28f17390b62b8f2f520e2915095b3c94d88681ecf0041e75389d9667f202/kiwisolver-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b", size = 73480, upload-time = "2026-03-09T13:13:20.818Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0e/2ee5debc4f77a625778fec5501ff3e8036fe361b7ee28ae402a485bb9694/kiwisolver-1.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:ad4ae4ffd1ee9cd11357b4c66b612da9888f4f4daf2f36995eda64bd45370cac", size = 64930, upload-time = "2026-03-09T13:13:21.997Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b2/818b74ebea34dabe6d0c51cb1c572e046730e64844da6ed646d5298c40ce/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9", size = 123158, upload-time = "2026-03-09T13:13:23.127Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d9/405320f8077e8e1c5c4bd6adc45e1e6edf6d727b6da7f2e2533cf58bff71/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588", size = 66388, upload-time = "2026-03-09T13:13:24.765Z" }, + { url = "https://files.pythonhosted.org/packages/99/9f/795fedf35634f746151ca8839d05681ceb6287fbed6cc1c9bf235f7887c2/kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819", size = 64068, upload-time = "2026-03-09T13:13:25.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/13/680c54afe3e65767bed7ec1a15571e1a2f1257128733851ade24abcefbcc/kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f", size = 1477934, upload-time = "2026-03-09T13:13:27.166Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2f/cebfcdb60fd6a9b0f6b47a9337198bcbad6fbe15e68189b7011fd914911f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf", size = 1278537, upload-time = "2026-03-09T13:13:28.707Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0d/9b782923aada3fafb1d6b84e13121954515c669b18af0c26e7d21f579855/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d", size = 1296685, upload-time = "2026-03-09T13:13:30.528Z" }, + { url = "https://files.pythonhosted.org/packages/27/70/83241b6634b04fe44e892688d5208332bde130f38e610c0418f9ede47ded/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083", size = 1346024, upload-time = "2026-03-09T13:13:32.818Z" }, + { url = "https://files.pythonhosted.org/packages/e4/db/30ed226fb271ae1a6431fc0fe0edffb2efe23cadb01e798caeb9f2ceae8f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6", size = 987241, upload-time = "2026-03-09T13:13:34.435Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bd/c314595208e4c9587652d50959ead9e461995389664e490f4dce7ff0f782/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1", size = 2227742, upload-time = "2026-03-09T13:13:36.4Z" }, + { url = "https://files.pythonhosted.org/packages/c1/43/0499cec932d935229b5543d073c2b87c9c22846aab48881e9d8d6e742a2d/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0", size = 2323966, upload-time = "2026-03-09T13:13:38.204Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6f/79b0d760907965acfd9d61826a3d41f8f093c538f55cd2633d3f0db269f6/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15", size = 1977417, upload-time = "2026-03-09T13:13:39.966Z" }, + { url = "https://files.pythonhosted.org/packages/ab/31/01d0537c41cb75a551a438c3c7a80d0c60d60b81f694dac83dd436aec0d0/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314", size = 2491238, upload-time = "2026-03-09T13:13:41.698Z" }, + { url = "https://files.pythonhosted.org/packages/e4/34/8aefdd0be9cfd00a44509251ba864f5caf2991e36772e61c408007e7f417/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9", size = 2294947, upload-time = "2026-03-09T13:13:43.343Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/0348374369ca588f8fe9c338fae49fa4e16eeb10ffb3d012f23a54578a9e/kiwisolver-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384", size = 73569, upload-time = "2026-03-09T13:13:45.792Z" }, + { url = "https://files.pythonhosted.org/packages/28/26/192b26196e2316e2bd29deef67e37cdf9870d9af8e085e521afff0fed526/kiwisolver-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7", size = 64997, upload-time = "2026-03-09T13:13:46.878Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/024d6711d5ba575aa65d5538042e99964104e97fa153a9f10bc369182bc2/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09", size = 123166, upload-time = "2026-03-09T13:13:48.032Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/adbb40df306f587054a348831220812b9b1d787aff714cfbc8556e38fccd/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3", size = 66395, upload-time = "2026-03-09T13:13:49.365Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd", size = 64065, upload-time = "2026-03-09T13:13:50.562Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3", size = 1477903, upload-time = "2026-03-09T13:13:52.084Z" }, + { url = "https://files.pythonhosted.org/packages/18/d8/55638d89ffd27799d5cc3d8aa28e12f4ce7a64d67b285114dbedc8ea4136/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96", size = 1278751, upload-time = "2026-03-09T13:13:54.673Z" }, + { url = "https://files.pythonhosted.org/packages/b8/97/b4c8d0d18421ecceba20ad8701358453b88e32414e6f6950b5a4bad54e65/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099", size = 1296793, upload-time = "2026-03-09T13:13:56.287Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/f862f94b6389d8957448ec9df59450b81bec4abb318805375c401a1e6892/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8", size = 1346041, upload-time = "2026-03-09T13:13:58.269Z" }, + { url = "https://files.pythonhosted.org/packages/a3/6a/f1650af35821eaf09de398ec0bc2aefc8f211f0cda50204c9f1673741ba9/kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87", size = 987292, upload-time = "2026-03-09T13:13:59.871Z" }, + { url = "https://files.pythonhosted.org/packages/de/19/d7fb82984b9238115fe629c915007be608ebd23dc8629703d917dbfaffd4/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23", size = 2227865, upload-time = "2026-03-09T13:14:01.401Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/46b7f386589fd222dac9e9de9c956ce5bcefe2ee73b4e79891381dda8654/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859", size = 2324369, upload-time = "2026-03-09T13:14:02.972Z" }, + { url = "https://files.pythonhosted.org/packages/92/8b/95e237cf3d9c642960153c769ddcbe278f182c8affb20cecc1cc983e7cc5/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902", size = 1977989, upload-time = "2026-03-09T13:14:04.503Z" }, + { url = "https://files.pythonhosted.org/packages/1b/95/980c9df53501892784997820136c01f62bc1865e31b82b9560f980c0e649/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167", size = 2491645, upload-time = "2026-03-09T13:14:06.106Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/900647fd0840abebe1561792c6b31e6a7c0e278fc3973d30572a965ca14c/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0", size = 2295237, upload-time = "2026-03-09T13:14:08.891Z" }, + { url = "https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276", size = 73573, upload-time = "2026-03-09T13:14:12.327Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d2/64be2e429eb4fca7f7e1c52a91b12663aeaf25de3895e5cca0f47ef2a8d0/kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c", size = 64998, upload-time = "2026-03-09T13:14:13.469Z" }, + { url = "https://files.pythonhosted.org/packages/b0/69/ce68dd0c85755ae2de490bf015b62f2cea5f6b14ff00a463f9d0774449ff/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1", size = 125700, upload-time = "2026-03-09T13:14:14.636Z" }, + { url = "https://files.pythonhosted.org/packages/74/aa/937aac021cf9d4349990d47eb319309a51355ed1dbdc9c077cdc9224cb11/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e", size = 67537, upload-time = "2026-03-09T13:14:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/ee/20/3a87fbece2c40ad0f6f0aefa93542559159c5f99831d596050e8afae7a9f/kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7", size = 65514, upload-time = "2026-03-09T13:14:18.035Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7f/f943879cda9007c45e1f7dba216d705c3a18d6b35830e488b6c6a4e7cdf0/kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c", size = 1584848, upload-time = "2026-03-09T13:14:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/37/f8/4d4f85cc1870c127c88d950913370dd76138482161cd07eabbc450deff01/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368", size = 1391542, upload-time = "2026-03-09T13:14:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/04/0b/65dd2916c84d252b244bd405303220f729e7c17c9d7d33dca6feeff9ffc4/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489", size = 1404447, upload-time = "2026-03-09T13:14:23.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/2606a373247babce9b1d056c03a04b65f3cf5290a8eac5d7bdead0a17e21/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1", size = 1455918, upload-time = "2026-03-09T13:14:24.74Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d1/c6078b5756670658e9192a2ef11e939c92918833d2745f85cd14a6004bdf/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3", size = 1072856, upload-time = "2026-03-09T13:14:26.597Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c8/7def6ddf16eb2b3741d8b172bdaa9af882b03c78e9b0772975408801fa63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18", size = 2333580, upload-time = "2026-03-09T13:14:28.237Z" }, + { url = "https://files.pythonhosted.org/packages/9e/87/2ac1fce0eb1e616fcd3c35caa23e665e9b1948bb984f4764790924594128/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021", size = 2423018, upload-time = "2026-03-09T13:14:30.018Z" }, + { url = "https://files.pythonhosted.org/packages/67/13/c6700ccc6cc218716bfcda4935e4b2997039869b4ad8a94f364c5a3b8e63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310", size = 2062804, upload-time = "2026-03-09T13:14:32.888Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/877056304626943ff0f1f44c08f584300c199b887cb3176cd7e34f1515f1/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3", size = 2597482, upload-time = "2026-03-09T13:14:34.971Z" }, + { url = "https://files.pythonhosted.org/packages/75/19/c60626c47bf0f8ac5dcf72c6c98e266d714f2fbbfd50cf6dab5ede3aaa50/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2", size = 2394328, upload-time = "2026-03-09T13:14:36.816Z" }, + { url = "https://files.pythonhosted.org/packages/47/84/6a6d5e5bb8273756c27b7d810d47f7ef2f1f9b9fd23c9ee9a3f8c75c9cef/kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53", size = 68410, upload-time = "2026-03-09T13:14:38.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/060f45052f2a01ad5762c8fdecd6d7a752b43400dc29ff75cd47225a40fd/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615", size = 123231, upload-time = "2026-03-09T13:14:41.323Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/78da680eadd06ff35edef6ef68a1ad273bad3e2a0936c9a885103230aece/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02", size = 66489, upload-time = "2026-03-09T13:14:42.534Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e", size = 64063, upload-time = "2026-03-09T13:14:44.759Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac", size = 1475913, upload-time = "2026-03-09T13:14:46.247Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05", size = 1282782, upload-time = "2026-03-09T13:14:48.458Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9f/1de7aad00697325f05238a5f2eafbd487fb637cc27a558b5367a5f37fb7f/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd", size = 1300815, upload-time = "2026-03-09T13:14:50.721Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c2/297f25141d2e468e0ce7f7a7b92e0cf8918143a0cbd3422c1ad627e85a06/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a", size = 1347925, upload-time = "2026-03-09T13:14:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d3/f4c73a02eb41520c47610207b21afa8cdd18fdbf64ffd94674ae21c4812d/kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554", size = 991322, upload-time = "2026-03-09T13:14:54.637Z" }, + { url = "https://files.pythonhosted.org/packages/7b/46/d3f2efef7732fcda98d22bf4ad5d3d71d545167a852ca710a494f4c15343/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581", size = 2232857, upload-time = "2026-03-09T13:14:56.471Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ec/2d9756bf2b6d26ae4349b8d3662fb3993f16d80c1f971c179ce862b9dbae/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303", size = 2329376, upload-time = "2026-03-09T13:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9f/876a0a0f2260f1bde92e002b3019a5fabc35e0939c7d945e0fa66185eb20/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9", size = 1982549, upload-time = "2026-03-09T13:14:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/ba3624dfac23a64d54ac4179832860cb537c1b0af06024936e82ca4154a0/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79", size = 2494680, upload-time = "2026-03-09T13:15:01.364Z" }, + { url = "https://files.pythonhosted.org/packages/39/b7/97716b190ab98911b20d10bf92eca469121ec483b8ce0edd314f51bc85af/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796", size = 2297905, upload-time = "2026-03-09T13:15:03.925Z" }, + { url = "https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e", size = 75086, upload-time = "2026-03-09T13:15:07.775Z" }, + { url = "https://files.pythonhosted.org/packages/70/15/9b90f7df0e31a003c71649cf66ef61c3c1b862f48c81007fa2383c8bd8d7/kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df", size = 66577, upload-time = "2026-03-09T13:15:09.139Z" }, + { url = "https://files.pythonhosted.org/packages/17/01/7dc8c5443ff42b38e72731643ed7cf1ed9bf01691ae5cdca98501999ed83/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e", size = 125794, upload-time = "2026-03-09T13:15:10.525Z" }, + { url = "https://files.pythonhosted.org/packages/46/8a/b4ebe46ebaac6a303417fab10c2e165c557ddaff558f9699d302b256bc53/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4", size = 67646, upload-time = "2026-03-09T13:15:12.016Z" }, + { url = "https://files.pythonhosted.org/packages/60/35/10a844afc5f19d6f567359bf4789e26661755a2f36200d5d1ed8ad0126e5/kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028", size = 65511, upload-time = "2026-03-09T13:15:13.311Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8a/685b297052dd041dcebce8e8787b58923b6e78acc6115a0dc9189011c44b/kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657", size = 1584858, upload-time = "2026-03-09T13:15:15.103Z" }, + { url = "https://files.pythonhosted.org/packages/9e/80/04865e3d4638ac5bddec28908916df4a3075b8c6cc101786a96803188b96/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920", size = 1392539, upload-time = "2026-03-09T13:15:16.661Z" }, + { url = "https://files.pythonhosted.org/packages/ba/01/77a19cacc0893fa13fafa46d1bba06fb4dc2360b3292baf4b56d8e067b24/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9", size = 1405310, upload-time = "2026-03-09T13:15:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/53/39/bcaf5d0cca50e604cfa9b4e3ae1d64b50ca1ae5b754122396084599ef903/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d", size = 1456244, upload-time = "2026-03-09T13:15:20.444Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7a/72c187abc6975f6978c3e39b7cf67aeb8b3c0a8f9790aa7fd412855e9e1f/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65", size = 1073154, upload-time = "2026-03-09T13:15:22.039Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ca/cf5b25783ebbd59143b4371ed0c8428a278abe68d6d0104b01865b1bbd0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa", size = 2334377, upload-time = "2026-03-09T13:15:23.741Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e5/b1f492adc516796e88751282276745340e2a72dcd0d36cf7173e0daf3210/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0", size = 2425288, upload-time = "2026-03-09T13:15:25.789Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e5/9b21fbe91a61b8f409d74a26498706e97a48008bfcd1864373d32a6ba31c/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", size = 2063158, upload-time = "2026-03-09T13:15:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/b1/02/83f47986138310f95ea95531f851b2a62227c11cbc3e690ae1374fe49f0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f", size = 2597260, upload-time = "2026-03-09T13:15:29.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/18/43a5f24608d8c313dd189cf838c8e68d75b115567c6279de7796197cfb6a/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646", size = 2394403, upload-time = "2026-03-09T13:15:31.517Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b5/98222136d839b8afabcaa943b09bd05888c2d36355b7e448550211d1fca4/kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681", size = 79687, upload-time = "2026-03-09T13:15:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/99/a2/ca7dc962848040befed12732dff6acae7fb3c4f6fc4272b3f6c9a30b8713/kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57", size = 70032, upload-time = "2026-03-09T13:15:34.411Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fa/2910df836372d8761bb6eff7d8bdcb1613b5c2e03f260efe7abe34d388a7/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797", size = 130262, upload-time = "2026-03-09T13:15:35.629Z" }, + { url = "https://files.pythonhosted.org/packages/0f/41/c5f71f9f00aabcc71fee8b7475e3f64747282580c2fe748961ba29b18385/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203", size = 138036, upload-time = "2026-03-09T13:15:36.894Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/7399a607f434119c6e1fdc8ec89a8d51ccccadf3341dee4ead6bd14caaf5/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7", size = 194295, upload-time = "2026-03-09T13:15:38.22Z" }, + { url = "https://files.pythonhosted.org/packages/b5/91/53255615acd2a1eaca307ede3c90eb550bae9c94581f8c00081b6b1c8f44/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57", size = 75987, upload-time = "2026-03-09T13:15:39.65Z" }, + { url = "https://files.pythonhosted.org/packages/17/6f/6fd4f690a40c2582fa34b97d2678f718acf3706b91d270c65ecb455d0a06/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:295d9ffe712caa9f8a3081de8d32fc60191b4b51c76f02f951fd8407253528f4", size = 59606, upload-time = "2026-03-09T13:15:40.81Z" }, + { url = "https://files.pythonhosted.org/packages/82/a0/2355d5e3b338f13ce63f361abb181e3b6ea5fffdb73f739b3e80efa76159/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:51e8c4084897de9f05898c2c2a39af6318044ae969d46ff7a34ed3f96274adca", size = 57537, upload-time = "2026-03-09T13:15:42.071Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b9/1d50e610ecadebe205b71d6728fd224ce0e0ca6aba7b9cbe1da049203ac5/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b83af57bdddef03c01a9138034c6ff03181a3028d9a1003b301eb1a55e161a3f", size = 79888, upload-time = "2026-03-09T13:15:43.317Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ee/b85ffcd75afed0357d74f0e6fc02a4507da441165de1ca4760b9f496390d/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf4679a3d71012a7c2bf360e5cd878fbd5e4fcac0896b56393dec239d81529ed", size = 77584, upload-time = "2026-03-09T13:15:44.605Z" }, + { url = "https://files.pythonhosted.org/packages/6b/dd/644d0dde6010a8583b4cd66dd41c5f83f5325464d15c4f490b3340ab73b4/kiwisolver-1.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:41024ed50e44ab1a60d3fe0a9d15a4ccc9f5f2b1d814ff283c8d01134d5b81bc", size = 73390, upload-time = "2026-03-09T13:15:45.832Z" }, + { url = "https://files.pythonhosted.org/packages/e9/eb/5fcbbbf9a0e2c3a35effb88831a483345326bbc3a030a3b5b69aee647f84/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ec4c85dc4b687c7f7f15f553ff26a98bfe8c58f5f7f0ac8905f0ba4c7be60232", size = 59532, upload-time = "2026-03-09T13:15:47.047Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9b/e17104555bb4db148fd52327feea1e96be4b88e8e008b029002c281a21ab/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:12e91c215a96e39f57989c8912ae761286ac5a9584d04030ceb3368a357f017a", size = 57420, upload-time = "2026-03-09T13:15:48.199Z" }, + { url = "https://files.pythonhosted.org/packages/48/44/2b5b95b7aa39fb2d8d9d956e0f3d5d45aef2ae1d942d4c3ffac2f9cfed1a/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be4a51a55833dc29ab5d7503e7bcb3b3af3402d266018137127450005cdfe737", size = 79892, upload-time = "2026-03-09T13:15:49.694Z" }, + { url = "https://files.pythonhosted.org/packages/52/7d/7157f9bba6b455cfb4632ed411e199fc8b8977642c2b12082e1bd9e6d173/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daae526907e262de627d8f70058a0f64acc9e2641c164c99c8f594b34a799a16", size = 77603, upload-time = "2026-03-09T13:15:50.945Z" }, + { url = "https://files.pythonhosted.org/packages/0a/dd/8050c947d435c8d4bc94e3252f4d8bb8a76cfb424f043a8680be637a57f1/kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1", size = 73558, upload-time = "2026-03-09T13:15:52.112Z" }, +] + +[[package]] +name = "librt" +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/9b/356320fbae2ac8467e21c5e73e1389c80468e4998c62cc7d3536cc51b614/librt-0.15.0.tar.gz", hash = "sha256:4e66cbe84437497d951b799d3e1551291b6fb3d643820a7014b3655d57a59162", size = 214338, upload-time = "2026-08-07T10:49:42.663Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/12/e2e9ca532cf5a0e08c9489826c4a35c6958c92ba0313fda70e8c6c3912be/librt-0.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1a49adf16a7c9d9646816c2946135527197b6fcf4347c7b8b761cf1bfbf4489", size = 148673, upload-time = "2026-08-07T10:46:22.569Z" }, + { url = "https://files.pythonhosted.org/packages/6d/7c/02005e23478bd5950618d9712e0fd2b4c511657857f3efd8ba6a5feabcdd/librt-0.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81a398f45b45a59200e13cd5ad1ae1d3f44334de98b148331afe2cdfee701c52", size = 153547, upload-time = "2026-08-07T10:46:23.931Z" }, + { url = "https://files.pythonhosted.org/packages/a0/90/d8848a735f5642077fc4b3b4bebcdb08edf10178e3add45597f5201a368f/librt-0.15.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4eafbaff06b9563f8b1c850621ce51605de05208e09d4d71ce490bc972b7b9e8", size = 494355, upload-time = "2026-08-07T10:46:25.122Z" }, + { url = "https://files.pythonhosted.org/packages/e1/0b/8604f41ea02feace490e9e405a338a15f9905369f55b239a9ce31c946f24/librt-0.15.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b0411b4066db926b80258c60dcb0e6db4c9cee312eab45b7e8866b17ddf9ada1", size = 485459, upload-time = "2026-08-07T10:46:26.447Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ac/84153bda1ce0da609182527ab92b40d961809e544eefdc5a1c2422971416/librt-0.15.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:febb1ce6cac545a54e6b769982824e955a700fdd9fbf3a08a3d82c990968b57d", size = 498398, upload-time = "2026-08-07T10:46:27.701Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3a/5ca6cd282b2c244bec8ec84102e09773264e9c02891d56ab3a8f0e4d7083/librt-0.15.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b230acc1c3bfe2d6f2627ba2b95dc92e58aa494600e9722d0e6ccbc931e59702", size = 515474, upload-time = "2026-08-07T10:46:28.9Z" }, + { url = "https://files.pythonhosted.org/packages/73/d3/bd34110234779eb843c6ed66aba7c9b2091d3dd85989f1fb9922f564cb7a/librt-0.15.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6da110e5f314c19ab8478464d02ae18808ae73d522c15260fa4918acdcd64da9", size = 509484, upload-time = "2026-08-07T10:46:30.124Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/43c3f7f071d71631a7daa3b835ef2168ea39f20692d81464d4e47fbaa6d6/librt-0.15.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:eab9208b00ca55bf75983ec99f7bf13acc746a36102e98953addaad7f7ea1e1b", size = 532534, upload-time = "2026-08-07T10:46:31.511Z" }, + { url = "https://files.pythonhosted.org/packages/c5/1c/b854adf036ea817c40408873a5b794d65a91d9f0f39826f2ad2a2d5d7f48/librt-0.15.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6c013cd3a1721e69e14380ada97eaa4b7b0cdf1c6b96fa765d4ea47c875088db", size = 537087, upload-time = "2026-08-07T10:46:32.734Z" }, + { url = "https://files.pythonhosted.org/packages/25/5c/c9a890e244e7dd725d3bd8b560e41f0aec787eaf343b46956a290ab7b841/librt-0.15.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:567b1c430f8bd560e689421468278ac5941bab4a05303b5d95b6ae10db03f451", size = 536575, upload-time = "2026-08-07T10:46:33.965Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c5/c8e70b60b704299555f55db468eb46b1c81bfc60201ffbfe20407d89870c/librt-0.15.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:29c4cab9df457b19672c39be7f384ebb2bc925c4e2684b8780c222b43eb36389", size = 517142, upload-time = "2026-08-07T10:46:35.577Z" }, + { url = "https://files.pythonhosted.org/packages/56/d1/767a90c41f5d381b3195bc88ac0ec4afda35777c9c781e1f9848fedd965e/librt-0.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bccbd8e5b0bffb7106cf18eb1baa3d7194b1cebb3b4b1cdbd4bdb19382a6ee6c", size = 558714, upload-time = "2026-08-07T10:46:36.829Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b4/3c0624b8dc8301ab808f2b3a910995bcabe28df070fb9a0e5505ae997dae/librt-0.15.0-cp310-cp310-win32.whl", hash = "sha256:8ae493ed5f659a7761c43d42f183db514536073ded9bcf671d2d1df47e29a07e", size = 104426, upload-time = "2026-08-07T10:46:38.594Z" }, + { url = "https://files.pythonhosted.org/packages/31/98/e91c0382304bedb2db9c6801897319a9dcb68daac5e975819b562362f20d/librt-0.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:bc25fb356d0c7810bb49ff3df908ad1fda6995d660ab099ded69244ed7ab6053", size = 125057, upload-time = "2026-08-07T10:46:40.052Z" }, + { url = "https://files.pythonhosted.org/packages/59/52/06790ced2ac7117f890c21bda43c39c958ec82aa665c0718e821d33ff939/librt-0.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:823b92cf3c18ecd08afc70c42473888b41b6e8ef5046f3b82c05c154a2fa3d22", size = 148039, upload-time = "2026-08-07T10:46:41.165Z" }, + { url = "https://files.pythonhosted.org/packages/e7/1d/8e150b7fc449a1f33c8a760965cc1f43b14fc1577d9d0b50ab2701420e74/librt-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c70bc1b602cf59917e8f0c7a2cbc8bcc6fbc14d5486136b00707a79619121d63", size = 153067, upload-time = "2026-08-07T10:46:42.418Z" }, + { url = "https://files.pythonhosted.org/packages/51/87/a162bc5a66a35599dc619ecb215145f4de7d68e886b479b6d12593139f7c/librt-0.15.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:814ff83a25b5fce8b9c80c4dd803153fb5c5599fc74db9e022466938368957ef", size = 493087, upload-time = "2026-08-07T10:46:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/e5/3a/aeea1fc620cf48060d3065b37614edbf97043c099d0f50782bc8ca61d897/librt-0.15.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:57f5eeb6ad4c180de583b1038e61fe5fbd9796bb69a8a1c1a0c7ddbec4c8c60f", size = 485608, upload-time = "2026-08-07T10:46:45.038Z" }, + { url = "https://files.pythonhosted.org/packages/52/ff/fe571ad416f0856fd0d5578ffc2e6dc531891e586e36b647bcf50569cab8/librt-0.15.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82909c8f7eb9952656b65d3147afde4cf8e6d5a991eebc86418b5e65843b0ab8", size = 498723, upload-time = "2026-08-07T10:46:46.35Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e1/7a65eb5dedb1f00aebd948cdd8e17add48bf066cab3514e9daf84ab45a6c/librt-0.15.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f779070399f991400fc451719e0ea388eb7de313388bada2c127a35de05f798a", size = 516002, upload-time = "2026-08-07T10:46:47.599Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/59832b0ebfbd08c2742e6ece372ceb53f18bf1faef5d33c8daf3abebf749/librt-0.15.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bac89069bc496ebdf4f79ebb57bbd10d0b214c8454225deb672d91002bd17e18", size = 508607, upload-time = "2026-08-07T10:46:48.873Z" }, + { url = "https://files.pythonhosted.org/packages/ea/0d/37fa73f3b43ebd8259f91ae9102a15e5a54e65d581e48dea72df3e81d7a4/librt-0.15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e0d00c708fb2f5822b152429b1ac80a58dbbbc3f6c232c4d13a3f7fcf2ea5b4c", size = 530422, upload-time = "2026-08-07T10:46:50.45Z" }, + { url = "https://files.pythonhosted.org/packages/26/02/e046c6fe7a5881ac34623242192f484426ba8a75595fd18f22c53a3f530f/librt-0.15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6c6624fe268625869485553dd7cc1daf30d22558215bb2a4ff16f67a9801a31a", size = 534303, upload-time = "2026-08-07T10:46:51.693Z" }, + { url = "https://files.pythonhosted.org/packages/95/32/d5e6d861ab0366f3edf74f887ab0c9eb9f535aaf01d32b80b4f734daa179/librt-0.15.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f56b397858a23dacf35ede366ed2212fdc03a6a57a1ad36468ad6e9dc5fac091", size = 536084, upload-time = "2026-08-07T10:46:52.951Z" }, + { url = "https://files.pythonhosted.org/packages/2a/de/d69d725513fe53fc90c6d7a1f86e4428939bad2fb905b17fe4c18d413dde/librt-0.15.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4388184646efe2054911c5b00a1077d6d1ee86a95b7e8ba96dc7850a809f3f40", size = 514307, upload-time = "2026-08-07T10:46:54.194Z" }, + { url = "https://files.pythonhosted.org/packages/36/93/f8aded0d6682b4f25820fa86e0690f87f01df9fd7bd09ddb04d9167ad021/librt-0.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:97335f59082f9fe2ce6c2a9cc6433a0114bbb6cd4d5c09dd76c95c68b9f9a8b0", size = 557686, upload-time = "2026-08-07T10:46:55.443Z" }, + { url = "https://files.pythonhosted.org/packages/74/09/ffeb6bdeb6cd862b4272fddc8ad05f938dd25d020ed517e631813917d80a/librt-0.15.0-cp311-cp311-win32.whl", hash = "sha256:83380ffde38062a2e9bb55d83e74474f6614665528b98a6928720fc006dfffbb", size = 104917, upload-time = "2026-08-07T10:46:56.605Z" }, + { url = "https://files.pythonhosted.org/packages/96/28/7e2313a3ffbf0b4de7ba3da58a09e488507b4bd1ea2b5e69378354a23415/librt-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:f75720477ee05d509a310e856cacc8d909adc182f7b91193c207bcc26d7ee6db", size = 125886, upload-time = "2026-08-07T10:46:57.729Z" }, + { url = "https://files.pythonhosted.org/packages/39/9e/04b8c3cde014ef255ee785730425268354543acc38902093a40afa0dc164/librt-0.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:256237037a3ab001ae8d9803b2d43562a4c3aa38739843694349e4d5ebb0fd56", size = 111885, upload-time = "2026-08-07T10:46:58.787Z" }, + { url = "https://files.pythonhosted.org/packages/ba/39/99c25030e782bdfb7a21be8c05254806a2e4bbb05c8d50c2a2130acbfa05/librt-0.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e87bc679f86a99aa3b26e3c78eeb821a247c9a28eae48eaafcc32c3bf4c3bb9e", size = 151021, upload-time = "2026-08-07T10:47:00.057Z" }, + { url = "https://files.pythonhosted.org/packages/14/43/f4b1bd1b2888798a1409808889a25ea1ba49eaabce7d681ed27734c2df9d/librt-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71599e011ac880e8e45d46047d714871894c7d4ab6f25626f8d4f89da21f368d", size = 155267, upload-time = "2026-08-07T10:47:01.311Z" }, + { url = "https://files.pythonhosted.org/packages/0c/db/3ad9c965c72f1e1d6beeec44ec10a54e17be8ae042fbb4baade16cbadced/librt-0.15.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c802434092b769b1d613ed2e13fac15fbfce1934a74bd10283b03c0fae231cd1", size = 503136, upload-time = "2026-08-07T10:47:02.45Z" }, + { url = "https://files.pythonhosted.org/packages/4b/07/5888a6d76acd62ebce66c61b74d94e9370b9c32929f111e487bb6546f8ed/librt-0.15.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5500eeae393a184d14e1f35645962c27129d20c81afa4069e6ef826ebc2b3aaa", size = 496670, upload-time = "2026-08-07T10:47:03.675Z" }, + { url = "https://files.pythonhosted.org/packages/29/39/ab57cc2f5b276156da02bb7f5a8921bada1cb1993ffec99acf811c602c23/librt-0.15.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6ecfc32dfb46fb7b565bcd6abf9412acf978775a998273d22888a6d7953730dd", size = 513688, upload-time = "2026-08-07T10:47:04.981Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b9/bdbb0b648b5c2befb031f4c6f3b1dd857415e8fb492a25a3c764a6681e6c/librt-0.15.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cc46cfd15022e35084355478c9ac809d90b1152222706ac9a7655ec21df6fa", size = 531904, upload-time = "2026-08-07T10:47:06.211Z" }, + { url = "https://files.pythonhosted.org/packages/93/26/473c2e4b6c104e9e58e27ce95fc8005c8bd4fc36cae4f254371125a92db8/librt-0.15.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5f51401d102c885b9ca509e62c79b1dbff286e1b9b047fde6f763780789356d", size = 524427, upload-time = "2026-08-07T10:47:07.592Z" }, + { url = "https://files.pythonhosted.org/packages/26/60/03b3abb82b41714671b907bf6989b228e31e6a8af52dec82b5b0728dc250/librt-0.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cc30523e3f1a23fb7511cc659834a0d01a1042bb9de359bc1c131cc4ec6c9656", size = 543155, upload-time = "2026-08-07T10:47:08.866Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0e/9bb1f0a4affbd0a1888f4f79dc03ed2a299d9a2c26c59ab2a97dcbf11903/librt-0.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:59fe030d8ae4a57e3fb7756bf35a858de74e04066fc8555c53d0af979132af81", size = 546890, upload-time = "2026-08-07T10:47:10.327Z" }, + { url = "https://files.pythonhosted.org/packages/dc/84/6937a280d461f7de6e031ffb02edc2b7c3c90d49d630565ce8ff27cbc5f2/librt-0.15.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5a6526a2a956bbb1e4ae3568c82e650fc99119c66bb011ea60715744955a2b4d", size = 555163, upload-time = "2026-08-07T10:47:11.798Z" }, + { url = "https://files.pythonhosted.org/packages/bc/95/2a2853c1ee014bf102116e7f897a04beeaeb2461b45b79af98bdfb95f1ef/librt-0.15.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:85ea21ec6730194d67156b0e0b5430ccb1d61f8b8b907e39b37f9812b74a13f0", size = 535812, upload-time = "2026-08-07T10:47:13.279Z" }, + { url = "https://files.pythonhosted.org/packages/c9/4c/cf9601c1b4c5f09280acd5d83abdb2e68527a2be8257136eb42304218622/librt-0.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1e47b8ba865d7ede071a91a7163073bbaeb72541f1ef8a07d512c45c7b5007f2", size = 573688, upload-time = "2026-08-07T10:47:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/47/6d/9ac7cbec46189a7625af4b5acbd25f10d827f4141b2002181848c8418923/librt-0.15.0-cp312-cp312-win32.whl", hash = "sha256:a5207ec414d1c4a2a7231b2086970dc036f94293cdf338190984958a013a42f1", size = 106138, upload-time = "2026-08-07T10:47:15.973Z" }, + { url = "https://files.pythonhosted.org/packages/38/d0/2ae99c83be86ce23f925ac1aeeedc777e97f427c4a8d190c70d0a16e9a87/librt-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:73b30cfa976659b3917c8f6153bdb0591c6a9ec6583599fd24a689b690622022", size = 126974, upload-time = "2026-08-07T10:47:17.049Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ef/dd24f9635c730b86b87587967dda7516b1845e8b17684603d31607fed598/librt-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:a54cf9e0ef47b96af580849db5471142200568ce1e02cbf416addab551369570", size = 112292, upload-time = "2026-08-07T10:47:18.222Z" }, + { url = "https://files.pythonhosted.org/packages/e7/42/467b53a601b406ccd7b97c1fd54b59cb34f9185ad5ce7e9d5c3c4e8961c8/librt-0.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:db13ca398005abcbe538deda87b686d9bd08b7001cf40c4c06b444960ae10a26", size = 151029, upload-time = "2026-08-07T10:47:19.312Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e6/36c2299b7a94b84fdd01220d8a777a71be5be0925bb0dbdf71c0a06a34d9/librt-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa1f1995789dca3698bc550aaceb09a51bd5df0a057ff84ff15296cd1975b801", size = 155194, upload-time = "2026-08-07T10:47:20.398Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b6/ed5071f9325845e670bd36012757419767fbf56af77ed483077b9e4db541/librt-0.15.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55456ea87d8df21808446d03817be2f65e20391c1c615d9187440dff28cd08dc", size = 502568, upload-time = "2026-08-07T10:47:21.652Z" }, + { url = "https://files.pythonhosted.org/packages/7f/81/6450c67c3615d87704bcbc21323fafc69c799b06a044c447529f725d4b01/librt-0.15.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5a86a5a08c2235316bdb359d5dbb6ce0abfca7fac06363103e2c5af571d92f95", size = 496153, upload-time = "2026-08-07T10:47:22.925Z" }, + { url = "https://files.pythonhosted.org/packages/e1/d6/5f52b722bc75076954b3bfd49be15ea362df4d580c6fb315d0f617100d30/librt-0.15.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e56b6a368529bed262da40ce13f8fef590db0479819cca84f16a1f01ac356d0b", size = 513336, upload-time = "2026-08-07T10:47:24.213Z" }, + { url = "https://files.pythonhosted.org/packages/8d/e2/c08fd1d36ce63ea5a12b85c5d37f4550b5f86a692167e41e5a74222607ae/librt-0.15.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:234d8d394721fa0d786af15ebf1f3fb7f3ed82fd1cd0cde45c2f247b5d4281d2", size = 531661, upload-time = "2026-08-07T10:47:25.507Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d8/d9482fcbeb177b9eb87bb3899eeb3b42be690313c652f9e146b1d0681fb2/librt-0.15.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d8363d7accb0286ac3a0e633f396e93800dafb8150494505daf9515bbda591f3", size = 524487, upload-time = "2026-08-07T10:47:26.79Z" }, + { url = "https://files.pythonhosted.org/packages/10/cc/075171517b41f861753034fbb151b42cfc83bcc853849f24f5e66fd60ccf/librt-0.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0f0ee3644d951f31055ad07d77d92520e84505dd7a432cc4cd501dd70ee06785", size = 543201, upload-time = "2026-08-07T10:47:27.999Z" }, + { url = "https://files.pythonhosted.org/packages/b0/03/42c2330f37eeb475b6affeedd06518f60035f323af3a839335e3fc9fef2d/librt-0.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2cfd1a81a648806e6a7717be4cc4d1bb392fa229752bf8444ba365e381e984d6", size = 546467, upload-time = "2026-08-07T10:47:29.396Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/1ad4c5638f7e64d8560328bd25c54b409a661bdb6ff254b38ff90744288d/librt-0.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a6cd22c9da0d866558e46a041f1cc0c2bbb26b61b137b2347fa834c332e1d101", size = 555139, upload-time = "2026-08-07T10:47:30.815Z" }, + { url = "https://files.pythonhosted.org/packages/49/41/39fa7d15db1204cd1cbe6514680fbdc243adf754a0885061308f43afc013/librt-0.15.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6d5225ef8801e4ea5e482fa9b5dfb891dd9ef6f6d870f1f25d449ca2c70ac218", size = 536050, upload-time = "2026-08-07T10:47:32.222Z" }, + { url = "https://files.pythonhosted.org/packages/1e/88/c6dcf0dd8e26dc0c9a499a2abab8646c86dcaf9ecea9524cb46d3686331a/librt-0.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d28a05796b99f749bf8794f17ba9ba1612d0076b802e9cfc62c554634e9ce3b", size = 573700, upload-time = "2026-08-07T10:47:33.527Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9b/ab54c71a7918a7c34fa5327fb61390a77446a07a146fbfb1165250a61035/librt-0.15.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:2067ff438048cead9d223ca5675bae2a25e520a7c3e6c1498bf9c6892d22caab", size = 82194, upload-time = "2026-08-07T10:47:34.835Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b2/4f9a243bb892395f3becb80789ade13771701091f9f07ab8230247953ba8/librt-0.15.0-cp313-cp313-win32.whl", hash = "sha256:1cd3b721f24c206398b9e26da3c3a9c011e6e89d06f318ba8ebefc30f1003890", size = 106231, upload-time = "2026-08-07T10:47:36.251Z" }, + { url = "https://files.pythonhosted.org/packages/bf/af/64aff4885a40b93132382f2c314647d722574605416504379184ef3045ea/librt-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:f395a4a9a03ac062dbe9a9f82e0c720502e590a38feee6a757bc82e9c63afbd8", size = 126996, upload-time = "2026-08-07T10:47:37.453Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/335bccf6c7cb9028cb0b54aead27d9ece3f01f83bc6baa2abace5da655c1/librt-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:0a15cb554761247d84a3ec0cbdf4078d70725384f0e4662c0fa3b26266eb60ad", size = 112188, upload-time = "2026-08-07T10:47:38.729Z" }, + { url = "https://files.pythonhosted.org/packages/a8/93/949053fb462eecc4a9a5ee770a81f4b40be7b79538b245545d4aebc6b58b/librt-0.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f5de7feedc56337a088eb15cd9fafa9938367362221d8cc62c642b7f94821993", size = 149833, upload-time = "2026-08-07T10:47:39.86Z" }, + { url = "https://files.pythonhosted.org/packages/61/ca/8281aa6cd560a3420e4497729f6b704b53be3eeaaef82d5aeadddaf7441f/librt-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c0eb900c0e91f4aebe680845242e614f1864edfd44106380d0752ac29522bf8", size = 154088, upload-time = "2026-08-07T10:47:41.065Z" }, + { url = "https://files.pythonhosted.org/packages/dd/02/1a1662dceaba6a086360891448d5ce9a7d3555976cae59a31a39d744b9c7/librt-0.15.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8c9a650a188e38bac005048cbe6342e81407782944d01934540ab75e417df21", size = 494215, upload-time = "2026-08-07T10:47:42.388Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/99211619dc656370a3740c33d2b0b6d5a3fb1e73689314f6ed477a397dc4/librt-0.15.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:92bfed8deec93df30286b9fe9e3b1dd17329cc076a192b4ee5ec223841d54953", size = 491173, upload-time = "2026-08-07T10:47:43.683Z" }, + { url = "https://files.pythonhosted.org/packages/d4/aa/5448d0b05f4579b635d3899176817ebf561af0e57bacd425b5b1887264c1/librt-0.15.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec4b19788f835711a2072f9dbe6b03b3bf32ed1f0fb30cf399bdd59d9f0c33fa", size = 505512, upload-time = "2026-08-07T10:47:45.314Z" }, + { url = "https://files.pythonhosted.org/packages/95/82/01940e40b83c43a546c4a3c896cf34ca272a9690899d55914e4827b3dcce/librt-0.15.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4c7bacb70930f3d0a56f4ecf1be474a1f0d941b01dd73b756f3c256d42cb879", size = 523073, upload-time = "2026-08-07T10:47:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/88/fa/759c0030f3ee371439eb26de34fc745807caf0abb878af7af4b8b7c3dd3d/librt-0.15.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e79f05e4a08b4d880342673312bbc895b56df7765605796f15902eb5367d3ae", size = 515080, upload-time = "2026-08-07T10:47:48.319Z" }, + { url = "https://files.pythonhosted.org/packages/0b/27/894e072228fcb159703c655da69f8cd10dbed489c36e3df7dd032a2483be/librt-0.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a417149c0cba4d50b61e992e5a15e69eaf96746609b461cc4ed168aeef6b79dd", size = 534164, upload-time = "2026-08-07T10:47:49.875Z" }, + { url = "https://files.pythonhosted.org/packages/98/a3/0078e91c1f36f8815db17827de15650b9a3fe56c55fbf998c854b34e40d3/librt-0.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:da7a94d6a3411f579d72aa3e3bc5fbca7ed4549f3dbd7e5de3aa567333374285", size = 540616, upload-time = "2026-08-07T10:47:51.408Z" }, + { url = "https://files.pythonhosted.org/packages/86/33/81a29b796dd52a45e9ef7974c7732926e8f10f15b8d2be505665979f896d/librt-0.15.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:856f743ae607f2c1380eccb566c0038a9fb3eabf0fc2be2704d76d9f73557239", size = 545890, upload-time = "2026-08-07T10:47:52.818Z" }, + { url = "https://files.pythonhosted.org/packages/05/82/8be1baa1350e5d30cfd70ae79d0a6f4dc5862ef47f7bb2808aabc9bb86e5/librt-0.15.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:779a6e7c894737e5983e7790a9c78c4000c30e23c9aada08081bdbea53b0fa60", size = 523287, upload-time = "2026-08-07T10:47:54.165Z" }, + { url = "https://files.pythonhosted.org/packages/c6/4f/d1be6a01a35c20ef734e0e44113f87d4af756a9354a89dcfbe3b4f8af5e1/librt-0.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96bb17dbe8bab3c0954fbebfc69ed395599de75b6bbc35e3270a878e15d4dd65", size = 565868, upload-time = "2026-08-07T10:47:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/67/88/649cfa33f5825927b160610f670bdab012a64d627eddb94fa795ea4292fd/librt-0.15.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:7220697efaa6e5348fc3d18ee7f8563d4bfecd9872b37ffb915bfc1d08840622", size = 81619, upload-time = "2026-08-07T10:47:56.886Z" }, + { url = "https://files.pythonhosted.org/packages/22/31/8e88a8d5e48fc8d1a817787fb6811dfff6499acd6c8683dd83934aa6ede0/librt-0.15.0-cp314-cp314-win32.whl", hash = "sha256:f54598964d357b1c5ab77cf5d92f21e598fe0e23cdbe9618480807f81b4eba15", size = 100138, upload-time = "2026-08-07T10:47:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/80/92/20fd6c4b6a1b1a564b076d55cd3d427d8428217d7638dc25a654cc4791d4/librt-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ff5893a2c23d886aa9ce786de5ac6ddc74aeeaf90743682b74d920e117d2e28", size = 121258, upload-time = "2026-08-07T10:47:59.564Z" }, + { url = "https://files.pythonhosted.org/packages/fc/28/6af430b44d9ebb897b865a3c363b6dcace51357be2347cc0f8f869656a86/librt-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:3722a099730704c9a3d70c879fc0f51daec25fe5f1555672d97bc595abeafb95", size = 106467, upload-time = "2026-08-07T10:48:01.097Z" }, + { url = "https://files.pythonhosted.org/packages/7e/aa/b42bb798942ced219f6d63b27e07f91237887a8d0bd0921666db79a13790/librt-0.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:38c0c7d4b6fc06c3324b3f9162c8391bfc4fd9dde53afe1033ce7edb48d5a714", size = 159523, upload-time = "2026-08-07T10:48:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/75/03/1b53cd4ef904e73b1d828a5f90143bf94a2967d7cfff0b9ccf93e12aa9b4/librt-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8b2fdd7ead3c995c37940a790690660d0ca006c302db26cc51933f6766866fc3", size = 161638, upload-time = "2026-08-07T10:48:03.725Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c4/9f9c9fba097d49e9e694c2b4dc331df31884645ecbc58a93b4b5fc69d2c5/librt-0.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fde98cf1fc4bac144ce23c2c4c017b924ba714509ea9334977b0b27050c837d", size = 701795, upload-time = "2026-08-07T10:48:05.135Z" }, + { url = "https://files.pythonhosted.org/packages/4c/05/0966840bda0380c8ae167b9043c6230202941cc90ea29c48e096964c765e/librt-0.15.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:e3b461183c5fa7681b48560f91515f53a953122fb30c71e07abc67d7ddf58c38", size = 682147, upload-time = "2026-08-07T10:48:06.555Z" }, + { url = "https://files.pythonhosted.org/packages/18/af/1c47ca573c30ea47d195aec26133af522fea1104afaace028d7b32247ea8/librt-0.15.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4bbcc257e3babea20a91715c361b24554ec4e8f51aa578568afc230799fe1a19", size = 696397, upload-time = "2026-08-07T10:48:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/2e/0f/1aed6223d4f9f9d1171a8596ff100ea4c3f7699fea7a4ba657c3e60daa6c/librt-0.15.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b845b8d48088fad0cadc84be4b8fda63203be7e9237b71015b3925443c1f35ab", size = 722542, upload-time = "2026-08-07T10:48:09.569Z" }, + { url = "https://files.pythonhosted.org/packages/c6/22/9e3a929aea456c97d69e6ef3884efea56d4807f97399471cc946baebd8af/librt-0.15.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b30e600e8f337b9bd7f39b86d9fdfedc73cc46e3d0f745931a23a234220bb7e2", size = 729709, upload-time = "2026-08-07T10:48:11.129Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1b/c327ef6018e3a9ca0b8e7c5eddeeb331ba8f9b76c24e126d37d0f6d62faf/librt-0.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:64b0c8c35aa4c4ed79896359f3e0b285cbe4e610042106500da4811c322cc108", size = 752891, upload-time = "2026-08-07T10:48:12.558Z" }, + { url = "https://files.pythonhosted.org/packages/d7/d1/d5f1ea02c56930087009e39db9b70660a663e76c730b27b925d786718457/librt-0.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0da0d94cb802f32a0524653e7201f2cef72d5f700a5407678f5290483d4fcd08", size = 745301, upload-time = "2026-08-07T10:48:14.55Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3c/5f7c585d15ebb2250c73e7c0ee4e9e47be72c65d520c07ddbcdc62037674/librt-0.15.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4a6369168d371207339b1e50d4532b06a7121586141f82599505a3f315751d47", size = 747921, upload-time = "2026-08-07T10:48:16.453Z" }, + { url = "https://files.pythonhosted.org/packages/7f/52/1443a446486eba966bcbca1696b472e4f210320ec42f490a47f48fbf0fdc/librt-0.15.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c434e072557ade9cbc642d052c89d031efe47d5c9614523619d0d74a02378e81", size = 727561, upload-time = "2026-08-07T10:48:18.089Z" }, + { url = "https://files.pythonhosted.org/packages/79/91/2270a9380f11725cf83ce1925a5e32dd1dde2be9bba597f25c10a38644e7/librt-0.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7eec6a42018bc1d45763b1c162d3d2bf7c3b9a1b0ed30d3e91dcba390efefcc", size = 774417, upload-time = "2026-08-07T10:48:19.611Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/f4b1548d4f5b99186737fe27aec238e9823e8d5d23bf4df007c030689dc5/librt-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:6912fa5e635d74529ac7cdb1bdf6ca3af4453da8d1edbe0110ee1cb4ad407ebf", size = 104381, upload-time = "2026-08-07T10:48:21.048Z" }, + { url = "https://files.pythonhosted.org/packages/80/b6/134afad262def1de04c0843c376d02135f1168af43f22e09a52bd8394727/librt-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8e11699ed745931c395acd3621b07062e0f840efa6935aad87a64ed0995f0915", size = 127034, upload-time = "2026-08-07T10:48:22.561Z" }, + { url = "https://files.pythonhosted.org/packages/99/5f/1b6846b20572bd699c9e9ec321a5f781845bee477df2aa2a43b28bc40119/librt-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5d2a91724463bfed4f573cd7a9fdc856d2e230d0c0e5a61416a93481dccd8605", size = 110827, upload-time = "2026-08-07T10:48:23.804Z" }, + { url = "https://files.pythonhosted.org/packages/c6/44/4de9f4ddadb009a55c7758eb5736d62534a7daaf27bd71bc50e64b606b06/librt-0.15.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:8443e38dcfcfdbcf5add5118c623efd788d65ac2e25756d6251a54a06a4d0aca", size = 149843, upload-time = "2026-08-07T10:48:25.148Z" }, + { url = "https://files.pythonhosted.org/packages/1f/eb/5d9ab71e30119c44094e0275f38b47dd327aea0f843a080396677029d508/librt-0.15.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6d15a29033c57490cfe2069097c6fc4049e4e65ffbb749be7dc453b7c4c68965", size = 154510, upload-time = "2026-08-07T10:48:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9c/8505d1b8f5e8c19587bd03f7429993b3e9ce5c06819d856bfb11d919374c/librt-0.15.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d2c05c729b589e734c09578bf5964be48a911765484840d017bbc84f49d4c4ad", size = 497543, upload-time = "2026-08-07T10:48:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/1d/9a/3a8390775cb095765aded027ac9c63e7c8ea74e731498607544c6505de0e/librt-0.15.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:fa60887537e1d0cd2d9982269d33a709bf54b195cd2b9364fc0a758022af5bd9", size = 480452, upload-time = "2026-08-07T10:48:29.531Z" }, + { url = "https://files.pythonhosted.org/packages/e7/40/258a4a7117ee915d66de5cd9b8ade65a440993161107ce3a686f1859955c/librt-0.15.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d8bc24219b24c0af375718942ab75e3544b2763085f40f965be4326734ae8328", size = 507768, upload-time = "2026-08-07T10:48:31.007Z" }, + { url = "https://files.pythonhosted.org/packages/6b/c6/2f4dd296c97a0b85b98894519b279408ec9dd602d4f692b1ea0e25dee670/librt-0.15.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86a21a7bd3fe3a419512ef424cc1c020f6771d0b29cfddff36d1635a855e63f0", size = 525122, upload-time = "2026-08-07T10:48:32.7Z" }, + { url = "https://files.pythonhosted.org/packages/49/dd/29eab42be13b2bf0ea8cb227135a45d44693e30a7e8b92871981ff56b82b/librt-0.15.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dbab647e88d90b3167b91efe7091e248653688ed4337e4f90907a722c7361bb9", size = 520371, upload-time = "2026-08-07T10:48:34.294Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/4bad71adeca8fe208b775c2a35417fa5a2584c8f4791daaf89a89450fea1/librt-0.15.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d8edcf6f550e918dca779c069b9e156385c60b406f99fc7641f32c52f7193659", size = 537258, upload-time = "2026-08-07T10:48:35.88Z" }, + { url = "https://files.pythonhosted.org/packages/4c/63/59dba6143fdcc7240c54458b629f3250000a61b8945890fc9efd451b19c5/librt-0.15.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:8b62076030baa2d8b1501a46bf0e19c27a489aa90671c55665bff7887f7660b0", size = 527432, upload-time = "2026-08-07T10:48:37.466Z" }, + { url = "https://files.pythonhosted.org/packages/ec/21/21a24c6a2327d8362580efebe77286bf47b0f4062ec5ea41766e609d3c7d/librt-0.15.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:d00d20d1818e82a07a0ee0aa89a98b17ed7916b92441090b683719cb20a59b6d", size = 548108, upload-time = "2026-08-07T10:48:39.384Z" }, + { url = "https://files.pythonhosted.org/packages/5a/6d/fc68c89a7971418b41f9a873623ff935cb864097544c6a2f8ce491c8ef5d/librt-0.15.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4e6ee93fc3cf848dcbf0cce2eca73d8e7dcd0cc2b6df3a529d57750b30a4c55c", size = 529681, upload-time = "2026-08-07T10:48:41.392Z" }, + { url = "https://files.pythonhosted.org/packages/65/7e/c2d98766124400d722063a630b0fde38a9fc768705d37eecca15c47dc192/librt-0.15.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:32896a0af72508ea979e0acb4e4c04cbeeae04938167950d535c83c45597167d", size = 567736, upload-time = "2026-08-07T10:48:43.124Z" }, + { url = "https://files.pythonhosted.org/packages/55/6c/f8c34a95e3a515c6e1c192b89511e7253c89a7760c6b500d57ffdb8d2dc8/librt-0.15.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:ec3ba415afaf951f6951b1dd16d3c8e4f540065fc382d7e70b823a79567ca374", size = 81673, upload-time = "2026-08-07T10:48:44.645Z" }, + { url = "https://files.pythonhosted.org/packages/c9/9e/e23fa8e78679ec45728188650b39e8ff476c83b691c96f749217df3b1b7c/librt-0.15.0-cp315-cp315-win32.whl", hash = "sha256:d2813ba2503764f0450680c533d13df7cff9b49df1411062eded5f67db4195b9", size = 100081, upload-time = "2026-08-07T10:48:46.171Z" }, + { url = "https://files.pythonhosted.org/packages/e1/dc/3eb4c5e297343f0620a55532cd7c8d764d3001fa2159212dadf480464827/librt-0.15.0-cp315-cp315-win_amd64.whl", hash = "sha256:b87d67e33afaf265262f2a66db578284b88ee2e6fcd224579cb5c15518677ad8", size = 121228, upload-time = "2026-08-07T10:48:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/97/70/43abce19f04e49762f8ec834c8fafee13cc40fd6b94a72a24e534febfcd0/librt-0.15.0-cp315-cp315-win_arm64.whl", hash = "sha256:713bd7df21170b982e729e46870f31d6b437bd1a9b4648cffb529bd3c2ec5c4b", size = 106487, upload-time = "2026-08-07T10:48:49.095Z" }, + { url = "https://files.pythonhosted.org/packages/de/15/83f2deddb9368b8951ec8c9477269b5b9b8bd9bbf15e57402d0f38817dca/librt-0.15.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:3de789c82752730f94782a5ee518baf9c05edf85733aeaf73bb6e518755cdf54", size = 159448, upload-time = "2026-08-07T10:48:50.649Z" }, + { url = "https://files.pythonhosted.org/packages/06/bf/043097353f9b3c73b583d07f6b8e552795463f4bfc8caf85e42eee50c26a/librt-0.15.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:e0b5deec9a8664eb722c797241970fd4aa1894d25fda36a1ddac0f7407606bd6", size = 161686, upload-time = "2026-08-07T10:48:52.174Z" }, + { url = "https://files.pythonhosted.org/packages/f4/2a/8ae77f9719d42ce71cd708560a3557b38ac3c17a0383e57f87084de45bbe/librt-0.15.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5563302a8359bc2295bb7084d1a8ed1519df96afb30eb2aa4e0bff7b54228988", size = 710668, upload-time = "2026-08-07T10:48:53.782Z" }, + { url = "https://files.pythonhosted.org/packages/61/34/c0436ea134deb9a0d6da80a396a2739a81cb31e0418f7227239e23140898/librt-0.15.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:22d6263b9d39d7bbb286fa791945646e3218f1be2d693e36fb630f1d0e59cd13", size = 679396, upload-time = "2026-08-07T10:48:55.645Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/001e0d99aa9250d5cd5715a9081291a20656083459f9019cda15255329e1/librt-0.15.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39ffd14646190c454f0d86e0d256b33f00a87a26ab410e619773b841d0e41416", size = 704313, upload-time = "2026-08-07T10:48:57.46Z" }, + { url = "https://files.pythonhosted.org/packages/2d/53/b34fa9d0ff00f136f4d58ebb4c411ff634baed1eb412bb602a2bc8dcafcb/librt-0.15.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47318cd3a61401452de11282242937e3e057c4fd3dbaf601e269d0928a06c0a", size = 729847, upload-time = "2026-08-07T10:48:59.231Z" }, + { url = "https://files.pythonhosted.org/packages/86/ac/fa4d7a424665040e95baf480a6d523446057684b6758624c85338e8a23b2/librt-0.15.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a56a1d4f859a82ca5b99fc4b82c9b027b15e3c455c5cd99e7d0719f27bb20b6c", size = 742736, upload-time = "2026-08-07T10:49:01.151Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/e17a9bb5de6fb8c3186ed1a7d68d21618b027ac2d3633e03d3b6109c67ae/librt-0.15.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:077471b3182db4e17c36ae91555f36a4d2c00080b267f749bcad34a478a9a302", size = 763454, upload-time = "2026-08-07T10:49:03.039Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ec/ecd02cd30935b931b9cdbfed6ab5a099c51b280b4e7baa274da80978ed27/librt-0.15.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:411ca4d1b905b860ceba7570dd6717a71dedaddcc4b0f77ece710aa41ee11f8d", size = 743296, upload-time = "2026-08-07T10:49:04.941Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b5/b3c2b8353ce820a4854f78d19321344242f89fa71c975b71132ba9bf242a/librt-0.15.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1256589e0b0adb31751d685a68bce29d73407ddf4ef05d4188f49d5dcf9566d9", size = 756217, upload-time = "2026-08-07T10:49:06.825Z" }, + { url = "https://files.pythonhosted.org/packages/3c/52/6cc22542ba59146b05cca2a656f9ff8bb67e38e63d12c3b0cc183d837bf1/librt-0.15.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:f42b74a53e5f26a0ba0007411a7455b66c67ce4022a39cc1f56fc4efd65bcbab", size = 741934, upload-time = "2026-08-07T10:49:08.839Z" }, + { url = "https://files.pythonhosted.org/packages/40/32/a04b72b1aa86e3be23b2ecff8c1aad2dcc955bd3956d6d26e7e34267e57a/librt-0.15.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:291bf73caf78b9e88d6fae9bfd693207ff7d832e2fdbe2cf8e746bc13f5f892b", size = 783763, upload-time = "2026-08-07T10:49:10.661Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f0/89eb11dffbe9279ff37144dec786927314502ae0b114f1449dc78c458aab/librt-0.15.0-cp315-cp315t-win32.whl", hash = "sha256:c16d15ee371643ab48dc8248a3e680ebbeca573a13af2c3dd0c985b142d77162", size = 104313, upload-time = "2026-08-07T10:49:12.305Z" }, + { url = "https://files.pythonhosted.org/packages/6d/4a/1f1978c200f563beda63c36adff2d65bbecb81e365e8e69e572f5f70fbc6/librt-0.15.0-cp315-cp315t-win_amd64.whl", hash = "sha256:dbd605739f228912dc49027cb764456b9757750bdc2b6b7773164db7096c6fd1", size = 126889, upload-time = "2026-08-07T10:49:13.881Z" }, + { url = "https://files.pythonhosted.org/packages/38/a6/800800bfed7b1fb10fc3f3d557785c3854e80d3f7a9800d784b176a1fc2d/librt-0.15.0-cp315-cp315t-win_arm64.whl", hash = "sha256:84d244b00604d17df3fc7736c327892d6bba66181254aa4087be807b6c342bdc", size = 110700, upload-time = "2026-08-07T10:49:15.499Z" }, +] + +[[package]] +name = "lightgbm" +version = "4.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "narwhals" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/8e/4db5e29290d7e619c307fdb8dab0a0514090af2ce3ec483050e024ec6126/lightgbm-4.7.0.tar.gz", hash = "sha256:f8e20f682c9aabd000bcf4a7ed8aa6f473c1adfecccae34ec24e823d156f4af0", size = 1792896, upload-time = "2026-07-18T21:00:56.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/05/7213965863cba1ed0150ad045bceed6276a1afaaaedbaeff4699ec4f0ccb/lightgbm-4.7.0-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:dfc1cfe8e760387be1e7ba7a214688be21fdff96e4ed9749188f83e1877c2477", size = 1877851, upload-time = "2026-07-18T21:00:35.225Z" }, + { url = "https://files.pythonhosted.org/packages/b2/86/f4fe714f2e0bf3941705a20d7f6849dc476276d71236e82ea6b0d6539b86/lightgbm-4.7.0-py3-none-macosx_12_0_arm64.whl", hash = "sha256:129535462686f274df179133643118c5c5c5667167fe6c3a28d955f0b3c8e868", size = 1498914, upload-time = "2026-07-18T21:00:36.549Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/b29580948b92e8c2f84dea70118ac702ff067dc52ec4ffb5d73c953536a5/lightgbm-4.7.0-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4529acec5c6fefe4768302a529707d0ead90f6a6f42df694b856212e09695b8", size = 3349492, upload-time = "2026-07-18T21:00:37.943Z" }, + { url = "https://files.pythonhosted.org/packages/15/eb/837ea3b40cc36e22eeebb9785c01e42b2c255d033eea1d2d9ee8e2540e55/lightgbm-4.7.0-py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d23e922acd891e77212e4d0fbcee9ba973c96dee479491341d05ba595357ebb7", size = 3476028, upload-time = "2026-07-18T21:00:39.331Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0b/c5c17d862b12ce292f24cd85d40f2f8f8981668fbdbd43fdc2625eccbc79/lightgbm-4.7.0-py3-none-win_amd64.whl", hash = "sha256:f42d1e5b32b6f170e606d7c689c6165671da98d7bf37f1addec2623efc8740c9", size = 1360833, upload-time = "2026-07-18T21:00:40.865Z" }, +] + +[[package]] +name = "mako" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/12/b5fa2353e2754cd67fb9f83793fa48ff42c213a5da7e719869d2301f6ab8/mako-1.4.1.tar.gz", hash = "sha256:d7904710b662996425a21627710c4777c45053146942cf8a7aebf757c92b8c27", size = 410165, upload-time = "2026-08-05T06:10:56.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/54/12ed58d458474aaab5c3d180173e745a4fe131bb330370596876d19ff60f/mako-1.4.1-py3-none-any.whl", hash = "sha256:a359d9a94a541213958742b2698d0a7757bb83551767bc468a74b9905aba9617", size = 80010, upload-time = "2026-08-05T06:10:58.248Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.9" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "cycler", marker = "python_full_version < '3.11'" }, + { name = "fonttools", marker = "python_full_version < '3.11'" }, + { name = "kiwisolver", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "packaging", marker = "python_full_version < '3.11'" }, + { name = "pillow", marker = "python_full_version < '3.11'" }, + { name = "pyparsing", marker = "python_full_version < '3.11'" }, + { name = "python-dateutil", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/1b/4be5be87d43d327a0cf4de1a56e86f7f84c89312452406cf122efe2839e6/matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358", size = 34811233, upload-time = "2026-04-24T00:14:13.539Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/6f/340b04986e67aac6f66c5145ce68bf72c64bed30f92c8913499a6e6b8f99/matplotlib-3.10.9-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77210dce9cb8153dffc967efaae990543392563d5a376d4dd8539bebcb0ed217", size = 8296625, upload-time = "2026-04-24T00:11:43.376Z" }, + { url = "https://files.pythonhosted.org/packages/bb/2f/127081eb83162053ebb9678ceac64220b93a663e0167432566e9c7c82aab/matplotlib-3.10.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1e7698ac9868428e84d2c967424803b2472ff7167d9d6590d4204ed775343c3b", size = 8188790, upload-time = "2026-04-24T00:11:46.556Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b7/d8bcec2626c35f96972bff656299fef4578113ea6193c8fdad324710410c/matplotlib-3.10.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1aa972116abb4c9d201bf245620b433726cb6856f3bef6a78f776a00f5c92d37", size = 8769389, upload-time = "2026-04-24T00:11:48.959Z" }, + { url = "https://files.pythonhosted.org/packages/12/49/b78e214a527ea732033b7f4d37f7afb504d74ba9d134bd47938230dfb8b1/matplotlib-3.10.9-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae2f11957b27ce53497dd4d7b235c4d4f1faf383dfb39d0c5beb833bff883294", size = 9589657, upload-time = "2026-04-24T00:11:51.915Z" }, + { url = "https://files.pythonhosted.org/packages/5f/15/5246f7b43beae19c74dfee651d58d6cc8112e06f77adb4e88cc04f2e3a23/matplotlib-3.10.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b049278ddce116aaa1c1377ebf58adea909132dfce0281cf7e3a1ea9fc2e2c65", size = 9651983, upload-time = "2026-04-24T00:11:54.766Z" }, + { url = "https://files.pythonhosted.org/packages/75/77/5acecfe672ba0fa1b8c0454f69ce155d1e6fc5852fa7206bf9afaf767121/matplotlib-3.10.9-cp310-cp310-win_amd64.whl", hash = "sha256:82834c3c292d24d3a8aae77cd2d20019de69d692a34a970e4fdb8d33e2ea3dda", size = 8199701, upload-time = "2026-04-24T00:11:58.389Z" }, + { url = "https://files.pythonhosted.org/packages/4c/8c/290f021104741fea63769c31494f5324c0cd249bf536a65a4350767b1f22/matplotlib-3.10.9-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:68cfdcede415f7c8f5577b03303dd94526cdb6d11036cecdc205e08733b2d2bb", size = 8306860, upload-time = "2026-04-24T00:12:01.207Z" }, + { url = "https://files.pythonhosted.org/packages/51/18/325cd32ece1120d1da51cc4e4294c6580190699490183fc2fe8cb6d61ec5/matplotlib-3.10.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfca0129678bd56379db26c52b5d77ed7de314c047492fbdc763aa7501710cfb", size = 8199254, upload-time = "2026-04-24T00:12:04.239Z" }, + { url = "https://files.pythonhosted.org/packages/79/db/e28c1b83e3680740aa78925f5fb2ae4d16207207419ad75ea9fe604f8676/matplotlib-3.10.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e436d155fa8a3399dc62683f8f5d0e2e50d25d0144a73edd73f82eec8f4abfb", size = 8777092, upload-time = "2026-04-24T00:12:06.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/fa/3ce7adfe9ba101748f465211660d9c6374c876b671bdb8c2bb6d347e8b94/matplotlib-3.10.9-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56fc0bd271b00025c6edfdc7c2dcd247372c8e1544971d62e1dc7c17367e8bf9", size = 9595691, upload-time = "2026-04-24T00:12:09.706Z" }, + { url = "https://files.pythonhosted.org/packages/36/c4/6960a76686ed668f2c60f84e9799ba4c0d56abdb36b1577b60c1d061d1ec/matplotlib-3.10.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5a6104ed666402ba5106d7f36e0e0cdca4e8d7fa4d39708ca88019e2835a2eb", size = 9659771, upload-time = "2026-04-24T00:12:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0d/271aace3342157c64700c9ff4c59c7b392f3dbab393692e8db6fbe7ab96c/matplotlib-3.10.9-cp311-cp311-win_amd64.whl", hash = "sha256:d730e984eddf56974c3e72b6129c7ca462ac38dc624338f4b0b23eb23ecba00f", size = 8205112, upload-time = "2026-04-24T00:12:15.773Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ee/cb57ad4754f3e7b9174ce6ce66d9205fb827067e48a9f58ac09d7e7d6b77/matplotlib-3.10.9-cp311-cp311-win_arm64.whl", hash = "sha256:51bf0ddbdc598e060d46c16b5590708f81a1624cefbaaf62f6a81bf9285b8c80", size = 8132310, upload-time = "2026-04-24T00:12:18.645Z" }, + { url = "https://files.pythonhosted.org/packages/35/c6/5581e26c72233ebb2a2a6fed2d24fb7c66b4700120b813f51b0555acf0b6/matplotlib-3.10.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0c3c28d9fbcc1fe7a03be236d73430cf6409c41fb2383a7ac52fe932b072cb1", size = 8319908, upload-time = "2026-04-24T00:12:21.323Z" }, + { url = "https://files.pythonhosted.org/packages/b7/18/4880dd762e40cd360c1bf06e890c5a97b997e91cb324602b1a19950ad5ce/matplotlib-3.10.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cb28c2bd769aa3e98322c6ab09854cbcc52ab69d2759d681bba3e327b2b320", size = 8216016, upload-time = "2026-04-24T00:12:23.4Z" }, + { url = "https://files.pythonhosted.org/packages/32/91/d024616abdba99e83120e07a20658976f6a343646710760c4a51df126029/matplotlib-3.10.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae20801130378b82d647ff5047c07316295b68dc054ca6b3c13519d0ea624285", size = 8789336, upload-time = "2026-04-24T00:12:26.096Z" }, + { url = "https://files.pythonhosted.org/packages/5c/04/030a2f61ef2158f5e4c259487a92ac877732499fb33d871585d89e03c42d/matplotlib-3.10.9-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c63ebcd8b4b169eb2f5c200552ae6b8be8999a005b6b507ed76fb8d7d674fe2", size = 9604602, upload-time = "2026-04-24T00:12:29.052Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c2/541e4d09d87bb6b5830fc28b4c887a9a8cf4e1c6cee698a8c05552ae2003/matplotlib-3.10.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d75d11c949914165976c621b2324f9ef162af7ebf4b057ddf95dd1dba7e5edcf", size = 9670966, upload-time = "2026-04-24T00:12:32.131Z" }, + { url = "https://files.pythonhosted.org/packages/04/a1/4571fc46e7702de8d0c2dc54ad1b2f8e29328dea3ee90831181f7353d93c/matplotlib-3.10.9-cp312-cp312-win_amd64.whl", hash = "sha256:d091f9d758b34aaaaa6331d13574bf01891d903b3dec59bfff458ef7551de5d6", size = 8217462, upload-time = "2026-04-24T00:12:35.226Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d0/2269edb12aa30c13c8bcc9382892e39943ce1d28aab4ec296e0381798e81/matplotlib-3.10.9-cp312-cp312-win_arm64.whl", hash = "sha256:10cc5ce06d10231c36f40e875f3c7e8050362a4ee8f0ee5d29a6b3277d57bb42", size = 8136688, upload-time = "2026-04-24T00:12:37.442Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d3/8d4f6afbecb49fc04e060a57c0fce39ea51cc163a6bd87303ccd698e4fa6/matplotlib-3.10.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b580440f1ff81a0e34122051a3dfabb7e4b7f9e380629929bde0eff9af72165f", size = 8320331, upload-time = "2026-04-24T00:12:39.688Z" }, + { url = "https://files.pythonhosted.org/packages/63/d9/9e14bc7564bf92d5ffa801ae5fac819ce74b925dfb55e3ebde61a3bbad3e/matplotlib-3.10.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b1b745c489cd1a77a0dc1120a05dc87af9798faebc913601feb8c73d89bf2d1e", size = 8216461, upload-time = "2026-04-24T00:12:42.494Z" }, + { url = "https://files.pythonhosted.org/packages/8a/17/4402d0d14ccf1dfc70932600b68097fbbf9c898a4871d2cbbe79c7801a32/matplotlib-3.10.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8f3bcac1ca5ed000a6f4337d47ba67dfddf37ed6a46c15fd7f014997f7bf865f", size = 8790091, upload-time = "2026-04-24T00:12:44.789Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0b/322aeec06dd9b91411f92028b37d447342770a24392aa4813e317064dad5/matplotlib-3.10.9-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a8d66a55def891c33147ba3ba9bfcabf0b526a43764c818acbb4525e5ed0838", size = 9605027, upload-time = "2026-04-24T00:12:47.583Z" }, + { url = "https://files.pythonhosted.org/packages/74/88/5f13482f55e7b00bcfc09838b093c2456e1379978d2a146844aae05350ad/matplotlib-3.10.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d843374407c4017a6403b59c6c81606773d136f3259d5b6da3131bc814542cc2", size = 9671269, upload-time = "2026-04-24T00:12:50.878Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/0840fd2f93da988ec660b8ad1984abe9f25d2aed22a5e394ff1c68c88307/matplotlib-3.10.9-cp313-cp313-win_amd64.whl", hash = "sha256:f4399f64b3e94cd500195490972ae1ee81170df1636fa15364d157d5bdd7b921", size = 8217588, upload-time = "2026-04-24T00:12:53.784Z" }, + { url = "https://files.pythonhosted.org/packages/47/b9/d706d06dd605c49b9f83a2aed8c13e3e5db70697d7a80b7e3d7915de6b17/matplotlib-3.10.9-cp313-cp313-win_arm64.whl", hash = "sha256:ba7b3b8ef09eab7df0e86e9ae086faa433efbfbdb46afcb3aa16aabf779469a8", size = 8136913, upload-time = "2026-04-24T00:12:56.501Z" }, + { url = "https://files.pythonhosted.org/packages/9b/45/6e32d96978264c8ca8c4b1010adb955a1a49cfaf314e212bbc8908f04a61/matplotlib-3.10.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:09218df8a93712bd6ea133e83a153c755448cf7868316c531cffcc43f69d1cc9", size = 8368019, upload-time = "2026-04-24T00:12:58.896Z" }, + { url = "https://files.pythonhosted.org/packages/86/0a/c8e3d3bba245f0f7fc424937f8ff7ef77291a36af3edb97ccd78aa93d84f/matplotlib-3.10.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:82368699727bfb7b0182e1aa13082e3c08e092fa1a25d3e1fd92405bff96f6d4", size = 8264645, upload-time = "2026-04-24T00:13:01.406Z" }, + { url = "https://files.pythonhosted.org/packages/3d/aa/5bf5a14fe4fed73a4209a155606f8096ff797aad89c6c35179026571133e/matplotlib-3.10.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3225f4e1edcb8c86c884ddf79ebe20ecd0a67d30188f279897554ccd8fded4dc", size = 8802194, upload-time = "2026-04-24T00:13:03.702Z" }, + { url = "https://files.pythonhosted.org/packages/dd/5e/b4be852d6bba6fd15893fadf91ff26ae49cb91aac789e95dde9d342e664f/matplotlib-3.10.9-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de2445a0c6690d21b7eb6ce071cebad6d40a2e9bdf10d039074a96ba19797b99", size = 9622684, upload-time = "2026-04-24T00:13:06.647Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/ed428c971139112ef730f62770654d609467346d09d4b62617e1afd68a5a/matplotlib-3.10.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b2b9516251cb89ff618d757daec0e2ed1bf21248013844a853d87ef85ab3081d", size = 9680790, upload-time = "2026-04-24T00:13:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/e7/09/052e884aaf2b985c63cb79f715f1d5b6a3eaa7de78f6a52b9dbc077d5b53/matplotlib-3.10.9-cp313-cp313t-win_amd64.whl", hash = "sha256:e9fae004b941b23ff2edcf1567a857ed77bafc8086ffa258190462328434faf8", size = 8287571, upload-time = "2026-04-24T00:13:13.087Z" }, + { url = "https://files.pythonhosted.org/packages/f4/38/ae27288e788c35a4250491422f3db7750366fc8c97d6f36fbdecfc1f5518/matplotlib-3.10.9-cp313-cp313t-win_arm64.whl", hash = "sha256:6b63d9c7c769b88ab81e10dc86e4e0607cf56817b9f9e6cf24b2a5f1693b8e38", size = 8188292, upload-time = "2026-04-24T00:13:15.546Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e6/3bd8afd04949f02eabc1c17115ea5255e19cacd4d06fc5abdde4eeb0052c/matplotlib-3.10.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:172db52c9e683f5d12eaf57f0f54834190e12581fe1cc2a19595a8f5acb4e77d", size = 8321276, upload-time = "2026-04-24T00:13:18.318Z" }, + { url = "https://files.pythonhosted.org/packages/41/86/86231232fff41c9f8e4a1a7d7a597d349a02527109c3af7d618366122139/matplotlib-3.10.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97e35e8d39ccc85859095e01a53847432ba9a53ddf7986f7a54a11b73d0e143f", size = 8218218, upload-time = "2026-04-24T00:13:20.974Z" }, + { url = "https://files.pythonhosted.org/packages/85/8f/becc9722cafc64f5d2eb0b7c1bf5f585271c618a45dbd8fabeb021f898b6/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aba1615dabe83188e19d4f75a253c6a08423e04c1425e64039f800050a69de6b", size = 9608145, upload-time = "2026-04-24T00:13:23.228Z" }, + { url = "https://files.pythonhosted.org/packages/32/5d/f7e914f7d9325abff4057cee62c0fa70263683189f774473cbfb534cd13b/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34cf8167e023ad956c15f36302911d5406bd99a9862c1a8499ea6f7c0e015dc2", size = 9885085, upload-time = "2026-04-24T00:13:25.849Z" }, + { url = "https://files.pythonhosted.org/packages/a5/fd/fa69f2221534e80cc5772ac2b7d222011a2acafc2ec7216d5dd174c864ae/matplotlib-3.10.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59476c6d29d612b8e9bb6ce8c5b631be6ba8f9e3a2421f22a02b192c7dd28716", size = 9672358, upload-time = "2026-04-24T00:13:28.906Z" }, + { url = "https://files.pythonhosted.org/packages/ab/1a/5a4f747a8b271cbb024946d2dd3c913ab5032ba430626f8c3528ada96b4b/matplotlib-3.10.9-cp314-cp314-win_amd64.whl", hash = "sha256:336b9acc64d309063126edcdaca00db9373af3c476bb94388fe9c5a53ad13e6f", size = 8349970, upload-time = "2026-04-24T00:13:31.904Z" }, + { url = "https://files.pythonhosted.org/packages/64/dc/95d60ecaefe30680a154b52ea96ab4b0dab547f1fd6aa12f5fb655e89cae/matplotlib-3.10.9-cp314-cp314-win_arm64.whl", hash = "sha256:2dc9477819ffd78ad12a20df1d9d6a6bd4fec6aaa9072681465fddca052f1456", size = 8272785, upload-time = "2026-04-24T00:13:34.511Z" }, + { url = "https://files.pythonhosted.org/packages/70/a0/005d68bc8b8418300ce6591f18586910a8526806e2ab663933d9f20a41e9/matplotlib-3.10.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:da4e09638420548f31c354032a6250e473c68e5a4e96899b4844cf39ddea23fe", size = 8367999, upload-time = "2026-04-24T00:13:36.962Z" }, + { url = "https://files.pythonhosted.org/packages/22/05/1236cc9290be70b2498af20ca348add76e3fffe7f67b477db5133a84f3ea/matplotlib-3.10.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:345f6f68ecc8da0ca56fad2ea08fde1a115eda530079eca185d50a7bc3e146c6", size = 8264543, upload-time = "2026-04-24T00:13:39.851Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c2/071f5a5ff6c5bd63aaaf2f45c811d9bf2ced94bde188d9e1a519e21d0cba/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4edcfbd8565339aa62f1cd4012f7180926fdbe71850f7b0d3c379c175cd6b66c", size = 9622800, upload-time = "2026-04-24T00:13:42.296Z" }, + { url = "https://files.pythonhosted.org/packages/95/57/da7d1f10a85624b9e7db68e069dd94e58dc41dbf9463c5921632ecbe3661/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be157fe17fc37cb95ac1d7374cf717ce9259616edec911a78d9d26dae8522d4", size = 9888561, upload-time = "2026-04-24T00:13:45.026Z" }, + { url = "https://files.pythonhosted.org/packages/67/b2/ef8d6bb59b0edb6c16c968b70f548aa13b54348972def5aa6ac85df67145/matplotlib-3.10.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4e42042d54db34fda4e95a7bd3e5789c2a995d2dad3eb8850232ee534092fbbf", size = 9680884, upload-time = "2026-04-24T00:13:48.066Z" }, + { url = "https://files.pythonhosted.org/packages/61/1c/d21bfeb9931881ebe96bcfcff27c7ae4b160ae0ec291a714c42641a56d75/matplotlib-3.10.9-cp314-cp314t-win_amd64.whl", hash = "sha256:c27df8b3848f32a83d1767566595e43cfaa4460380974da06f4279a7ec143c39", size = 8432333, upload-time = "2026-04-24T00:13:51.008Z" }, + { url = "https://files.pythonhosted.org/packages/78/23/92493c3e6e1b635ccfff146f7b99e674808787915420373ac399283764c2/matplotlib-3.10.9-cp314-cp314t-win_arm64.whl", hash = "sha256:a49f1eadc84ca85fd72fa4e89e70e61bf86452df6f971af04b12c60761a0772c", size = 8324785, upload-time = "2026-04-24T00:13:53.633Z" }, + { url = "https://files.pythonhosted.org/packages/2c/2b/0e92ad0ac446633f928a1563db4aa8add407e1924faf0ded5b95b35afb27/matplotlib-3.10.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1872fb212a05b729e649754a72d5da61d03e0554d76e80303b6f83d1d2c0552b", size = 8293058, upload-time = "2026-04-24T00:13:56.339Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/74682fd369f5299ceda438fea2a0662e6383b85c9383fb9cdfcf04713e07/matplotlib-3.10.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:985f2238880e2e69093f588f5fe2e46771747febf0649f3cf7f7b7480875317f", size = 8186627, upload-time = "2026-04-24T00:13:58.623Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e8/368aab88f3c4cd8992800f31abfe0670c3e47540ba20a97e9fdbcde594b3/matplotlib-3.10.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6640f75af2c6148293caa0a2b39dd806a492dd66c8a8b04035813e33d0fd2585", size = 8764117, upload-time = "2026-04-24T00:14:01.684Z" }, + { url = "https://files.pythonhosted.org/packages/63/e2/9f66ca6a651a52abfe0d4964ce01439ed34f3f1e119de10ff3a07f403043/matplotlib-3.10.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:42fb814efabe95c06c1994d8ab5a8385f43a249e23badd3ba931d4308e5bca20", size = 8304420, upload-time = "2026-04-24T00:14:04.57Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e8/467c03568218792906aa87b5e7bb379b605e056ed0c74fe00c051786d925/matplotlib-3.10.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f76e640a5268850bfda54b5131b1b1941cc685e42c5fa98ed9f2d64038308cba", size = 8197981, upload-time = "2026-04-24T00:14:07.233Z" }, + { url = "https://files.pythonhosted.org/packages/6f/87/afead29192170917537934c6aff4b008c805fff7b1ccea0c79120d96beda/matplotlib-3.10.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3fc0364dfbe1d07f6d15c5ebd0c5bf89e126916e5a8667dd4a7a6e84c36653d4", size = 8774002, upload-time = "2026-04-24T00:14:09.816Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.11.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version >= '3.13' and python_full_version < '3.15'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "cycler", marker = "python_full_version >= '3.11'" }, + { name = "fonttools", marker = "python_full_version >= '3.11'" }, + { name = "kiwisolver", marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.11'" }, + { name = "pillow", marker = "python_full_version >= '3.11'" }, + { name = "pyparsing", marker = "python_full_version >= '3.11'" }, + { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/64/f9a391af28f518b11ad45a8a712353c94a0aefce09d3703200e5c54b610a/matplotlib-3.11.1.tar.gz", hash = "sha256:69647db5746941c793d6e445a4cd349323ffb87d9cc958c2ad84a659b4832d30", size = 32612045, upload-time = "2026-07-18T03:39:46.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/d0/791aa183dd88491555cf7d4be0b52b0bcf6c3c2a2c22c815a2e819bf53e2/matplotlib-3.11.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b7cf158e7add54a8d51ac9b5a84abd6d4e13ed4951b4f25f1c5139f41c2addb2", size = 9440302, upload-time = "2026-07-18T03:38:03.844Z" }, + { url = "https://files.pythonhosted.org/packages/35/74/82bbdf683a301f4478384c8aaba6903631a2ca18294b2d7655c9a542bffb/matplotlib-3.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d2ace7273b9a5061a3b420918a16fae1f2dc5dfee1abcc13aba71b5d94b1820c", size = 9268549, upload-time = "2026-07-18T03:38:06.144Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f0/9b4298911303f74e6d83e64a81d996c0616405ec95046fac7f17e4258b9e/matplotlib-3.11.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee55e9041211bf84302ab55ec3965df18dd90ae19f8b58332a7feaf208bfe83", size = 10024922, upload-time = "2026-07-18T03:38:08.236Z" }, + { url = "https://files.pythonhosted.org/packages/84/6f/0bc3c3d05b021db44c14bc379a7c0df7d57302aa15380c16fd4e63fd6a9b/matplotlib-3.11.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f4bdeea33a8d15a071dbfe6d119451b1d719c733ac666d65357082901a9099", size = 10832170, upload-time = "2026-07-18T03:38:10.276Z" }, + { url = "https://files.pythonhosted.org/packages/db/4d/e375f39acdb2af5a9342730618608e39790ec842e6f1b392863028781459/matplotlib-3.11.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b4c78ceb2f11bcac7389d305cda17aeb1f4586a857854ab5780bd3dd8dbfc407", size = 10916701, upload-time = "2026-07-18T03:38:12.512Z" }, + { url = "https://files.pythonhosted.org/packages/bc/be/fa26ed085b41298f64a8f9b7592c671bbf1acc8b0df124c1c5de96b859f8/matplotlib-3.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:7f33a781e12b1e53b278deb2f5373c2e55ec4f10727be3440c0cfb5cda9f944f", size = 9315331, upload-time = "2026-07-18T03:38:14.949Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f3/eb5bdf3b6e191b200db298b08bbc1638b7f3c82cdc8680f9d88bf72559ae/matplotlib-3.11.1-cp311-cp311-win_arm64.whl", hash = "sha256:67e4c3cd578c65ebd81bdc09a1b6592ceafee6dfafe116dc85dfcb647b5bbb18", size = 9003475, upload-time = "2026-07-18T03:38:17.205Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6c/7ef7ebcb2bd9739b2b66b18b076e077f44bb46fdbe28ca0506edb3c62c79/matplotlib-3.11.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e15ef41507f3d525f46154ac9e3ae785dacde9f20e593a25de8986267892ef74", size = 9453849, upload-time = "2026-07-18T03:38:19.593Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f8/6d0c312c8d9738e7d9677f09fe5c986b3239e651a7b73a2deb38b65e4a71/matplotlib-3.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:21a67b961a6d597bca54fae826cd20695ba4a6e4d05424a08da6e13e3176fd6b", size = 9283113, upload-time = "2026-07-18T03:38:21.95Z" }, + { url = "https://files.pythonhosted.org/packages/c9/cf/b4ad2cc81b6672ea29ea04e64e350a9f9b493b0908ccd884c67eeff8f7b2/matplotlib-3.11.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ba8f811b8ddfac493734d6af0b2dff96919d0c28ca0d641858dab4262777c6ea", size = 10035615, upload-time = "2026-07-18T03:38:24.315Z" }, + { url = "https://files.pythonhosted.org/packages/88/90/4e10e033d9b66589d8ed98b84c95cdbb57033d57c1f41339d7393dbd2f2e/matplotlib-3.11.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c52f7ad20ef476806ed212380b1d54d20310c8b86bdc2c9a68b51f0024a44472", size = 10842559, upload-time = "2026-07-18T03:38:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/88/eb/799612d0f8cd3e816a10fec59329fca52cd2353264df80378dfc541ae855/matplotlib-3.11.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8b14eb22961fe865efb0e4ff167e333e428908b00115a8d800ccb65ee108e481", size = 10927532, upload-time = "2026-07-18T03:38:28.532Z" }, + { url = "https://files.pythonhosted.org/packages/88/89/56649bbaa2fd12e20f3be03dbcc135b0c8676d88bac17977599e3eb442a0/matplotlib-3.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:88a2a27dd9691ae448dfae4b26f59036be90c3c28757edd3553a29559d00859f", size = 9333886, upload-time = "2026-07-18T03:38:30.477Z" }, + { url = "https://files.pythonhosted.org/packages/c1/11/4d124efbbad677b7b7552f6f85a3bd432d4232f95400cea98fcd2ae36ef3/matplotlib-3.11.1-cp312-cp312-win_arm64.whl", hash = "sha256:480194afceca4df2f137c2721227d3cba67121fbf4397b69cee7f83714b0a58a", size = 9007545, upload-time = "2026-07-18T03:38:32.833Z" }, + { url = "https://files.pythonhosted.org/packages/04/6c/4798363b7fb5644e309fe1fac30216e9146c9f70859d80d588c18caf5317/matplotlib-3.11.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6771b0cd7838c6a857a7209814158c0ad09bfef878db3033dd82d70ad101f191", size = 9454341, upload-time = "2026-07-18T03:38:35.001Z" }, + { url = "https://files.pythonhosted.org/packages/59/98/6acadbe7f98df19d274bc107ac58bb439fa75df82c33dc110d71a4a8501f/matplotlib-3.11.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2abdee5ffa2fe11b2d19f7a5c63b785fb7c28cc46c7bc1814156341d9d1a33e1", size = 9283627, upload-time = "2026-07-18T03:38:37.061Z" }, + { url = "https://files.pythonhosted.org/packages/24/ea/65cec46fe241390ccea1b1754207ee28eb71c5ab866bd5f22fe47e538fa4/matplotlib-3.11.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0a19dcf73406d3746d25a5ed42d713604c9a3e024d129b102852b0d941cb9f3", size = 10035860, upload-time = "2026-07-18T03:38:39.663Z" }, + { url = "https://files.pythonhosted.org/packages/c7/10/63fdccccbabe002fb0960876baabc5e3f24d9c1bb4cfb25651457f74b3a0/matplotlib-3.11.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7389b77ed2ab0552f46d9a90b81b7b8e6dfcdc42adc36c37a0865799843e0e3e", size = 10843594, upload-time = "2026-07-18T03:38:42.144Z" }, + { url = "https://files.pythonhosted.org/packages/98/51/a1155945bff7b91381875022ac1522c5dfdac0d006be8e7df389b3134eae/matplotlib-3.11.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c90be0b73568da4f662afac580956a76e308437e641b4a45aa08925eeb67d95f", size = 10927962, upload-time = "2026-07-18T03:38:44.302Z" }, + { url = "https://files.pythonhosted.org/packages/0d/3a/3d5e1f42dc761bf53401a62a83ff93389b37de9d2c093b2a3aa49ac34f1b/matplotlib-3.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:68408341f2312836fbbdf6b3c78047f65b2d8752f5fd221c3e72d348f5b34f8b", size = 9334074, upload-time = "2026-07-18T03:38:46.616Z" }, + { url = "https://files.pythonhosted.org/packages/e2/db/3f5ea5a5b64060ef5e1ff60a19170423e41ce21b8497a6fe15a36e0b43e3/matplotlib-3.11.1-cp313-cp313-win_arm64.whl", hash = "sha256:0c1f44890d435c1b4ef52f701ad5828cb450ea97bcc83918fda6be74965d6cd2", size = 9007662, upload-time = "2026-07-18T03:38:49.112Z" }, + { url = "https://files.pythonhosted.org/packages/98/6e/c7ae5e0531425b69c0826b00ebbc264c85cab853f1cd6e096c9983c2cdc1/matplotlib-3.11.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:5e510088c27a89d53580a752f959146893563e63c330e161d159b0fee652af6f", size = 9503790, upload-time = "2026-07-18T03:38:51.527Z" }, + { url = "https://files.pythonhosted.org/packages/92/79/15be162e0a2ed546939674e2e97d0e33ec2447d86d4d4e611fa295bb178c/matplotlib-3.11.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:1524e2bdd48a93557aa47ddcfe9c225dfdd57d5a01a5c49128c20f0632980ee1", size = 9336148, upload-time = "2026-07-18T03:38:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7f/36ffe144fc4aacfe0e3ed2318f72b6755d1e73b041d619b4d393e60f5a66/matplotlib-3.11.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:11664c551345553db92e61cae6cf1376f138f8c47cafdf13b64b18f3e3e9e464", size = 10049244, upload-time = "2026-07-18T03:38:55.911Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5f/55812d68c0a840d3a463638f48c00ab1fe338518ec49a640cb6473b444af/matplotlib-3.11.1-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e1f8922ba31959cf6a9dfb51be64b7f7bc582801a3957dc0c2f3afcd3537adf", size = 10860798, upload-time = "2026-07-18T03:38:58.282Z" }, + { url = "https://files.pythonhosted.org/packages/7a/64/cca444b4eb5e6c768c44fc5e1f0b5211f20ca2b282778051996e996a2bdf/matplotlib-3.11.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83235693abde86e5e0129998f80ee39fc7f58e6d56a88fafb28a9278833e9d5f", size = 10943282, upload-time = "2026-07-18T03:39:00.465Z" }, + { url = "https://files.pythonhosted.org/packages/e5/0f/a49c329d394f2e9ef38506982107e8b04ecf94dd41a9d8423ff82cc737c7/matplotlib-3.11.1-cp313-cp313t-win_amd64.whl", hash = "sha256:9a076f4fc5cdc43fdf510f5981418d25c2db4973418d9f22d8bb3dc8045ada78", size = 9383532, upload-time = "2026-07-18T03:39:02.468Z" }, + { url = "https://files.pythonhosted.org/packages/e4/50/103e86afb806d8f64d04ede14e4cfc09dbfc25f512421ff85fdd6ebd59cf/matplotlib-3.11.1-cp313-cp313t-win_arm64.whl", hash = "sha256:216fbb93a74add02ddb4cb38ef5348f59ac00b3e84567eaf16598772d40e150a", size = 9059665, upload-time = "2026-07-18T03:39:04.607Z" }, + { url = "https://files.pythonhosted.org/packages/35/04/3079499fa8cb661ea66d13d6439d5a3ae6710a7afd5c7f72e08914f275f8/matplotlib-3.11.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:30c492d4ba9448595b6fd8708c6725963f8148e25c0d8842948da5b05f0ee8d3", size = 9456022, upload-time = "2026-07-18T03:39:07.041Z" }, + { url = "https://files.pythonhosted.org/packages/53/a2/69acfe84ec1f32930e801a5782a07fc5c79c8c6599a507b806d859d5da8e/matplotlib-3.11.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ac104be2768ffdd8655db9e71b768cbb45f2b9aa7b450cf1595e8f65d3822319", size = 9285475, upload-time = "2026-07-18T03:39:09.562Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b3/31b15a2ca56d4ddd6aaa1c884c2f51cf9a61cfaf5ca6f6fbd6343d38e6df/matplotlib-3.11.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be943cb68bc6660ead58c55b3aa6366cba2ef7feb06460fbcce32360376f19f", size = 10847102, upload-time = "2026-07-18T03:39:11.532Z" }, + { url = "https://files.pythonhosted.org/packages/64/0d/a17e966e620545c1548125af0b29ac812dd17b197a18a7462ac12fa859ee/matplotlib-3.11.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5af0dcda57d471440a7b5b623e70e0a61003518443d9098f211a96ecfbbc25be", size = 11131087, upload-time = "2026-07-18T03:39:13.764Z" }, + { url = "https://files.pythonhosted.org/packages/97/c5/5e100efdd67abb7de20befaa333612ef9bfc63417fb71398f904f25d083c/matplotlib-3.11.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3d3fd84082b1afbd9398466c81309e20045be20d48fe0fb18c43504d164cbbb2", size = 10929036, upload-time = "2026-07-18T03:39:16.888Z" }, + { url = "https://files.pythonhosted.org/packages/ce/04/d719a0a36930ecc8dfc801ff340f9dcfc4223f8ca5d39d06b4020032fff8/matplotlib-3.11.1-cp314-cp314-win_amd64.whl", hash = "sha256:9601a1e90be21e4884c53b4f3dc3ee0544654946f9975258d691f1c2e2f119c6", size = 9489571, upload-time = "2026-07-18T03:39:19.449Z" }, + { url = "https://files.pythonhosted.org/packages/48/65/facabdc2f1f6caba7e856db64dfedddca25f7608df07d96a1c8fd114fd3b/matplotlib-3.11.1-cp314-cp314-win_arm64.whl", hash = "sha256:ae30c6109848ac0f9fa36c5d6270938487614c47ba31860bd5361266dabc5685", size = 9164486, upload-time = "2026-07-18T03:39:21.424Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/18da6cd01cf96354534f98c468a25380c68ce582a2c9dd0cae12b04af4f2/matplotlib-3.11.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:dadfe80797174e2984aae3be0b77594a3c72d2c0a40fbd4a0de48d2728caf3ae", size = 9504876, upload-time = "2026-07-18T03:39:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/b0/f0b63555a18b79d038c81fd6126f35fc4dfce0eaff48d96103348c7cf935/matplotlib-3.11.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:89b193b255f4f6f7948dbcee3691f4f341ab05d9a8874a67b45ddb4182922eda", size = 9336120, upload-time = "2026-07-18T03:39:25.797Z" }, + { url = "https://files.pythonhosted.org/packages/c6/dd/f210ec7c4a6f198d5567237048a93d0811fb5a1f1691f13320e592f95b41/matplotlib-3.11.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191163532cdefcb1571ca38a6d7e6474baccde64495783e6ba47aa07ec4b9bbb", size = 10858033, upload-time = "2026-07-18T03:39:27.999Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d2/d6d5324507c5fbb316db48e258c09c2807f3de03d9af47017e120070926f/matplotlib-3.11.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9fdf1c818ab05d0e74002091ddaf414478a3a449ec9d51c8976d45be7e3a01e2", size = 11141827, upload-time = "2026-07-18T03:39:30.092Z" }, + { url = "https://files.pythonhosted.org/packages/0f/68/3c22e9320bdce2c4d2f1320643ef706db7a24cb7420eea28b97a2d67f5a8/matplotlib-3.11.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b937b9dba5f5f6c1e31c47abe2186c865c0914fd18f2ce0dfc39c9adcef5951d", size = 10943061, upload-time = "2026-07-18T03:39:32.356Z" }, + { url = "https://files.pythonhosted.org/packages/f6/4a/907ed190ee81a9df581e0ed5456134fc0f7cb55ffcfda2f9e54ca900761c/matplotlib-3.11.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f2912f647f3fbe1ccf085f91e213936f9101bead81a5e670565b1f1b3712f4fb", size = 9540074, upload-time = "2026-07-18T03:39:34.789Z" }, + { url = "https://files.pythonhosted.org/packages/23/d4/97c19b77e0a6e3b48581185bb65088f431cd20186076cc0f650a1757ea46/matplotlib-3.11.1-cp314-cp314t-win_arm64.whl", hash = "sha256:54d47b8ae8b579633a3902ca5b4ad6c1e132a5626d64447b2e22a66394e79987", size = 9213472, upload-time = "2026-07-18T03:39:37.141Z" }, + { url = "https://files.pythonhosted.org/packages/ee/38/ceb1d637c4db6d06141f3739e93af3321e7caaabe69b57ae48ffe3ee95b1/matplotlib-3.11.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:427258425f9a3fc4ed79a91f9e9b9aaf5a82cb6571e85dc14063cc6fbb993741", size = 9438045, upload-time = "2026-07-18T03:39:39.491Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/72ad8b58602d3a6ef1dfc4b65ecd01634ab65a2bdf494c9fe0e966dbf081/matplotlib-3.11.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:1ac697e591c11b6ad04679a73c2d2f9980fe9d9f0311fb414a2e329706343dfb", size = 9266127, upload-time = "2026-07-18T03:39:41.597Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6d/69552382fcc8e93d1f2763ef2665980a900a48b7f3a4c57ed290726d1cbc/matplotlib-3.11.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e4b9ac2f1f607ecda2af90a5232beee2af7582fce1cc30c4b6a1b012dc21ee99", size = 10019439, upload-time = "2026-07-18T03:39:43.78Z" }, +] + +[[package]] +name = "ml-dtypes" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { 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/fe/3a/c5b855752a70267ff729c349e650263adb3c206c29d28cc8ea7ace30a1d5/ml_dtypes-0.5.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b95e97e470fe60ed493fd9ae3911d8da4ebac16bd21f87ffa2b7c588bf22ea2c", size = 679735, upload-time = "2025-11-17T22:31:31.367Z" }, + { url = "https://files.pythonhosted.org/packages/41/79/7433f30ee04bd4faa303844048f55e1eb939131c8e5195a00a96a0939b64/ml_dtypes-0.5.4-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4b801ebe0b477be666696bda493a9be8356f1f0057a57f1e35cd26928823e5a", size = 5051883, upload-time = "2025-11-17T22:31:33.658Z" }, + { url = "https://files.pythonhosted.org/packages/10/b1/8938e8830b0ee2e167fc75a094dea766a1152bde46752cd9bfc57ee78a82/ml_dtypes-0.5.4-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:388d399a2152dd79a3f0456a952284a99ee5c93d3e2f8dfe25977511e0515270", size = 5030369, upload-time = "2025-11-17T22:31:35.595Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a3/51886727bd16e2f47587997b802dd56398692ce8c6c03c2e5bb32ecafe26/ml_dtypes-0.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:4ff7f3e7ca2972e7de850e7b8fcbb355304271e2933dd90814c1cb847414d6e2", size = 210738, upload-time = "2025-11-17T22:31:37.43Z" }, + { 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" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "mypy" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/af/4e516a05d3ca2eb9283e9ec45b2c02225c1514dd6da49fd3c9eaa6639370/mypy-2.3.0.tar.gz", hash = "sha256:465965d41cd9a2726694e983e8ce7113259327bec798115d1e1dfa2a52fb666e", size = 3988104, upload-time = "2026-07-13T11:34:53.387Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/09/f2f5f45dae0c9a0891e4751a73312730e009395102e5d72a22a976cca41f/mypy-2.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1fa8d916ac3b705af733c4c1e6c9ebe38fd0d52beb15b105c3e8355b55e6ecdc", size = 14927774, upload-time = "2026-07-13T11:28:38.224Z" }, + { url = "https://files.pythonhosted.org/packages/56/b9/345367effd3a6877275a94d481614bfca983f45e028c6290e2cc54603811/mypy-2.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:28e1e2af8cd8fff551fd30f2fe4b03fb76764ac8b1ba6c6a1bd00ad32b412db3", size = 14000127, upload-time = "2026-07-13T11:30:19.57Z" }, + { url = "https://files.pythonhosted.org/packages/99/6c/a10b7a7b9f0a755fb94e27ae834d4cea9ad6c5221f9325eef8f182641feb/mypy-2.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3e77244df3843048c3f927182916730e40c124cbaa43905c1fb86cb382aa0805", size = 14229437, upload-time = "2026-07-13T11:28:17.765Z" }, + { url = "https://files.pythonhosted.org/packages/d9/bd/a26a602acb1bbf849fa4bdac4bc657ee2f11c0c2a764a2cc87a5304e865c/mypy-2.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9559ab18a9c9957dfa3004ab57cd4bac5f26a724329a9584e583367f0c2e1117", size = 15171457, upload-time = "2026-07-13T11:29:01.834Z" }, + { url = "https://files.pythonhosted.org/packages/7f/14/124f462bef69bcbc90b9358088460b6091954a3e004852fcd9948db617a5/mypy-2.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:09abd66d8685e73f8f7d17b847c3e104d9a7b164a8706ea87d6c96a3d45816d5", size = 15478281, upload-time = "2026-07-13T11:32:23.413Z" }, + { url = "https://files.pythonhosted.org/packages/db/a4/8bdca6a8ac8d856d82ed049144af2721245a135c2e8001d3890c93975852/mypy-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e91adad1ca81742ac7ef9893959911df867752206b37135185e88dfb3c89494", size = 11148008, upload-time = "2026-07-13T11:34:17.332Z" }, + { url = "https://files.pythonhosted.org/packages/83/41/490eea348e60ba50decec20bc750605444149a5d7a8cc560042f90ba2c75/mypy-2.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:6f99ec626e3c3a2f7c0b22c5b90ddb5dabb1c18729c971e9bdaca1f1766d2cee", size = 10142329, upload-time = "2026-07-13T11:32:52.116Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b9/d75b3082b05f1b3028828aeb18e74ae5ab0a0936051bbf1f32f59f654747/mypy-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3419d00717afbc5265b50dd14b1278f29ea4884dd398ab67873489ac093fd329", size = 14838725, upload-time = "2026-07-13T11:32:44.655Z" }, + { url = "https://files.pythonhosted.org/packages/a9/50/79a65c6ea6e115bc73296038a4543b2d5c91f07912b918a2c616a2514bba/mypy-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cfca8ee88544090f86b6dcce05ec55d66eb48a762412ac2507810ba4bd793b6f", size = 13911128, upload-time = "2026-07-13T11:32:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/90/48/e11ed7716c26953ca321f726e452e374dbf81a6f2b8b212ec02af29b6b8f/mypy-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75cbb4b9ef04a0c84a957f07abc4504fbf64b8dcc145675101f2d3a78a4b1d6a", size = 14146742, upload-time = "2026-07-13T11:33:03.313Z" }, + { url = "https://files.pythonhosted.org/packages/06/72/6807565b1c4861ef66f7fdd98b51c61556356eab80235717b46c53bb8627/mypy-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:982e3d53dd23d0a4cef67dd66791fdbede0cf38f9eb617bf47663554c51e1e36", size = 15081418, upload-time = "2026-07-13T11:31:13.899Z" }, + { url = "https://files.pythonhosted.org/packages/00/80/1ea14c5d80e589e415973db3e47c78c2219a305b808b2b506395342c1d79/mypy-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:85c5385b93012ffa3b31479ab579aef5415f4f3a32c6cf1ae07a984d2a0ff461", size = 15328164, upload-time = "2026-07-13T11:31:35.723Z" }, + { url = "https://files.pythonhosted.org/packages/37/28/8223157404a3d51920078459c37f80fbdc590e1d8ea049dc5ce48643022a/mypy-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:13b1b16e2fa39f3b2e33fb1c468abc7a69369fa2e886b4b87b5afc81472325cd", size = 11136472, upload-time = "2026-07-13T11:27:37.018Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cc/ea27e5959c5f258585a756b252031f3b313583d81b5064b2bebc41d3706b/mypy-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:b5cd2f027a972a4a5f2278a11fac9747f5f81a53a30b714d74950b6807e55568", size = 10135800, upload-time = "2026-07-13T11:30:08.92Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/0e7e592619e2133596a47cdd642534b0456545c218430bd3b9d8fefdd1b1/mypy-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d53fc67b9d28a43c6199077f49fea0f05839e36cf6158500331c9549225e5a5", size = 15026523, upload-time = "2026-07-13T11:34:49.206Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/1e1731df090a857df2807177a4626863e5ac0f0256513c35780efe53986f/mypy-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fbc00cee7bdbb9291979ddc9d08034a29dfcda4932628c9bbc28c1edd589df0c", size = 14032189, upload-time = "2026-07-13T11:33:57.168Z" }, + { url = "https://files.pythonhosted.org/packages/44/95/cab921f4a806e171f34113e6181dd23c55358ccf6a80741269ef594a410e/mypy-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04e617030eca5221909c8b7d8d7fd1c637948199aa2100b2ad9813feb07e1491", size = 14198696, upload-time = "2026-07-13T11:32:12.767Z" }, + { url = "https://files.pythonhosted.org/packages/66/80/e6d008bb19fe446e3662d85e0e2717bf9f2d611a2164fb29d6e067dbf46c/mypy-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56c184d2c20ca6b6378d58d1960270a767f41f5e44acbbd27f05effef4f4e1d7", size = 15286904, upload-time = "2026-07-13T11:34:27.594Z" }, + { url = "https://files.pythonhosted.org/packages/db/83/94397c9293608a364aa03e8084fb34ede4ae976a260384b9b52929308135/mypy-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3961a4a34b05f7c74b0f05aa51fbfe99a2d1e126038df40318d15c8f558b7ef3", size = 15528342, upload-time = "2026-07-13T11:34:07.819Z" }, + { url = "https://files.pythonhosted.org/packages/cf/96/d8b37d819adec6cfccfb1fd3afc1735d94717ddeafb45536db9c6943e09b/mypy-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b1942b9314d4c784b8ea1dbab4972603290e5dd5630f06675f13aec97526bc4c", size = 11218346, upload-time = "2026-07-13T11:28:27.745Z" }, + { url = "https://files.pythonhosted.org/packages/2b/cd/cd9f725b19b19e5b530a154cf9bcf9e94279c5d55b3c34fb42b3aa48ea1b/mypy-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:be51653d7669d7d7955d613b8d0bb57d5b652eaf71a873ddf65ac87254dd2595", size = 10204525, upload-time = "2026-07-13T11:31:02.552Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ae/f7d056eb0294586a572d0d0d89580ec633c064db520f11d37d5a2fb833bd/mypy-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:91ad22a52ae2c7e621c2f67c94d5a17f66b3209a4cff5cf8a573579835c69e97", size = 14947298, upload-time = "2026-07-13T11:27:47.734Z" }, + { url = "https://files.pythonhosted.org/packages/32/d5/db3e7af01e7844d21662c6ddc1f7825ec7cb4053f0391ac02faf3638396f/mypy-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:99ac767cc5d3b64c8d0ae226ead10c96694f94e4e7da1668642225dcd4e75aac", size = 13950768, upload-time = "2026-07-13T11:27:57.726Z" }, + { url = "https://files.pythonhosted.org/packages/d9/fb/43c031f0190513d1ec248ed037eceb742ddd2a4d74bbf406658a28173837/mypy-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6d2c484742a4d7b0ed6d07b143375624d3b899c5749c7b3c947f56261f48a6", size = 14151586, upload-time = "2026-07-13T11:29:18.615Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c3/f8b2ffc60883084da91be51af58e88a7ffd4ff9795acb7d902ff88d31eb1/mypy-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7da939dd335cfd2ad788bdfd081c9f4e47634ab995e5a45eb15fd1e5bc052f8b", size = 15227411, upload-time = "2026-07-13T11:30:29.904Z" }, + { url = "https://files.pythonhosted.org/packages/83/2e/16b917fc7adcf03f1aadddfc93aab804ffb234b1ab09c0ffd6d92a5d34a2/mypy-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7247eb2824f996722a949530183394921ca71deb9680052a338cf53cff7925c2", size = 15478790, upload-time = "2026-07-13T11:33:14.686Z" }, + { url = "https://files.pythonhosted.org/packages/c0/88/aaa65a93c73d0cdae7e42f8adb302bf6885bb281302084f99d0290a35347/mypy-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:75b0984bb3cbd76bb5c9291a8671f7ae66ca3b51c7584c358fc2e923259f0757", size = 11234919, upload-time = "2026-07-13T11:33:39.28Z" }, + { url = "https://files.pythonhosted.org/packages/35/19/b40de63f1a80e63bc2d40f0679a6a8dbd34e95176c8122119bdf406aa552/mypy-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:d78fcf900b59cb7e82cb7e3a235e31b462d9333d92285bd1e4952d355b8ffba1", size = 10201510, upload-time = "2026-07-13T11:31:52.619Z" }, + { url = "https://files.pythonhosted.org/packages/a4/58/fa0ae047da911f540284009b4f44b96fe09d83c076d7c103e9d645f46303/mypy-2.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ea317b060ce83e26050f8f9e4d7d6bf44ed7597c8ff9990bccffbb9d1d8522db", size = 14941909, upload-time = "2026-07-13T11:32:34.332Z" }, + { url = "https://files.pythonhosted.org/packages/15/14/2ba1d61452d7c2a7fe12741e8d374e52b183476b07aa7f9e2a0d02b0720a/mypy-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:094af99f92638aa92852326188b85a89e50f4a472f44827c03362228482f0762", size = 13967581, upload-time = "2026-07-13T11:30:00.587Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5a/483fb9e5ffbbb1a28dccc7b0a13d141b17ac769b6c9f488c0a0c63698962/mypy-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de121747278144fc9ae7caa2e978cf5df12aebc82933182f5b3b86081a30baef", size = 14168807, upload-time = "2026-07-13T11:28:48.6Z" }, + { url = "https://files.pythonhosted.org/packages/ae/77/70d7a10732063beb74ad713682cf871e88f5c5fa39bfc8beff8a524bf9cb/mypy-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37fa4de896a84e2dc9200d91e614c22563b43d1a266789d4bbac7b22ebe6192b", size = 15200144, upload-time = "2026-07-13T11:31:25.283Z" }, + { url = "https://files.pythonhosted.org/packages/56/72/766218ac783be4fdfcd699b90037b63017348a3e86fb2c1fbfb18302637d/mypy-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f1b3a98dfd21058bc759bb3337d5d1f61d0fdf9f3cf9c00f4291790fb5427bff", size = 15460389, upload-time = "2026-07-13T11:29:29.077Z" }, + { url = "https://files.pythonhosted.org/packages/38/4e/8a9db7411ecb8ec0cb1fd05dba432f28bafffcd38b4e887714a4a0506689/mypy-2.3.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:944c665d984157cb96a679dfb7a4a81dd1d36b24b9c284b699514e6e626b82d4", size = 7753664, upload-time = "2026-07-13T11:29:08.147Z" }, + { url = "https://files.pythonhosted.org/packages/65/4c/c3f8bfd6ed0e5e38b5a244403b27f821d433443df5a15a278417c10a3a3c/mypy-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:4359424140d985192c778c1ce2c114a10c1ca58a381ed79cfa70d37df94b299f", size = 11417237, upload-time = "2026-07-13T11:33:47.467Z" }, + { url = "https://files.pythonhosted.org/packages/3c/00/89a32eaf5ccf174bc4f90db0eaea5d70636c01b8d49f384bdab2e8834390/mypy-2.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:3dd0bed92c4bdec57c42505b96416fb9e6a5aa7be84d2809bcd5f2ecec2860d7", size = 10389252, upload-time = "2026-07-13T11:31:43.81Z" }, + { url = "https://files.pythonhosted.org/packages/31/56/104f93d69aa9f339b6b9d3b0a7faa699b8b466c942cf3ae86cc2a2ec0915/mypy-2.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:691fdc37132b1ae628d834f672e74de83462d9fb4aff621835767fb43a8dd373", size = 16385495, upload-time = "2026-07-13T11:29:49.818Z" }, + { url = "https://files.pythonhosted.org/packages/d2/03/f1d2123313f55efafdd27706960f43a771c62f1b68426c76043f3ab9ebf3/mypy-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:aec15d465d477558fd842757b487849007311cf3897849cdda0e3162ac0ac556", size = 15098155, upload-time = "2026-07-13T11:30:40.301Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5d/d5f9200399b445e81726c4f23becee33f233aee81c72680b1ef3a258b641/mypy-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b352b7e49f5e6576009e8df730e1ff4f915cb565b851b396d2ffe2f5a6f5da88", size = 15514155, upload-time = "2026-07-13T11:34:38.569Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ce/69977c555f08faa3190cfde44189b89dbd56861b1ab97aa18fc5f3a2e4a3/mypy-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c6c6bf687b17f90dbfcad95b960d32eaa0154c00da45f03ab50bf8952e047fe", size = 16766351, upload-time = "2026-07-13T11:33:29.195Z" }, + { url = "https://files.pythonhosted.org/packages/bc/92/6648b6caa3ab9e00f9ac0c2a78307805f873dd48139b24a6f6f7c3667bbf/mypy-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f4ed18f111bfe2d599bca7468e7f9251042c1c2118f762c8de2766a56d773c60", size = 17043490, upload-time = "2026-07-13T11:30:53.927Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ab/0dc91d80f3f016634c68d451f294a97320fe903a9b6f90b9e57b3f7f1717/mypy-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0b025a93cffb9781d231f232be07a17912f35f10a313c24f301c81e842870654", size = 12146869, upload-time = "2026-07-13T11:29:38.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/b5/4c964d02634ba81f4d1c84838e5c5b18ab06d13ed568960f5d6318495ccc/mypy-2.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:adebc76aab4f3495a88b41d48aa4aff0c03f2822501da76625afcca5975f19e5", size = 10965113, upload-time = "2026-07-13T11:28:07.056Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fa/fdc54fe583ba3cafbcedfb70eeeaf03849f75b1827a07096c7bd996f582d/mypy-2.3.0-py3-none-any.whl", hash = "sha256:6b1cdb579446b60432432b2b2403a6201b4b475a004d7f488511c9ba177c9e88", size = 2753292, upload-time = "2026-07-13T11:33:18.48Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "narwhals" +version = "2.24.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/1d/58946e5aab18393e793bd4add6985b95d0e01c3a2d832f38f54468b10dcd/narwhals-2.24.0.tar.gz", hash = "sha256:b5c0f684ccd9d7475b564111e319a4964abcf2baf79d3cf6b1003d06ac9b828d", size = 661143, upload-time = "2026-07-13T10:49:19.086Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/85/a5bfaebfd305ac18b57b0854d74e37e586809061a91fda62f0bd50c8518e/narwhals-2.24.0-py3-none-any.whl", hash = "sha256:42fdedf44e5b2ca7505630d45b4ac3058f38d8485cba9fe1652ca23152df7489", size = 461030, upload-time = "2026-07-13T10:49:17.571Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" }, + { url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" }, + { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" }, + { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" }, + { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" }, + { url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, + { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" }, + { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" }, + { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" }, + { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" }, + { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" }, + { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" }, + { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" }, + { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" }, + { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" }, + { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" }, + { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" }, + { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" }, + { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" }, + { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" }, + { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" }, + { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" }, + { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" }, + { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" }, + { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" }, + { url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" }, + { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" }, +] + +[[package]] +name = "numpy" +version = "2.5.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version >= '3.13' and python_full_version < '3.15'", + "python_full_version == '3.12.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/22/fd/89965aa4ac08c74998539fcbf24fa3540f3e15237fbeb6bcf9c908f4aade/numpy-2.5.1.tar.gz", hash = "sha256:a48a113e6afea91f5608793bafa7ef2ad481fefbda87ec5069f483de61cb9fa3", size = 20755553, upload-time = "2026-07-04T17:08:00.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/7b/14687aa674250e5e546f616f486b0d56d3631cd5b2415739141ce40bdcea/numpy-2.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c889b56fe48b1018f764b0eec8df59ab654e9148aa91faa12596043500de277", size = 16801574, upload-time = "2026-07-04T17:06:12.423Z" }, + { url = "https://files.pythonhosted.org/packages/e1/19/cc5bb2a3f2913d27d6dbb2c78d25921fabaedc6741d4a5a615a11f3c5bf3/numpy-2.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab451b59c5643c570974c43aef780703ef1d3b4965d2be07afd530615a9358d1", size = 11772250, upload-time = "2026-07-04T17:06:15.726Z" }, + { url = "https://files.pythonhosted.org/packages/42/77/fdf34a71dd30f54979b18603bee915e0aaf825b07afe79acd60b04b691e2/numpy-2.5.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:78798bd5b9ad744056af8efa90e3b9ddaa53272a0848a483084a1cc0a13b2dc0", size = 5331516, upload-time = "2026-07-04T17:06:17.913Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e2/eb7efa015b4cce41e2517bf182a7fce0d7d5b9d9ed76a29bfa0f4fe4505c/numpy-2.5.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:2ae0ca40bcb22d6ba59c1dfd5446f49940b0f2d821fde133f10dda11f816b84e", size = 6664863, upload-time = "2026-07-04T17:06:20.02Z" }, + { url = "https://files.pythonhosted.org/packages/a9/4b/a2b32dd94ee9ffbeecb28152240042a3949db33b1c834d44090b80e1b3b8/numpy-2.5.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61ac47e772e6b8ea489e1d2f441a34c5c3ac17327e7ce294cbdf535795ad4e75", size = 15167977, upload-time = "2026-07-04T17:06:21.621Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a9/6e73d68500f80773f65f0654ea932019d6694329a0eb0ed0533de38df376/numpy-2.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:59fda5e192b570217ec2580c96f00e9a7e12ef6866a900eb089b62c1a32545ca", size = 16672469, upload-time = "2026-07-04T17:06:24.064Z" }, + { url = "https://files.pythonhosted.org/packages/24/7d/ad3e59015135f5261c95fd4cafeff159c955febd83a99a1d9250c4233815/numpy-2.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f7119ebff1a9829e9f431a4f9d28e703023bb6b9fe7c8f724467dbfc27c94ab3", size = 16527531, upload-time = "2026-07-04T17:06:26.69Z" }, + { url = "https://files.pythonhosted.org/packages/83/d0/a39b2fbcde9cb17a1dac678f254b33a6336298af9df338824c685425d5e8/numpy-2.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e824c2acf8862052246be5a44c15da1777940c60d010dd2aab897824d9c430f9", size = 18431940, upload-time = "2026-07-04T17:06:29.521Z" }, + { url = "https://files.pythonhosted.org/packages/04/12/cff070947791c1ed425ff76413189adbdc2fbe215eba7ce7fa454a03c7f8/numpy-2.5.1-cp312-cp312-win32.whl", hash = "sha256:08d60c810432eb83360958dea0999ac4cfb94531ea8efcbf0b7f277c2068aeb2", size = 6066764, upload-time = "2026-07-04T17:06:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/65/66/53f31807a48a750f9d748da273bc3fcedd12b27ff1f3e373bfec55ef2dc0/numpy-2.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:f7d60026c0bdb1380e83bfa7a0419c4577ee4b9a08880afcb6dadeb74c649fa2", size = 12430966, upload-time = "2026-07-04T17:06:34.926Z" }, + { url = "https://files.pythonhosted.org/packages/2b/2a/d1a88066b1c14186f5d3c0d18c94f17b064511982bab0578d49ee9d43c29/numpy-2.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:17a25e09640602e10bc8de0e6fa2b3fd68eedd84ba6d7842dc8f32f9ab87bd0b", size = 10350488, upload-time = "2026-07-04T17:06:37.785Z" }, + { url = "https://files.pythonhosted.org/packages/eb/07/ec2a3f0c91761581d4b7104a740791800025983f9a4dc4e73f91a99aeac4/numpy-2.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0bfebd8695f9863592fe744be833a258120b14a9f39da255e8aa8fade2c0ddd1", size = 16796419, upload-time = "2026-07-04T17:06:40.37Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ab/ddb499fc4f8780354395face5b65c7fd107bcd6e1d667a5f07d046956f6f/numpy-2.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30b44a6b53a7ae63c54c089a8726e5563ed302716c5b7ccc85afade40b0e7ff6", size = 11765832, upload-time = "2026-07-04T17:06:42.768Z" }, + { url = "https://files.pythonhosted.org/packages/88/b3/3c28c558a09fc72100c646dac6d2fce8e834c471b0edca01a29996706117/numpy-2.5.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6165343f81b56ef8f514f396989e529b61d9dc709b99421b07e9f3e698e2287d", size = 5325143, upload-time = "2026-07-04T17:06:45.466Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0e/ce19b985bb15c596f4f05954e76cccc77c845083b3b8f938a6c68e523128/numpy-2.5.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4939237038ada79308dda3204ac6462df056b5672b2e25db1149cf873668b3e1", size = 6659749, upload-time = "2026-07-04T17:06:47.288Z" }, + { url = "https://files.pythonhosted.org/packages/2e/20/1ee6614d64332a1bba6411f38e68cb79eec1b2459e20a623777c5c5492a2/numpy-2.5.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c6759f538fb912fc46de0a6b1758ccf7b57bc7c7ebebc23974fdac3de8db0cd", size = 15164716, upload-time = "2026-07-04T17:06:49.494Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a7/2bcd3fdbb87804755c35b729bf8709d62025c5f4cfd7d5b2415997097515/numpy-2.5.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9726558e8db4a5bf7929a70ae50f63abda4daf0efe810e3bfbab95976f75fc1a", size = 16661440, upload-time = "2026-07-04T17:06:52.061Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d7/a41e3310c886fe457d36e670bbf24fae411aca8a7b6ad92a32afd924077c/numpy-2.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3935f3b419b244a02732676fa5317a9193cc596a4c0646db07e5b421229ac9f7", size = 16526305, upload-time = "2026-07-04T17:06:54.605Z" }, + { url = "https://files.pythonhosted.org/packages/53/75/4333a9a707c1edd3a4e1a0c58eca52c0f31e55089fa80db02b5565b24df7/numpy-2.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc932a65ded7ce9013d120845a2514dcccb1a67bfc8deb8d37633762951904a6", size = 18423008, upload-time = "2026-07-04T17:06:57.54Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/e314a32b1c11a2ffe818ddad3a57b50b4b6e1b6c487192eb50cdef0415d0/numpy-2.5.1-cp313-cp313-win32.whl", hash = "sha256:4b4ff1608417eb7a59da7b967bbb798cacfe071d2caf526a24281cd562072ed9", size = 6063885, upload-time = "2026-07-04T17:07:00.14Z" }, + { url = "https://files.pythonhosted.org/packages/10/70/800b3fca480af32df9e8ea9f3d4a0c8feb4b32d7f195d174eabbda4829ad/numpy-2.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:6c3fe51bc6a16453d452997053454f309e8e0ed7b42d6b361ce4ac8c32913d74", size = 12425674, upload-time = "2026-07-04T17:07:02.387Z" }, + { url = "https://files.pythonhosted.org/packages/8b/0b/196350c122f50f6ca56846f2d71efd5e0d24b7b2e07355e019b2e2c7a11e/numpy-2.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f7feb014281029e628ba2d5a007407443b06e418b6fe451d1e2adcbc8eba0107", size = 10350256, upload-time = "2026-07-04T17:07:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/db/f4/731b6085a83faf6ca843394cbd5e217280c214399f7e8b21b9f552af0ae2/numpy-2.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7c786fe9a5bbe360022e584c5a34cf6b54265c71bd7ec8ac3d8fec38968071f8", size = 16795063, upload-time = "2026-07-04T17:07:07.374Z" }, + { url = "https://files.pythonhosted.org/packages/bf/64/0e215f2048dd11a55bb989ed41b3585ef57452404e638d703a211a3e4157/numpy-2.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32985c896d897419ef8da6917872d80b78ad0ea26d85b23245c7366ffde76d75", size = 11776652, upload-time = "2026-07-04T17:07:09.907Z" }, + { url = "https://files.pythonhosted.org/packages/b5/59/2b844c7a6e9deff69b404a66221e1542937734f65d5e6e39411876053862/numpy-2.5.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:efd736408cc97c79b9e6917338dfc8f06013b2274f992e96b1d9a81a71e2a2c2", size = 5335944, upload-time = "2026-07-04T17:07:12.227Z" }, + { url = "https://files.pythonhosted.org/packages/86/51/9bf7cb2cabcebc9e017e4ec7e6322b378317a542c08b4cb68479c1efc716/numpy-2.5.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:ab84dc6b074fa881cae55bea94cc4f68e285181ba7f32497bf7dee6b1496165b", size = 6656266, upload-time = "2026-07-04T17:07:14.368Z" }, + { url = "https://files.pythonhosted.org/packages/83/3e/fb7615b211b82a32f44d5180a6d421b61f84d4fadd578b48ba4ac34e189f/numpy-2.5.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caf3e317d33d60c37986b452613f4ab51246d0691350c03d0cb4a898627f4a95", size = 15179720, upload-time = "2026-07-04T17:07:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/0f992cb24560673496c5d68de61913b57166ce530ffda07c1f280e0cc464/numpy-2.5.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54ad769f17bc2d833b620851989f62054fb9ab93c969d9e1dc3c8e3d56beea21", size = 16664835, upload-time = "2026-07-04T17:07:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/97d6475ee91afe2587797d09446f9d3e475ad4cb681662d824809327b75a/numpy-2.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c12afb53450fa976d4c681c50a7423729a4c51c0465ed9f32b8a9cabbc472373", size = 16539135, upload-time = "2026-07-04T17:07:22.015Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/4db81e4ba0be7e2776b1de68c82aa862c7f8ec27e1b4927d4ae075e20678/numpy-2.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c11c405efc5ff6816d5983c96cdfa215bab3428961243af3ff59b228490438", size = 18426684, upload-time = "2026-07-04T17:07:24.941Z" }, + { url = "https://files.pythonhosted.org/packages/1f/64/c0ba2d90724d450279a7df8f32057241070250a26a7e2b5337d77347f481/numpy-2.5.1-cp314-cp314-win32.whl", hash = "sha256:f2479a47f8d5932d1718168a681ad6e536a9df484c83cfcf9de365e164537ace", size = 6116103, upload-time = "2026-07-04T17:07:27.622Z" }, + { url = "https://files.pythonhosted.org/packages/c1/1a/837f9ed7405adcd7a40538792eb169eddd8fa5630c16a1ef49dae71a30f4/numpy-2.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:24d0eb82c0541d3415a33425db64ae439dffccd7b4dbcb30e7c35120205c506a", size = 12562177, upload-time = "2026-07-04T17:07:29.887Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/49707938b6dd0a78a9178dd93227dc89e4c11af47f5c798d70366e8d0483/numpy-2.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:5a4c988b38d261deeeaad9954e3deb091ad905c94e8bb6708654ef1d97f286b0", size = 10627739, upload-time = "2026-07-04T17:07:32.568Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c7/bb4b882cfe7f299cbc8b66e42e7dd78cf9d14e40f9469fc5e3db7e15b3bd/numpy-2.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a33276be12fa045805f477f22482088b66bb758ffbe89a9d21457de863a32e22", size = 11894709, upload-time = "2026-07-04T17:07:34.941Z" }, + { url = "https://files.pythonhosted.org/packages/40/3f/5af7f4a7f6224aef48017aa82bb6174c7a659d724be0c75017b7e64a55b4/numpy-2.5.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f089d7b00756190aacf1f5d34bdf38c3c430ac82b4f868f8cede73380460fce7", size = 5453810, upload-time = "2026-07-04T17:07:37.495Z" }, + { url = "https://files.pythonhosted.org/packages/20/c9/3474309bc94d634d3f9c3eddf03250ecb8c22cd948ef16fef69a77cc5d7b/numpy-2.5.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:09e9bfd8d2cf479c7d174804fb3811c53a8e9f20a37444008606b57d6b7a826d", size = 6761189, upload-time = "2026-07-04T17:07:39.563Z" }, + { url = "https://files.pythonhosted.org/packages/90/8a/558ae39fdd55d7e7f7fef9a84a6e964ac6b23edbd2a07e52bb084500507d/numpy-2.5.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e68d8dd1e7eba712948f2053a29ec86917bc70ba1358df869d9f06649ef9cf09", size = 15225039, upload-time = "2026-07-04T17:07:41.682Z" }, + { url = "https://files.pythonhosted.org/packages/63/27/ca7392b2d030277bdf0273e7d23255b3ee57d57a7c170a6f4fb3981e1e5d/numpy-2.5.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99d5095fa265a0c4152e7bb12759e14381ef5496152f1ce58f44bdf55c44beb4", size = 16701306, upload-time = "2026-07-04T17:07:44.611Z" }, + { url = "https://files.pythonhosted.org/packages/02/42/03d53ae7996c44d4374a8262e9dc41671fd56cbb98f7d47ef85cf5da4c6b/numpy-2.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ab87a91b3cc3382b8956095bd8f95e00cf679bb81554339be1a2ba404a1473c1", size = 16589955, upload-time = "2026-07-04T17:07:47.694Z" }, + { url = "https://files.pythonhosted.org/packages/7b/15/6c1784ae469640e65db111e9a34b3d0f14d91e8a38b9ce34810ced370dbb/numpy-2.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:224ca51130ef7da85bea2191625181cb4f337f9cb64b471f10c1a12aa8b60077", size = 18464252, upload-time = "2026-07-04T17:07:50.684Z" }, + { url = "https://files.pythonhosted.org/packages/94/a8/f98e50356cf167df656c526c2dfeec2d7dde182f2a3da4b458a5938e2776/numpy-2.5.1-cp314-cp314t-win32.whl", hash = "sha256:6eab239876581b2b3c5a242281b6007bbdbcd1c7085d7709bb57c5929b11e6bf", size = 6263298, upload-time = "2026-07-04T17:07:53.445Z" }, + { url = "https://files.pythonhosted.org/packages/72/ac/96ae880cdecad0b3275d9359fcec72667b49a4863c9f12942e43679dda02/numpy-2.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:83ce9c80d5b521b0d77ddcbe5447c218d247929b6cc056ca5351342accfff0af", size = 12748623, upload-time = "2026-07-04T17:07:55.384Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5a/4d2b1601df3602dba7a14f3348ba9bfe94a18adb428e693df6154c293831/numpy-2.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:5a6db61f9aaa57e369905c67d852045d3c4f7126405b29d09b19dec118e9c9cb", size = 10697674, upload-time = "2026-07-04T17:07:58.506Z" }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.30.7" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/8c/554bb020501d6c04ad8127d83f728137f8f9123f991666efbdcf9095a221/nvidia_nccl_cu12-2.30.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:03ecd776fd1d58fd2c9a0a687dcf8db9ecd0057382dba646fa3d65786d4a9ea1", size = 303277471, upload-time = "2026-06-09T03:24:16.327Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/e7ffa9c324ae260e5dbb4af2cd557bf7a8d155c8ac7b79a785fe1796fb92/nvidia_nccl_cu12-2.30.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:8ce1b8213f61f2bfac132e6df890af6450b77cbd140c6ce4e98cb0c2d8e678c9", size = 303361239, upload-time = "2026-06-09T03:24:53.816Z" }, +] + +[[package]] +name = "nvidia-nccl-cu13" +version = "2.30.7" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/21/a73174c6157101bdf1ffc22b517f76ff0082613989dd9bc8f43e8034caac/nvidia_nccl_cu13-2.30.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:ca786ffa5a647c75d4d1f5cc72a6c4f537947e2ba8823d7c8aaf768e7a7b9f77", size = 215983881, upload-time = "2026-06-09T03:23:15.633Z" }, + { url = "https://files.pythonhosted.org/packages/3f/34/c500f90c7ae641b8e0f98965b36b8a7ac79cc8b296e8d251fe3eb592ee54/nvidia_nccl_cu13-2.30.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:cefa7fdb9710efd0f39c5f1be1d61ff6fc9a996c451265bd7fbdcf9455ed4b50", size = 215965170, upload-time = "2026-06-09T03:23:39.73Z" }, +] + +[[package]] +name = "onnx" +version = "1.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ml-dtypes" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { 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" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/04/471f234e2716c83f17a26e1b50cd64c39428373e91dd018aafb3d499c108/onnx-1.22.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:6d0ffffd63a4ecc21ddaeddd5bf02099cb701aa4243f2de00122726869065ca4", size = 20167110, upload-time = "2026-06-15T12:48:59.152Z" }, + { url = "https://files.pythonhosted.org/packages/99/40/540a2fe3c49ce1709ff2015de20d9a351264fb442f8998f92cf0ba7e279e/onnx-1.22.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33ce94119bbb7f05d9caea4ea7549f5185a54369f6bbc9f70171bd5ee6935bbc", size = 18892738, upload-time = "2026-06-15T12:49:02.139Z" }, + { url = "https://files.pythonhosted.org/packages/f8/0c/f41d5b89c38fb2ec410ab23c24fa110af786093b140644f7f953e436743b/onnx-1.22.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:87a3077958f66f9a26dec10077ac28326d9cec2cbe1f0b040947243449754573", size = 19110354, upload-time = "2026-06-15T12:49:05.031Z" }, + { url = "https://files.pythonhosted.org/packages/11/8e/9f41d132855e93c2808cdd4afab1b5af67bd5e82e4a4fa9248006e4df87e/onnx-1.22.0-cp310-cp310-win32.whl", hash = "sha256:8a5eccce2d5fc6c5046928a9aa7cdd9750ea4a586f8de341d3d40d820c35fdec", size = 17083595, upload-time = "2026-06-15T12:49:08.599Z" }, + { url = "https://files.pythonhosted.org/packages/e8/52/86caff81786a5428485795c79175ae2b12a630795bcb267b84e5f9e98450/onnx-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:5c1c0408a9d4b4df33851672e5fc7590b96301ee123396d608f9ab6f045ab06b", size = 17215270, upload-time = "2026-06-15T12:49:11.483Z" }, + { 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 = "onnxmltools" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "onnx" }, + { name = "protobuf" }, + { name = "skl2onnx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/3e/85a40b6e56a8aaa45bffc9eb00f93182b87841b4dc5a4198ea609993e17c/onnxmltools-1.16.0.tar.gz", hash = "sha256:cd76e0a7ba6a3c4ca4acf3b4c7973cda6a70f2edc146ab11d4efc3dfbee6805a", size = 208397, upload-time = "2026-01-30T12:45:06.1Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e6/6713d9a089a6861b4bf748f02a6238cb2759968aadf672dccef3e960376b/onnxmltools-1.16.0-py3-none-any.whl", hash = "sha256:7b27196e7dcc0d9de29110f211e7941ad1c71dd97606baa729144d9acd105d3c", size = 303991, upload-time = "2026-01-30T12:45:04.809Z" }, +] + +[[package]] +name = "onnxruntime" +version = "1.23.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "coloredlogs", marker = "python_full_version < '3.11'" }, + { name = "flatbuffers", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "packaging", marker = "python_full_version < '3.11'" }, + { name = "protobuf", marker = "python_full_version < '3.11'" }, + { name = "sympy", marker = "python_full_version < '3.11'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/d6/311b1afea060015b56c742f3531168c1644650767f27ef40062569960587/onnxruntime-1.23.2-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:a7730122afe186a784660f6ec5807138bf9d792fa1df76556b27307ea9ebcbe3", size = 17195934, upload-time = "2025-10-27T23:06:14.143Z" }, + { url = "https://files.pythonhosted.org/packages/db/db/81bf3d7cecfbfed9092b6b4052e857a769d62ed90561b410014e0aae18db/onnxruntime-1.23.2-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:b28740f4ecef1738ea8f807461dd541b8287d5650b5be33bca7b474e3cbd1f36", size = 19153079, upload-time = "2025-10-27T23:05:57.686Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4d/a382452b17cf70a2313153c520ea4c96ab670c996cb3a95cc5d5ac7bfdac/onnxruntime-1.23.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f7d1fe034090a1e371b7f3ca9d3ccae2fabae8c1d8844fb7371d1ea38e8e8d2", size = 15219883, upload-time = "2025-10-22T03:46:21.66Z" }, + { url = "https://files.pythonhosted.org/packages/fb/56/179bf90679984c85b417664c26aae4f427cba7514bd2d65c43b181b7b08b/onnxruntime-1.23.2-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4ca88747e708e5c67337b0f65eed4b7d0dd70d22ac332038c9fc4635760018f7", size = 17370357, upload-time = "2025-10-22T03:46:57.968Z" }, + { url = "https://files.pythonhosted.org/packages/cd/6d/738e50c47c2fd285b1e6c8083f15dac1a5f6199213378a5f14092497296d/onnxruntime-1.23.2-cp310-cp310-win_amd64.whl", hash = "sha256:0be6a37a45e6719db5120e9986fcd30ea205ac8103fd1fb74b6c33348327a0cc", size = 13467651, upload-time = "2025-10-27T23:06:11.904Z" }, + { url = "https://files.pythonhosted.org/packages/44/be/467b00f09061572f022ffd17e49e49e5a7a789056bad95b54dfd3bee73ff/onnxruntime-1.23.2-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:6f91d2c9b0965e86827a5ba01531d5b669770b01775b23199565d6c1f136616c", size = 17196113, upload-time = "2025-10-22T03:47:33.526Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a8/3c23a8f75f93122d2b3410bfb74d06d0f8da4ac663185f91866b03f7da1b/onnxruntime-1.23.2-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:87d8b6eaf0fbeb6835a60a4265fde7a3b60157cf1b2764773ac47237b4d48612", size = 19153857, upload-time = "2025-10-22T03:46:37.578Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d8/506eed9af03d86f8db4880a4c47cd0dffee973ef7e4f4cff9f1d4bcf7d22/onnxruntime-1.23.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbfd2fca76c855317568c1b36a885ddea2272c13cb0e395002c402f2360429a6", size = 15220095, upload-time = "2025-10-22T03:46:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/e9/80/113381ba832d5e777accedc6cb41d10f9eca82321ae31ebb6bcede530cea/onnxruntime-1.23.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da44b99206e77734c5819aa2142c69e64f3b46edc3bd314f6a45a932defc0b3e", size = 17372080, upload-time = "2025-10-22T03:47:00.265Z" }, + { url = "https://files.pythonhosted.org/packages/3a/db/1b4a62e23183a0c3fe441782462c0ede9a2a65c6bbffb9582fab7c7a0d38/onnxruntime-1.23.2-cp311-cp311-win_amd64.whl", hash = "sha256:902c756d8b633ce0dedd889b7c08459433fbcf35e9c38d1c03ddc020f0648c6e", size = 13468349, upload-time = "2025-10-22T03:47:25.783Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9e/f748cd64161213adeef83d0cb16cb8ace1e62fa501033acdd9f9341fff57/onnxruntime-1.23.2-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:b8f029a6b98d3cf5be564d52802bb50a8489ab73409fa9db0bf583eabb7c2321", size = 17195929, upload-time = "2025-10-22T03:47:36.24Z" }, + { url = "https://files.pythonhosted.org/packages/91/9d/a81aafd899b900101988ead7fb14974c8a58695338ab6a0f3d6b0100f30b/onnxruntime-1.23.2-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:218295a8acae83905f6f1aed8cacb8e3eb3bd7513a13fe4ba3b2664a19fc4a6b", size = 19157705, upload-time = "2025-10-22T03:46:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/4e40f2fba272a6698d62be2cd21ddc3675edfc1a4b9ddefcc4648f115315/onnxruntime-1.23.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76ff670550dc23e58ea9bc53b5149b99a44e63b34b524f7b8547469aaa0dcb8c", size = 15226915, upload-time = "2025-10-22T03:46:27.773Z" }, + { url = "https://files.pythonhosted.org/packages/ef/88/9cc25d2bafe6bc0d4d3c1db3ade98196d5b355c0b273e6a5dc09c5d5d0d5/onnxruntime-1.23.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f9b4ae77f8e3c9bee50c27bc1beede83f786fe1d52e99ac85aa8d65a01e9b77", size = 17382649, upload-time = "2025-10-22T03:47:02.782Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b4/569d298f9fc4d286c11c45e85d9ffa9e877af12ace98af8cab52396e8f46/onnxruntime-1.23.2-cp312-cp312-win_amd64.whl", hash = "sha256:25de5214923ce941a3523739d34a520aac30f21e631de53bba9174dc9c004435", size = 13470528, upload-time = "2025-10-22T03:47:28.106Z" }, + { url = "https://files.pythonhosted.org/packages/3d/41/fba0cabccecefe4a1b5fc8020c44febb334637f133acefc7ec492029dd2c/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_arm64.whl", hash = "sha256:2ff531ad8496281b4297f32b83b01cdd719617e2351ffe0dba5684fb283afa1f", size = 17196337, upload-time = "2025-10-22T03:46:35.168Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f9/2d49ca491c6a986acce9f1d1d5fc2099108958cc1710c28e89a032c9cfe9/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:162f4ca894ec3de1a6fd53589e511e06ecdc3ff646849b62a9da7489dee9ce95", size = 19157691, upload-time = "2025-10-22T03:46:43.518Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a1/428ee29c6eaf09a6f6be56f836213f104618fb35ac6cc586ff0f477263eb/onnxruntime-1.23.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45d127d6e1e9b99d1ebeae9bcd8f98617a812f53f46699eafeb976275744826b", size = 15226898, upload-time = "2025-10-22T03:46:30.039Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2b/b57c8a2466a3126dbe0a792f56ad7290949b02f47b86216cd47d857e4b77/onnxruntime-1.23.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8bace4e0d46480fbeeb7bbe1ffe1f080e6663a42d1086ff95c1551f2d39e7872", size = 17382518, upload-time = "2025-10-22T03:47:05.407Z" }, + { url = "https://files.pythonhosted.org/packages/4a/93/aba75358133b3a941d736816dd392f687e7eab77215a6e429879080b76b6/onnxruntime-1.23.2-cp313-cp313-win_amd64.whl", hash = "sha256:1f9cc0a55349c584f083c1c076e611a7c35d5b867d5d6e6d6c823bf821978088", size = 13470276, upload-time = "2025-10-22T03:47:31.193Z" }, + { url = "https://files.pythonhosted.org/packages/7c/3d/6830fa61c69ca8e905f237001dbfc01689a4e4ab06147020a4518318881f/onnxruntime-1.23.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d2385e774f46ac38f02b3a91a91e30263d41b2f1f4f26ae34805b2a9ddef466", size = 15229610, upload-time = "2025-10-22T03:46:32.239Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ca/862b1e7a639460f0ca25fd5b6135fb42cf9deea86d398a92e44dfda2279d/onnxruntime-1.23.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2b9233c4947907fd1818d0e581c049c41ccc39b2856cc942ff6d26317cee145", size = 17394184, upload-time = "2025-10-22T03:47:08.127Z" }, +] + +[[package]] +name = "onnxruntime" +version = "1.28.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version >= '3.13' and python_full_version < '3.15'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "flatbuffers", marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.11'" }, + { name = "protobuf", marker = "python_full_version >= '3.11'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/4d/5014667e2a3a77d6e1b74cc3d88948d06163b8e0a33a84c85073322b5dec/onnxruntime-1.28.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:f5c5daabd28aad610f83fdcf32acec8fb57e6adc6c6a39fe2a3c755db957b410", size = 19130506, upload-time = "2026-07-25T01:22:34.489Z" }, + { url = "https://files.pythonhosted.org/packages/ea/97/b7ce1bc8bb6048b5fe9129f55d6506dc19499068ef2e0a0af1ae3c8aa4e7/onnxruntime-1.28.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8d66f9ceb29909c70839e4e4fb3435c7b490050d8f162bd5f3aba4ca01ee517f", size = 17039880, upload-time = "2026-07-25T01:21:37.538Z" }, + { url = "https://files.pythonhosted.org/packages/f3/17/4e5ecd8764f87573c495d834ce79e61ecca47f7a01d1e444a606e570edcb/onnxruntime-1.28.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a166b78ee04f3a37fa1ef82034b6a3ce96d9684e582d4d30b296de83e9998bb5", size = 19193162, upload-time = "2026-07-25T01:21:59.151Z" }, + { url = "https://files.pythonhosted.org/packages/9f/10/3d946d5d5f2cdcc3c8da36cae63190c516d16349edaffd944bda60ca4c3e/onnxruntime-1.28.0-cp311-cp311-win_amd64.whl", hash = "sha256:0d650aeee29368414367b65529e90afe4bf1bab76254789063b8b2f7ea3013c8", size = 13752539, upload-time = "2026-07-25T01:22:24.524Z" }, + { url = "https://files.pythonhosted.org/packages/8f/74/1c440be7af1e026280b139caa1be5d11bd4dc368011ddbe8f5362b58e12f/onnxruntime-1.28.0-cp311-cp311-win_arm64.whl", hash = "sha256:0faf85fb447a663c9cdadc39bd6b19bdf7bedded6699e45731b9b36c46fd993d", size = 13449940, upload-time = "2026-07-25T01:22:14.97Z" }, + { url = "https://files.pythonhosted.org/packages/98/f8/dcbe7700dca82fa540035abd3c868fe5ad0f86af00b9a3db7c2e27d15c7d/onnxruntime-1.28.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:26ff0fdd06efb6c155bae95387a09db1a2be89c7a03e4d0bffd5a171cc2826da", size = 19141362, upload-time = "2026-07-25T01:22:36.965Z" }, + { url = "https://files.pythonhosted.org/packages/28/5b/1d77e62097fdbe07e2dc827f389b1c4c0c275f6fab0369a8f46d2461af27/onnxruntime-1.28.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e81a23df16e7acb9d51b06d30cc098e49315ef9180f97bc2221d167b4b04d9c", size = 17050628, upload-time = "2026-07-25T01:21:40.481Z" }, + { url = "https://files.pythonhosted.org/packages/95/df/5486ab03e9be288d5268867054c8b04bebcf95bfd12e801c05cc67703dab/onnxruntime-1.28.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0a83bdb70d143cede762b677789bf2a7acca54b3fb82565601d5c30695aa933c", size = 19214257, upload-time = "2026-07-25T01:22:01.695Z" }, + { url = "https://files.pythonhosted.org/packages/3e/3b/986ca67c274932ba9ac5332fb10de56f643dfd433c74e33f8ae8f847cf24/onnxruntime-1.28.0-cp312-cp312-win_amd64.whl", hash = "sha256:c35064f9b3c43c81c5d5d282091401d0f1ff22796d93ccade4ea2ece5e137ab8", size = 13755036, upload-time = "2026-07-25T01:22:26.89Z" }, + { url = "https://files.pythonhosted.org/packages/1d/46/059dba81d46c6ba88e0c2d1c64321ac8098847d678423300a183d42ecbd6/onnxruntime-1.28.0-cp312-cp312-win_arm64.whl", hash = "sha256:e02feeb0165c5f13b4cc954738078d59b90128516ac12b671ee24a530242bf02", size = 13454462, upload-time = "2026-07-25T01:22:17.38Z" }, + { url = "https://files.pythonhosted.org/packages/9c/12/3807e2b17d9eb71d3cb78ed2ba76869b05c637c9b9d6112e636098b0c97a/onnxruntime-1.28.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:31410f544674f534c2f27348af52ef81682ca9c8719154bf4d48f0ef23823b1e", size = 19141759, upload-time = "2026-07-25T01:21:53.765Z" }, + { url = "https://files.pythonhosted.org/packages/c0/23/b46045c3bf67a9cf54c12f5df0f018a422c65fbb9d6072b10071bebfaae2/onnxruntime-1.28.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f649dd6f6452d12a8059888aa489fe519e062e18793dac72b9efa0f9fdb64135", size = 17049339, upload-time = "2026-07-25T01:21:43.005Z" }, + { url = "https://files.pythonhosted.org/packages/78/b6/8c5396e7894e77c5a7d1e026f3acb9dd39c4b5644e412e37a0055eaa3bc5/onnxruntime-1.28.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54fa221d669282bd8f582708ce4c96010a7e9fb0661f9006b37fe2fedafb73fe", size = 19214329, upload-time = "2026-07-25T01:22:04.133Z" }, + { url = "https://files.pythonhosted.org/packages/56/f1/51225c202edba4dfc94e1ea03f3d78f1aaf307da75fd792c0ce1946b2514/onnxruntime-1.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:1a1a19175464665c9b8d50bc916f216cc0b569110045b7bbca8f9f290b186f58", size = 13755033, upload-time = "2026-07-25T01:22:29.302Z" }, + { url = "https://files.pythonhosted.org/packages/f4/db/f59f715edfdd96a051f32b5ef0e680a20a8755d4ecd75f63090e960e347a/onnxruntime-1.28.0-cp313-cp313-win_arm64.whl", hash = "sha256:cfab507abe09d6ffeb817eee07944d452fdc0b00fdcef34cab4db10a45e378c7", size = 13454175, upload-time = "2026-07-25T01:22:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/47/28/810314fa88647af9f4cdaf438a30ad1cfebebb53ded55499232d7a0094e6/onnxruntime-1.28.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac301f53b1930402fc46c368e268acfed02f3207272aaff05070d7e09f96f031", size = 17057307, upload-time = "2026-07-25T01:21:45.492Z" }, + { url = "https://files.pythonhosted.org/packages/3d/cc/9e9f193cc0f29f263a8f09ec08487aed6c96ee856d5fd77da32a425c1949/onnxruntime-1.28.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7f022a1103cae591c75fc4565589a515f2ddd14a6ac8e8a05812dfeda142e28", size = 19222954, upload-time = "2026-07-25T01:22:06.952Z" }, + { url = "https://files.pythonhosted.org/packages/4e/eb/952314c451d9463e5c9aed9978eec76cf32930d407d9ab8700dd0f4ea1ea/onnxruntime-1.28.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:8adff67a3f28257b37cfe945a7e952e4122666aa8c91a0380862e9fd4c2ed19f", size = 19143748, upload-time = "2026-07-25T01:21:56.297Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e9/139180b4dd810329aaa42c238b4e6383c906202d98609ae29d66eb7c32b1/onnxruntime-1.28.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc2565e487b4896fb988d6383577d875d958e071fc5f6c3550bd5d02ae98264b", size = 17051950, upload-time = "2026-07-25T01:21:48.606Z" }, + { url = "https://files.pythonhosted.org/packages/03/88/9432428273356ad3c8aa01f52c1b3e7f53c4c0192748f41ad983872b436b/onnxruntime-1.28.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6afdc83f1317c136e92fc29f5ee9f058de59d87c0b22cee3fdbfbaa0ccc2098a", size = 19214924, upload-time = "2026-07-25T01:22:09.727Z" }, + { url = "https://files.pythonhosted.org/packages/bb/e2/6feb3a43517aaf2b1bf7e46897ba5eb81a29717f7d7901420614d5ee4653/onnxruntime-1.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:f2a3b9e30ce880d4ca54999cb313569e36da4f62eefe25f87be18f43e9a3a4d5", size = 14093738, upload-time = "2026-07-25T01:22:31.629Z" }, + { url = "https://files.pythonhosted.org/packages/fc/8f/83974a1e201dc2e58e5e7111bcaeb1ca2413e9c41f505d26419ee9e3dddf/onnxruntime-1.28.0-cp314-cp314-win_arm64.whl", hash = "sha256:07fb3cbe990d6bf0ab3c22bfbbfb0e314151266046ea6edb4a07f556b4258c5f", size = 13821117, upload-time = "2026-07-25T01:22:22.387Z" }, + { url = "https://files.pythonhosted.org/packages/0d/83/00e606bc25c756d76a267370c39b7516ad52f9cf134d7ff2bff8b6108bc4/onnxruntime-1.28.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e562d6e36a749f6764481c0ddb0f2af3d0b5a3c164291361d08803c557f369af", size = 17055518, upload-time = "2026-07-25T01:21:51.08Z" }, + { url = "https://files.pythonhosted.org/packages/94/a9/68707e1ce345cbdbcd4df65932ebc82a673e917d63eda0007ebcff948691/onnxruntime-1.28.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f6e92367ddce1e4d33cf295024f40192be6c6171a09208f515ba169ced06c8e", size = 19222976, upload-time = "2026-07-25T01:22:12.474Z" }, +] + +[[package]] +name = "optuna" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alembic" }, + { name = "colorlog" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "sqlalchemy" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/aa/05f5e3f662cc96a4c478fc3446b8ed6359825a2b504ecb614a9ac84e4a4d/optuna-4.9.0.tar.gz", hash = "sha256:b322e5cbdf1655fb84c37646c4a7a1f391de1b47806bbe222e015825d0a82b87", size = 485834, upload-time = "2026-06-01T06:23:30.424Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/f3/e5fcd5d9b15771ed6dc10e3a7eeddc672e418f4f4c4653d216cc1d857e2d/optuna-4.9.0-py3-none-any.whl", hash = "sha256:f52f3be6148654850c92a5860d398fd88ec6b2c84ab68d9c3d07dcff02e7afee", size = 425553, upload-time = "2026-06-01T06:23:28.804Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763, upload-time = "2025-09-29T23:16:53.287Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217, upload-time = "2025-09-29T23:17:04.522Z" }, + { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791, upload-time = "2025-09-29T23:17:18.444Z" }, + { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373, upload-time = "2025-09-29T23:17:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" }, + { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, + { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, + { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, + { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, + { 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 = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[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/25/c2/669d88644cddb1485bd9534e63e8cf476c8e51cb3c3a1297677023505c0e/pillow-12.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6c0016e7b354317c4e9e525b937ac8596c38d2d232b419529b9cd7a1cd46e39a", size = 5392418, upload-time = "2026-07-01T11:53:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ba/3762f376a2948e3036488d773a146e0ae6ecc2ca03ac20e2615bd0b2ba02/pillow-12.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bcc33feacfaefce60c12fd500a277533bdc02b10a19f7f6d348763d8140bbba7", size = 4785287, upload-time = "2026-07-01T11:53:29.761Z" }, + { url = "https://files.pythonhosted.org/packages/07/50/b5d688cc9c52d4482f3d5bcab6ce20bc2a74a85d2343841c907444a3be2c/pillow-12.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5594fc43d548a7ed94949d139aa1341b270f1863f11cfd37f5a6c8b778a6b67f", size = 6253754, upload-time = "2026-07-01T11:53:32.298Z" }, + { url = "https://files.pythonhosted.org/packages/4e/89/36f4cd76cf4baf05c50ababb976249153f18c959171c7f6ba09a6f217260/pillow-12.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0606c8bf2cdefea14a43530f7657cbbb7ecf1c4222512492ef4a4434a9501ec", size = 6925605, upload-time = "2026-07-01T11:53:34.487Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c0/4de58cf6633b9e3a6061ef4be6fb91fc3c90b812ece886f531e3c523d777/pillow-12.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:85f998ea1848bc6757289e739cfbdda3a04adfd58b02fc018ce54d754a5ce468", size = 6327788, upload-time = "2026-07-01T11:53:36.433Z" }, + { url = "https://files.pythonhosted.org/packages/87/3c/14d53682a19550dbbaf3b598f807d5457646c510805a44c7d7891cd1cd1a/pillow-12.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:25b9b82bb22e6e2b3cd07b39c68b7b862001226cb3dff7130d1cb914121b39ed", size = 7036288, upload-time = "2026-07-01T11:53:38.712Z" }, + { url = "https://files.pythonhosted.org/packages/38/1d/36279e3c77efe034e4cc2b0393ee74ffdb5a62391dacbf9b916154f5f0b8/pillow-12.3.0-cp310-cp310-win32.whl", hash = "sha256:37dc8f7bbb66efe481bb60defacef820c950c24713fb44962ed6aa2a50966de1", size = 6472396, upload-time = "2026-07-01T11:53:40.781Z" }, + { url = "https://files.pythonhosted.org/packages/48/7c/8fa0039574c476d7c6fa57dd7c32a130436877c6ec1e5ce1cc8ec44878c1/pillow-12.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:300557495eb45ebb8aec96c2da9c4be642fbf7cd937278b4013ba894ea8eb0eb", size = 7226887, upload-time = "2026-07-01T11:53:42.764Z" }, + { url = "https://files.pythonhosted.org/packages/fa/17/e324be141d173c1c919428066c3259f21c1b8982e564e01a4a81e96dbdcf/pillow-12.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:514435a37670e3e5e08f3945b68718b6ed329bb84367777e16f9f4dfe1e61a0f", size = 2568039, upload-time = "2026-07-01T11:53:45.372Z" }, + { 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 = "plotly" +version = "6.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "narwhals" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/07/795c79dbce40c39bece88e69d049babbd23ffa95b5d117f248db8ea03abb/plotly-6.9.0.tar.gz", hash = "sha256:967ad33e8c704fed051800d11d985eb206a9c795c14206b30a6f463ed9c67d0d", size = 6919903, upload-time = "2026-07-09T14:55:59.982Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/18/d8544811ab076f876c4892b3714f5b0dad335e1dc33aef826df431b8325d/plotly-6.9.0-py3-none-any.whl", hash = "sha256:36bebe2f1bb13884774fe61689c329071446f6ce4a8927fb1f0d6fb24f581236", size = 9909646, upload-time = "2026-07-09T14:55:55.421Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, +] + +[[package]] +name = "pyarrow" +version = "25.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/f3/95428098d1fa7d04432fb750eed06b41304c2f6a5d3319985e64db2d9d41/pyarrow-25.0.0.tar.gz", hash = "sha256:d2d697008b5ec06d75952ef260c2e9a8a0f6ccfce24266c04c9c8ade927cb3b4", size = 1199181, upload-time = "2026-07-10T08:29:50.116Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/2a/eaa70e6d6ed430c2e90c0599e2831a41a50251879e44788ccdbc73115af1/pyarrow-25.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ce0ca222802087b9a8cb031a6468442cb6b67c290a45a601cac64753d34954d3", size = 35945551, upload-time = "2026-07-10T08:25:23.153Z" }, + { url = "https://files.pythonhosted.org/packages/df/e0/917086af6b246143012cdc8a7c886b018b53204f3d69fc5f9be5857a8b80/pyarrow-25.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:7d6da02ffc7a3a9bda3b7ded4cc2a27ff73969ab37153f3afd46bbbc1ba4f0f7", size = 37636698, upload-time = "2026-07-10T08:25:28.031Z" }, + { url = "https://files.pythonhosted.org/packages/68/6a/c87829f92503f84993721791c942f3d9aa81044de51a8cfb1da5810e5345/pyarrow-25.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:dbf9fa5d4bde73b1cc16377dcaaa010f971e6fa7f5083f5d44f34b50bc1d74af", size = 46858364, upload-time = "2026-07-10T08:25:34.527Z" }, + { url = "https://files.pythonhosted.org/packages/cc/ba/2030d454c2747e26cce23e4a0338067ee0830a155b7894da04caa96783a5/pyarrow-25.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b72d943ff4e10fec8d48aedb23322d8f6ea8bc2d698b81db37e73730f69e4862", size = 50056398, upload-time = "2026-07-10T08:25:40.785Z" }, + { url = "https://files.pythonhosted.org/packages/78/ce/ba7a5ce7bf0cfc372ec48203a34ece42f73aa2f3231706f61c55e105ecd0/pyarrow-25.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5fb2d837960f1df7f679ff9f1a55065e306347d379e0768cebf14781254d6194", size = 49958146, upload-time = "2026-07-10T08:25:46.98Z" }, + { url = "https://files.pythonhosted.org/packages/75/eb/c34a29fb7a70dca2f903c7d85a928928ef55af20cd56e99de6b4c0d897bc/pyarrow-25.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:add690feafa0953c443cdba9e9e87f5eaa198f1ea2e43a3b146ea83f202262d0", size = 53096264, upload-time = "2026-07-10T08:25:53.925Z" }, + { url = "https://files.pythonhosted.org/packages/36/f9/35b1f83a0727d84951588e4034aca2feb76dfb45b0725918c0037b0a48f7/pyarrow-25.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:d293e9959b29a24c82d936d04ab2b7fd8b8d334030de2e56a99aba94f008ad7a", size = 27840572, upload-time = "2026-07-10T08:25:58.966Z" }, + { url = "https://files.pythonhosted.org/packages/a7/98/ae2b5acf9876dbeffa6f320776242c52caab062df55c8ac5501ed2679e74/pyarrow-25.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:2e3b6544e26e393fe2cd530f523e36c1c8d3c345bbbb60cca3fd866be8322517", size = 35939080, upload-time = "2026-07-10T08:26:04.53Z" }, + { url = "https://files.pythonhosted.org/packages/80/09/3de2a968edbd496c86cb8b932cdbee2d4b08c4a28e9884a15e5c705a646b/pyarrow-25.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:b724d127783b4c19f088fcdfc844cbc318809246a30307bcabd5ed02045e890e", size = 37633420, upload-time = "2026-07-10T08:26:10.354Z" }, + { url = "https://files.pythonhosted.org/packages/19/86/8399243a4ce080426ec37db18d5e29148b7ec960a8a8c7f9059a7bf6ef0a/pyarrow-25.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:244f98a595f70fa4fd35faa7508c4ae67e14a173397a4b3b49d2b3c360fb0062", size = 46861050, upload-time = "2026-07-10T08:26:16.397Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/72d704b02bc5fc6d06954d76a0208c1e79cad3ab370f6d6a91ffe5078870/pyarrow-25.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:0222f0071d13313962a88d21bf28b80d355ac39d81bfa6ff3fe00eeaf748e4be", size = 50056458, upload-time = "2026-07-10T08:26:23.271Z" }, + { url = "https://files.pythonhosted.org/packages/06/5d/3c31a60b6403d63cad2e0f829096f5fc5763a129ead4207a5d4690b96448/pyarrow-25.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b58726f118c079f9d4ed7e904975d4f15fd69d0741ba511a4e2dcaa4ef16354f", size = 49957793, upload-time = "2026-07-10T08:26:30.232Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/8f8a019061f9863a831915329264372a87ed25eaf9109ce56eb0e84012c5/pyarrow-25.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:38a2c887cb3883e241b70201688db34133b6dfadd04f03c8f9213df53770c18e", size = 53100544, upload-time = "2026-07-10T08:26:36.414Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e2/738071e95c5ddad7b3dfc12f569ffa992db89d7d7b4a95258fd184191249/pyarrow-25.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:161649d60a7a46c613a19fd795763ea8a88c36ba997dd99d9bc66e6794ee36e8", size = 27848311, upload-time = "2026-07-10T08:26:41.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/44/fdd3a4377807b7dcabe2d4b5aa99dbbc98e2e5df3f1ca4e7f0aec492d987/pyarrow-25.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:149730a3d1f0fb59d663a0b8aa210adfd9c17c27cd94a0d143e60daea8320d4e", size = 35850884, upload-time = "2026-07-10T08:26:47.357Z" }, + { url = "https://files.pythonhosted.org/packages/bf/71/9f053177a7709b8c90abb00a2375b916286f9f0d6cfb21a5cadd4ef811e8/pyarrow-25.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:0721332c30fdd453fdd1fc203b2ac1f4c9db5aea28fa38d41f2574c4b068b9ec", size = 37616197, upload-time = "2026-07-10T08:26:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/95/1a/22bfb6597dcdc861fa83c39c06e1457cb56f698940eff42fbb25de30e8e5/pyarrow-25.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fa1482b3da10cac2d4db6e26b81da543e237616af2ef6d466018b31ca586496f", size = 46841966, upload-time = "2026-07-10T08:27:07.685Z" }, + { url = "https://files.pythonhosted.org/packages/55/0e/cd705c042bc4fe7022478db577fcab4abdcfabb9bc37ab7a75556b3fcb2b/pyarrow-25.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:5d1dbf24e151042f2fa3c129563f65d66674128868496fb008c4272b16bdf778", size = 50088993, upload-time = "2026-07-10T08:27:14.268Z" }, + { url = "https://files.pythonhosted.org/packages/98/ee/d822e1ee31fe31ec5d057210e0605c950b975dcd8d9a332976cc859a9df8/pyarrow-25.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:20887a762dd61dcc530f93a140840ab1f6aa7836b33270e42d627ab3cf11e537", size = 49941005, upload-time = "2026-07-10T08:27:21.274Z" }, + { url = "https://files.pythonhosted.org/packages/33/1b/207a90cc64619a095eb75a263ae069735f2810056d43c667befd573ec083/pyarrow-25.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:58d1ab556b0cea1c93fdb799b24ad58adb2f2a2788dbce782a94f64ae1a5cc9b", size = 53112355, upload-time = "2026-07-10T08:27:27.911Z" }, + { url = "https://files.pythonhosted.org/packages/7e/fe/81d1e5f8beed15c01e98649d5c6e2167b67fd395884a2488f18bf1cf0dba/pyarrow-25.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:3f356afe61186395c861d5cd63dc21ff7d5fa335012a4668d979257df7fea0f5", size = 27945954, upload-time = "2026-07-10T08:27:32.903Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c8/098ce17d778fd9d29e40bb8c5f19a40cc90c3f0b46c9057b0d7993f42f54/pyarrow-25.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:8831a3ba52fa7cdb78d368d968b1dcd06171e6dff5461e16d90de91d371e47bc", size = 35844549, upload-time = "2026-07-10T08:27:37.956Z" }, + { url = "https://files.pythonhosted.org/packages/bc/66/24c28877219abf6263d909b1592c97ff82c59f13a59acbed11fc87c0654f/pyarrow-25.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:5f4bacb60f91dd2fca6c52f1b9a0012cd090e0294f1f781dc1881a247a352f8e", size = 37610397, upload-time = "2026-07-10T08:27:43.803Z" }, + { url = "https://files.pythonhosted.org/packages/53/55/6d1d5f5aff317ec5de9421594679ed51ed828fe7e2ce209327f819d801e4/pyarrow-25.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:59516c822d5fd8e544aaa0dfe72f36fed5d4c24ea8390aab1bcd31d7e959c6be", size = 46841701, upload-time = "2026-07-10T08:27:49.741Z" }, + { url = "https://files.pythonhosted.org/packages/b5/5d/f790fb6965ab54c9da0dda7856abc75fd0d7648d865f8d603c111d203a64/pyarrow-25.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6f9dbd83e91c239a1f5ee7ce13f108b5f6c0efbe40a4375260d8f08b43ad05e9", size = 50090118, upload-time = "2026-07-10T08:27:56.051Z" }, + { url = "https://files.pythonhosted.org/packages/0c/8c/faf025357ebf31bc96777f234277aa31e2aeca6dd4ecaa391f29085473c2/pyarrow-25.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:18dcc8cc50b5e72eae6fcbfc6c8776c21a007176b27a3cdec5c2f5bcf126708d", size = 49945559, upload-time = "2026-07-10T08:28:01.927Z" }, + { url = "https://files.pythonhosted.org/packages/07/a1/bd051871708ea99a5e0fc711926c26c6f2c6d0130c7aaac8093e34998af6/pyarrow-25.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4ec1895a87aa834c3b99b7a1e758747eb8bb57f922b32c0e0fa04afb8d6998b1", size = 53114238, upload-time = "2026-07-10T08:28:08.594Z" }, + { url = "https://files.pythonhosted.org/packages/7c/31/737f0c3cffcd6af647849477d1dd68045deac2e3963c3f9f211bedc48540/pyarrow-25.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:77c8d1ae46a44b4006e8db1cc977bbcc6ce4873c92f74137d68e45503b97fb18", size = 27861162, upload-time = "2026-07-10T08:28:12.975Z" }, + { url = "https://files.pythonhosted.org/packages/55/c7/581ccbcdb3d897eb2893328d68db3d52eca373bf2a7e964d0a6276b8e85b/pyarrow-25.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:72132b9a8a0a1840197794d4dea26080069b6b0981c116bc078762dc9691b21b", size = 35878945, upload-time = "2026-07-10T08:28:18.222Z" }, + { url = "https://files.pythonhosted.org/packages/64/d1/ccb01db7329ea0411ef4fbd9b62a04d3268b36777d4e758d5e39b91ddeab/pyarrow-25.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e009ef945e498dca2f050ea10d2e9764cb44017254826fc4574fdb8d2530173b", size = 37630854, upload-time = "2026-07-10T08:28:23.452Z" }, + { url = "https://files.pythonhosted.org/packages/af/9f/2d81ba89d1e4198d0cb25fe7529de936830fdaec0db926bb52a1ef7080d4/pyarrow-25.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:f57a39dbcb416345401c2e77a4373669b45fd111a1768e6cf267a7a0607ff0ec", size = 46905617, upload-time = "2026-07-10T08:28:29.376Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/0ed312ec800fb536f93783215126cee4b8977dcfeccba6f0f44df0cc87d7/pyarrow-25.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:447df764beb07c544f0178a5f6b70ef44b9ecf382b3cdfad4c2d7867353c3887", size = 50119765, upload-time = "2026-07-10T08:28:35.826Z" }, + { url = "https://files.pythonhosted.org/packages/ca/88/cab5063ba0c4d46a9f6b4b7eb1c9029dc0302d65cd5ab3510c949a386568/pyarrow-25.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac5dfeee59f9ceb4d45ba76e83b026c38c24334135bb329d8274baa49cec3c62", size = 50027563, upload-time = "2026-07-10T08:28:43.848Z" }, + { url = "https://files.pythonhosted.org/packages/7b/fb/4d24f1b7fe2e042dc4ef315ef75e4e702d8e46fe10c37e63caff00502b03/pyarrow-25.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f0f100dacf2c0f400601664a79d1a907ced4740514bb2b00917341038e2ce76f", size = 53162437, upload-time = "2026-07-10T08:28:52.819Z" }, + { url = "https://files.pythonhosted.org/packages/fa/65/da20806de93ca6ee91e72cb6a9b08b3ac890b46efc8d94a7326c651c4c81/pyarrow-25.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:2e093efbecb5317372f819228fa4b4e6157eee48d3f0a7b0303705ebf81a7104", size = 28613262, upload-time = "2026-07-10T08:29:47.544Z" }, + { url = "https://files.pythonhosted.org/packages/86/9f/c632afb1d3ef4a7814cee236718235f3a47eac46e97eb87df40f550b6b48/pyarrow-25.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:26be35b80780d2d21f4bae3d568b1666337c3a89722cc1794c956a77017cb24e", size = 36120702, upload-time = "2026-07-10T08:28:59.577Z" }, + { url = "https://files.pythonhosted.org/packages/36/0a/093d53a0e72ad06e45d6443e00651bbc2d21af4211295086cbf4d873d3b9/pyarrow-25.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:6f4812bfbf11ca7d8faf59eb8fff8bf4dd25ce3a38b62baa010cc17a0926d1b2", size = 37750674, upload-time = "2026-07-10T08:29:06.916Z" }, + { url = "https://files.pythonhosted.org/packages/8a/18/b37fc31a69cff4bdfb8842683def5612f551b93fff6f44375e4a4a6a5535/pyarrow-25.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b8af8ceedf0c9c160fd2b63440f2d205b9404db85866c1217bfea601de7cfb50", size = 46912304, upload-time = "2026-07-10T08:29:14.656Z" }, + { url = "https://files.pythonhosted.org/packages/32/35/5cae19ba72493e5598022468b56f6a5571f399f485bf412f157356476caa/pyarrow-25.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c70a5fd9a82bd1a702fd482bdc62d38dcb672fb2b449b1d7c0d7d1f4be7b7bfe", size = 50073652, upload-time = "2026-07-10T08:29:22.467Z" }, + { url = "https://files.pythonhosted.org/packages/2e/a5/ddd508424bdfd5e6945765e9e2ffc687e2f6115972badc8ecf423076c407/pyarrow-25.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0490a7f8b38ffe11cc26526b50c65d111cb54ddac3717cec781806793f1244dc", size = 50058654, upload-time = "2026-07-10T08:29:29.689Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a4/324d0db203ff5eebe8694ec2d6ec5a23f9aaa5d02e5b8c692914c518c33c/pyarrow-25.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e83916bbcf380866b4e14255850b33323ff678dc9758411d0409cdd2523880b0", size = 53140153, upload-time = "2026-07-10T08:29:36.041Z" }, + { url = "https://files.pythonhosted.org/packages/bd/8d/d236e9c82fe315f9128885c8be3ec719f41965a1eb6b6f4b42470904cd41/pyarrow-25.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:13240f0d3dc5932ccd0bfa90cd76d835680b9d94a7661c635df4b703d40ce849", size = 28743657, upload-time = "2026-07-10T08:29:42.742Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pyreadline3" +version = "3.5.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/6d/f94028646d7bbe6d9d873c47ee7c246f2d29129d253f0d96cb6fcab70733/pyreadline3-3.5.6.tar.gz", hash = "sha256:61e53218b99656091ddb077df9e71f25850e72e030b6183b39c9b7e6e4f4a9bf", size = 100368, upload-time = "2026-05-14T17:55:04.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/5e/35c856e186b74678c24927847ad9895a51f1bc02a0c6126477a6c6040064/pyreadline3-3.5.6-py3-none-any.whl", hash = "sha256:8449b734232e42a5dcd74048e39b60db2839a4c38cf3ae2bf7707d58b5389c0d", size = 85243, upload-time = "2026-05-14T17:55:03.262Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pytz" +version = "2026.3.post1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/48/fb042503b6ca6cd271261dc559fd6432f7d8c713153e9ec5c591af4dfc1c/pytz-2026.3.post1.tar.gz", hash = "sha256:2211d3fcf9a797d3405cac96ac7f61d80e6a644f72a3309607282fe8a2010c5d", size = 319745, upload-time = "2026-07-25T15:12:07.385Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/7b/39c34ca613b0b198cb866466651b26b045e2009864c5183c979a3b83f383/pytz-2026.3.post1-py2.py3-none-any.whl", hash = "sha256:dd95840dd199baea12d9cc096a1d452caa6596a1c1e4b5f3dbd1541855d5e815", size = 508283, upload-time = "2026-07-25T15:12:05.782Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "ruff" +version = "0.16.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/e1/4508a569211b35599016e84ba65c1a992b7a4004b4b6c4bea02a851cba1b/ruff-0.16.2.tar.gz", hash = "sha256:c3d7828d12e8927a6fc65fe38e2c2541b9e762d360a1786d752cb1b8883b3c9c", size = 4885811, upload-time = "2026-08-07T13:31:01.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/57/db19951540f98859c956b50bdb4d31089b4d91e9f15e2968e7d5193806d5/ruff-0.16.2-py3-none-linux_armv6l.whl", hash = "sha256:3c8de4cf2181f01d57946d87d777aa52916976fc09942aed89938fab5e013318", size = 10847925, upload-time = "2026-08-07T13:30:14.468Z" }, + { url = "https://files.pythonhosted.org/packages/13/5a/995fe85a8470d3e391ac0f7fa8054bb454eaf33ee138196d6172ed1079c0/ruff-0.16.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a48cc05c6fbc811ca81b5d7ba95375affea6582d1b8024e455e41afbbf55344", size = 11072662, upload-time = "2026-08-07T13:30:18.143Z" }, + { url = "https://files.pythonhosted.org/packages/32/53/370d767c61c71a971a4ace36703a7ecd8c393956349a7325d7fab2b56827/ruff-0.16.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a2c0d14fcbb26c91f0f867a6dc9bd71bbc30b1b6151829c884f23faeab2e5700", size = 10566771, upload-time = "2026-08-07T13:30:20.899Z" }, + { url = "https://files.pythonhosted.org/packages/85/d6/9d96948caf5a632be62d62202d5ec914d6856f204fd79eb036e5915e79ea/ruff-0.16.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:335c621622c4650330be50842561c6586ac6971bb8ab5407fe34dcc9efb16bbe", size = 10975825, upload-time = "2026-08-07T13:30:23.517Z" }, + { url = "https://files.pythonhosted.org/packages/3b/92/ea87129b3414acb0b5770563779c51804d37ac67675c7ba35447ddb14773/ruff-0.16.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20e66910f2c37cc753f9ef6580c914a621b80c4fa3549d3e3521e29d0f5bfc3f", size = 10649437, upload-time = "2026-08-07T13:30:26.097Z" }, + { url = "https://files.pythonhosted.org/packages/ac/43/f8f291dcd4af5bb7872b74fdfa41a7cd7c856ca1d4069670971cf1b9f5cb/ruff-0.16.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7e36fbfba65510548156902bcf1350a979a958ce0347ce0f90d73894036b39f", size = 11446761, upload-time = "2026-08-07T13:30:28.752Z" }, + { url = "https://files.pythonhosted.org/packages/71/4a/ef991fb2fcf516ab71f0808adcdd8da5e18c8cde447f4ceaf5f47a5132a5/ruff-0.16.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f0eab35f80df8f134aae5d1630e751901321d317cc8e50dc39e36fa3ed34cd12", size = 12336364, upload-time = "2026-08-07T13:30:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/f3/24/f615e74f307e6ca0e56a482872477b856c70d530aa356abfb6dfe5ca8a80/ruff-0.16.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ea8c0594feb894e89c8c61ab9c103d38b0ea72dfde6c594107147ca31b1140", size = 11630720, upload-time = "2026-08-07T13:30:34.426Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d3/8ef50149e8412a77f7ab409efdef0e2b23803707a3863da4fc64cb23d459/ruff-0.16.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab3d62dde0b19facdd632008cc4827fc28ada7736c6bd35ab6f1050f0bfed53f", size = 11466130, upload-time = "2026-08-07T13:30:36.958Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a7/a19334985c4dea8c381981fa252cd854c7ee52dc4b1686dc16f4a911c702/ruff-0.16.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e43e1f5b8388da9eca1b9e88328d47a5cec794633ccf6f7484ac2dd15eee92c0", size = 11523634, upload-time = "2026-08-07T13:30:39.822Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6c/96d192b0e742412ceda08c0a50f9669b253dde9fd6a60ea1a10c9fa79a63/ruff-0.16.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c24788a980581e1d7ea3a0cbe4344c4fbeb0a6a9b1f4713aa46bb104f8294690", size = 10949807, upload-time = "2026-08-07T13:30:42.745Z" }, + { url = "https://files.pythonhosted.org/packages/fa/51/e26599ceca11e79ee255c7df515995561edf87e9ca1893284e44d98f5a86/ruff-0.16.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:81806b08329130005dd4a8a8394a0c9da8c6f4cafb16ba438d2a2ee6a18bedf1", size = 10646891, upload-time = "2026-08-07T13:30:45.522Z" }, + { url = "https://files.pythonhosted.org/packages/68/01/800c4b1f97bc8d7c6029e06b1f20473a3cf1e13c4933d8f3342add83fc55/ruff-0.16.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4ce4e02bad779bef557f541a1b31f20d6abeae1cc05ed1b1ac019d4ffd1044c8", size = 11162063, upload-time = "2026-08-07T13:30:48.131Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d0/1477ea50fc5a0d4b0b71d1d63d50770bdd794d90b43e37a7618e63ec9894/ruff-0.16.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e0422abdf70070255fc4073ce9dfc814cc03db577013761ddd09bc1e4a9a4fbd", size = 11556038, upload-time = "2026-08-07T13:30:50.686Z" }, + { url = "https://files.pythonhosted.org/packages/b8/76/a7776f32048d991e16d4fa8ff91790b877342d3596cc3ed04acdbf1aaedc/ruff-0.16.2-py3-none-win32.whl", hash = "sha256:bf3a63d78fb39f4bf5ac8ae52051c5520505301abe19ba4e204c453b3f09bb0b", size = 10872850, upload-time = "2026-08-07T13:30:53.471Z" }, + { url = "https://files.pythonhosted.org/packages/00/0d/929c800d920e61397d82a01b60bffc68da3052c17d31de59efaad2e4ed75/ruff-0.16.2-py3-none-win_amd64.whl", hash = "sha256:bcabe2f6d0fc7819f1431793005af4e4de7371927d037345bf941252b195b9fa", size = 12023338, upload-time = "2026-08-07T13:30:56.193Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6c/93e26c22c5f78ff87363e07da49c84955affbeb1098bd1936bf3b3f293bf/ruff-0.16.2-py3-none-win_arm64.whl", hash = "sha256:d614e95cedf38a2053fd351c55b103ba30d017d61688fdbfd40ee0412852a99f", size = 11374065, upload-time = "2026-08-07T13:30:58.775Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "joblib", marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "threadpoolctl", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/3e/daed796fd69cce768b8788401cc464ea90b306fb196ae1ffed0b98182859/scikit_learn-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b33579c10a3081d076ab403df4a4190da4f4432d443521674637677dc91e61f", size = 9336221, upload-time = "2025-09-09T08:20:19.328Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ce/af9d99533b24c55ff4e18d9b7b4d9919bbc6cd8f22fe7a7be01519a347d5/scikit_learn-1.7.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:36749fb62b3d961b1ce4fedf08fa57a1986cd409eff2d783bca5d4b9b5fce51c", size = 8653834, upload-time = "2025-09-09T08:20:22.073Z" }, + { url = "https://files.pythonhosted.org/packages/58/0e/8c2a03d518fb6bd0b6b0d4b114c63d5f1db01ff0f9925d8eb10960d01c01/scikit_learn-1.7.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7a58814265dfc52b3295b1900cfb5701589d30a8bb026c7540f1e9d3499d5ec8", size = 9660938, upload-time = "2025-09-09T08:20:24.327Z" }, + { url = "https://files.pythonhosted.org/packages/2b/75/4311605069b5d220e7cf5adabb38535bd96f0079313cdbb04b291479b22a/scikit_learn-1.7.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a847fea807e278f821a0406ca01e387f97653e284ecbd9750e3ee7c90347f18", size = 9477818, upload-time = "2025-09-09T08:20:26.845Z" }, + { url = "https://files.pythonhosted.org/packages/7f/9b/87961813c34adbca21a6b3f6b2bea344c43b30217a6d24cc437c6147f3e8/scikit_learn-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:ca250e6836d10e6f402436d6463d6c0e4d8e0234cfb6a9a47835bd392b852ce5", size = 8886969, upload-time = "2025-09-09T08:20:29.329Z" }, + { url = "https://files.pythonhosted.org/packages/43/83/564e141eef908a5863a54da8ca342a137f45a0bfb71d1d79704c9894c9d1/scikit_learn-1.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7509693451651cd7361d30ce4e86a1347493554f172b1c72a39300fa2aea79e", size = 9331967, upload-time = "2025-09-09T08:20:32.421Z" }, + { url = "https://files.pythonhosted.org/packages/18/d6/ba863a4171ac9d7314c4d3fc251f015704a2caeee41ced89f321c049ed83/scikit_learn-1.7.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:0486c8f827c2e7b64837c731c8feff72c0bd2b998067a8a9cbc10643c31f0fe1", size = 8648645, upload-time = "2025-09-09T08:20:34.436Z" }, + { url = "https://files.pythonhosted.org/packages/ef/0e/97dbca66347b8cf0ea8b529e6bb9367e337ba2e8be0ef5c1a545232abfde/scikit_learn-1.7.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89877e19a80c7b11a2891a27c21c4894fb18e2c2e077815bcade10d34287b20d", size = 9715424, upload-time = "2025-09-09T08:20:36.776Z" }, + { url = "https://files.pythonhosted.org/packages/f7/32/1f3b22e3207e1d2c883a7e09abb956362e7d1bd2f14458c7de258a26ac15/scikit_learn-1.7.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8da8bf89d4d79aaec192d2bda62f9b56ae4e5b4ef93b6a56b5de4977e375c1f1", size = 9509234, upload-time = "2025-09-09T08:20:38.957Z" }, + { url = "https://files.pythonhosted.org/packages/9f/71/34ddbd21f1da67c7a768146968b4d0220ee6831e4bcbad3e03dd3eae88b6/scikit_learn-1.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:9b7ed8d58725030568523e937c43e56bc01cadb478fc43c042a9aca1dacb3ba1", size = 8894244, upload-time = "2025-09-09T08:20:41.166Z" }, + { url = "https://files.pythonhosted.org/packages/a7/aa/3996e2196075689afb9fce0410ebdb4a09099d7964d061d7213700204409/scikit_learn-1.7.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8d91a97fa2b706943822398ab943cde71858a50245e31bc71dba62aab1d60a96", size = 9259818, upload-time = "2025-09-09T08:20:43.19Z" }, + { url = "https://files.pythonhosted.org/packages/43/5d/779320063e88af9c4a7c2cf463ff11c21ac9c8bd730c4a294b0000b666c9/scikit_learn-1.7.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:acbc0f5fd2edd3432a22c69bed78e837c70cf896cd7993d71d51ba6708507476", size = 8636997, upload-time = "2025-09-09T08:20:45.468Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d0/0c577d9325b05594fdd33aa970bf53fb673f051a45496842caee13cfd7fe/scikit_learn-1.7.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e5bf3d930aee75a65478df91ac1225ff89cd28e9ac7bd1196853a9229b6adb0b", size = 9478381, upload-time = "2025-09-09T08:20:47.982Z" }, + { url = "https://files.pythonhosted.org/packages/82/70/8bf44b933837ba8494ca0fc9a9ab60f1c13b062ad0197f60a56e2fc4c43e/scikit_learn-1.7.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d6e9deed1a47aca9fe2f267ab8e8fe82ee20b4526b2c0cd9e135cea10feb44", size = 9300296, upload-time = "2025-09-09T08:20:50.366Z" }, + { url = "https://files.pythonhosted.org/packages/c6/99/ed35197a158f1fdc2fe7c3680e9c70d0128f662e1fee4ed495f4b5e13db0/scikit_learn-1.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:6088aa475f0785e01bcf8529f55280a3d7d298679f50c0bb70a2364a82d0b290", size = 8731256, upload-time = "2025-09-09T08:20:52.627Z" }, + { url = "https://files.pythonhosted.org/packages/ae/93/a3038cb0293037fd335f77f31fe053b89c72f17b1c8908c576c29d953e84/scikit_learn-1.7.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b7dacaa05e5d76759fb071558a8b5130f4845166d88654a0f9bdf3eb57851b7", size = 9212382, upload-time = "2025-09-09T08:20:54.731Z" }, + { url = "https://files.pythonhosted.org/packages/40/dd/9a88879b0c1104259136146e4742026b52df8540c39fec21a6383f8292c7/scikit_learn-1.7.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:abebbd61ad9e1deed54cca45caea8ad5f79e1b93173dece40bb8e0c658dbe6fe", size = 8592042, upload-time = "2025-09-09T08:20:57.313Z" }, + { url = "https://files.pythonhosted.org/packages/46/af/c5e286471b7d10871b811b72ae794ac5fe2989c0a2df07f0ec723030f5f5/scikit_learn-1.7.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:502c18e39849c0ea1a5d681af1dbcf15f6cce601aebb657aabbfe84133c1907f", size = 9434180, upload-time = "2025-09-09T08:20:59.671Z" }, + { url = "https://files.pythonhosted.org/packages/f1/fd/df59faa53312d585023b2da27e866524ffb8faf87a68516c23896c718320/scikit_learn-1.7.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a4c328a71785382fe3fe676a9ecf2c86189249beff90bf85e22bdb7efaf9ae0", size = 9283660, upload-time = "2025-09-09T08:21:01.71Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c7/03000262759d7b6f38c836ff9d512f438a70d8a8ddae68ee80de72dcfb63/scikit_learn-1.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:63a9afd6f7b229aad94618c01c252ce9e6fa97918c5ca19c9a17a087d819440c", size = 8702057, upload-time = "2025-09-09T08:21:04.234Z" }, + { url = "https://files.pythonhosted.org/packages/55/87/ef5eb1f267084532c8e4aef98a28b6ffe7425acbfd64b5e2f2e066bc29b3/scikit_learn-1.7.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9acb6c5e867447b4e1390930e3944a005e2cb115922e693c08a323421a6966e8", size = 9558731, upload-time = "2025-09-09T08:21:06.381Z" }, + { url = "https://files.pythonhosted.org/packages/93/f8/6c1e3fc14b10118068d7938878a9f3f4e6d7b74a8ddb1e5bed65159ccda8/scikit_learn-1.7.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:2a41e2a0ef45063e654152ec9d8bcfc39f7afce35b08902bfe290c2498a67a6a", size = 9038852, upload-time = "2025-09-09T08:21:08.628Z" }, + { url = "https://files.pythonhosted.org/packages/83/87/066cafc896ee540c34becf95d30375fe5cbe93c3b75a0ee9aa852cd60021/scikit_learn-1.7.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98335fb98509b73385b3ab2bd0639b1f610541d3988ee675c670371d6a87aa7c", size = 9527094, upload-time = "2025-09-09T08:21:11.486Z" }, + { url = "https://files.pythonhosted.org/packages/9c/2b/4903e1ccafa1f6453b1ab78413938c8800633988c838aa0be386cbb33072/scikit_learn-1.7.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191e5550980d45449126e23ed1d5e9e24b2c68329ee1f691a3987476e115e09c", size = 9367436, upload-time = "2025-09-09T08:21:13.602Z" }, + { url = "https://files.pythonhosted.org/packages/b5/aa/8444be3cfb10451617ff9d177b3c190288f4563e6c50ff02728be67ad094/scikit_learn-1.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:57dc4deb1d3762c75d685507fbd0bc17160144b2f2ba4ccea5dc285ab0d0e973", size = 9275749, upload-time = "2025-09-09T08:21:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/dee5acf66837852e8e68df6d8d3a6cb22d3df997b733b032f513d95205b7/scikit_learn-1.7.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fa8f63940e29c82d1e67a45d5297bdebbcb585f5a5a50c4914cc2e852ab77f33", size = 9208906, upload-time = "2025-09-09T08:21:18.557Z" }, + { url = "https://files.pythonhosted.org/packages/3c/30/9029e54e17b87cb7d50d51a5926429c683d5b4c1732f0507a6c3bed9bf65/scikit_learn-1.7.2-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:f95dc55b7902b91331fa4e5845dd5bde0580c9cd9612b1b2791b7e80c3d32615", size = 8627836, upload-time = "2025-09-09T08:21:20.695Z" }, + { url = "https://files.pythonhosted.org/packages/60/18/4a52c635c71b536879f4b971c2cedf32c35ee78f48367885ed8025d1f7ee/scikit_learn-1.7.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9656e4a53e54578ad10a434dc1f993330568cfee176dff07112b8785fb413106", size = 9426236, upload-time = "2025-09-09T08:21:22.645Z" }, + { url = "https://files.pythonhosted.org/packages/99/7e/290362f6ab582128c53445458a5befd471ed1ea37953d5bcf80604619250/scikit_learn-1.7.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96dc05a854add0e50d3f47a1ef21a10a595016da5b007c7d9cd9d0bffd1fcc61", size = 9312593, upload-time = "2025-09-09T08:21:24.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/87/24f541b6d62b1794939ae6422f8023703bbf6900378b2b34e0b4384dfefd/scikit_learn-1.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:bb24510ed3f9f61476181e4db51ce801e2ba37541def12dc9333b946fc7a9cf8", size = 8820007, upload-time = "2025-09-09T08:21:26.713Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version >= '3.13' and python_full_version < '3.15'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "joblib", marker = "python_full_version >= '3.11'" }, + { name = "narwhals", marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "threadpoolctl", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/6f/37092bdb25f712817231799fc5674d8e704066a8a70c1d2d40517e18b4ab/scikit_learn-1.9.0.tar.gz", hash = "sha256:8833266989d3a5110178a9fae30783675460724d0e1efb13b14901d2c660c557", size = 7750767, upload-time = "2026-06-02T11:54:32.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/be/e844fd9586e66540a15b71924d17a6cbc1bb749e81ddd0a796bcdba4c055/scikit_learn-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9db6f4d34e68c8899e4cab27fdf8eafe6ed21f2ba52ceb25ea250cd237f8e47b", size = 8789686, upload-time = "2026-06-02T11:53:05.439Z" }, + { url = "https://files.pythonhosted.org/packages/42/e2/ff880f62677a17d035817d543cb0fc8727d01eccbee81c5f7fc733a9d856/scikit_learn-1.9.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f401448645a3e7bc115aa3c094097865155b34bff1cba8101857d9104e99074c", size = 8256782, upload-time = "2026-06-02T11:53:08.904Z" }, + { url = "https://files.pythonhosted.org/packages/25/64/eb40435e1a508ab1b4e284ce43ae80f6a162e5be5e38ed5a6fab467a9ea4/scikit_learn-1.9.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd3a8ef0c758555a3b23c03adaa858af32f7736785ded50ad5991f59c4ed03fa", size = 8992419, upload-time = "2026-06-02T11:53:11.551Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/4810a28e473185429e45a57eebcc91fc991b33d889cc0676063e671db03d/scikit_learn-1.9.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7e254636164090da847715a27f8e5478feb98c40a9e0ee90cbd277de9e5ceb8", size = 9281411, upload-time = "2026-06-02T11:53:15.063Z" }, + { url = "https://files.pythonhosted.org/packages/3b/67/be3d369f40d8178ba3bd86635d132e08cb5329b023e4669d9426d84bc007/scikit_learn-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:5dc1818c77575d149e25fce9ef82dd7b7263ae372f03494158668ad632a69759", size = 8272736, upload-time = "2026-06-02T11:53:18.108Z" }, + { url = "https://files.pythonhosted.org/packages/37/79/a733f02dc2118da7e77a134b34f39f40201a353311b011d20859d2db3556/scikit_learn-1.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:366652351f092b219c248f1e72821e841960a63d8f358f1dcfd54dc1cbdbbc28", size = 7919564, upload-time = "2026-06-02T11:53:21.2Z" }, + { url = "https://files.pythonhosted.org/packages/ac/20/75f915ff375d6249e6550ac740fdbbd66159a068fd3af1400ff62036b07a/scikit_learn-1.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2bd41b0d201bc81575531b96b713d3eb5e5f50fb0b82101ff0f92294fdc236ac", size = 8741122, upload-time = "2026-06-02T11:53:24.08Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d5/2b5148f2279196775e1db2aeb85d14b70ac80e7e32b3b28e7ebeafb0901d/scikit_learn-1.9.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5be45aa4a42a68a533913a6ed736cf309de2226411c79ef8d609a5456f1939b1", size = 8261512, upload-time = "2026-06-02T11:53:27.183Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ee/5adbc77656b71f9456a2f5a7a9fdb4bcf9207a6b962889f1c2f9323afa4e/scikit_learn-1.9.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e50ed4da51974e86e940690e9a3d82e729b62b5a49f7c9bac534d515d39d86f", size = 8837603, upload-time = "2026-06-02T11:53:30.328Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c2/63fdda36c56437eeb44aaf9493c8bcd62ce230ab1598924fc626ffbfa943/scikit_learn-1.9.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:056c92bb67ad4c28463c2f2653d9701449201e7e7a9e94e321be0f71c4fef2b8", size = 9132097, upload-time = "2026-06-02T11:53:33.456Z" }, + { url = "https://files.pythonhosted.org/packages/83/a4/c8e67227c680e2259c8864ae72ff48b06e16a6f51253a22167aa02a8aa4e/scikit_learn-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:4306775fad04cc4b472a1b15af1ae9cede1540fbfcc17fbce3767cd8dc7ae283", size = 8211173, upload-time = "2026-06-02T11:53:36.602Z" }, + { url = "https://files.pythonhosted.org/packages/cf/fd/3c0863792e98e67e9184aa4029288a175935eb65443afcd30d4f143450cf/scikit_learn-1.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:26e22435f63bcdcf396b574273f29f13dd531f5ea035801f5be10ba1540a4e60", size = 7867451, upload-time = "2026-06-02T11:53:39.075Z" }, + { url = "https://files.pythonhosted.org/packages/3c/01/cf3310626b6d48d3e9be69a1223f9180360b5e6edb045f50fade723ce494/scikit_learn-1.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:80746d63bd4b6eaca54d36fe5feaf4d28bb38dc6f9470f81c7cad7c40155f119", size = 8705188, upload-time = "2026-06-02T11:53:41.964Z" }, + { url = "https://files.pythonhosted.org/packages/3e/04/5acd7ae280c5f93b6ac5ef6cdec14eef4c8d1cd91d85b3292989c94d96b1/scikit_learn-1.9.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5b934c45c252844a91d69fda3a34cff5e7307e1db10d77cb10a3980312c74713", size = 8228299, upload-time = "2026-06-02T11:53:44.817Z" }, + { url = "https://files.pythonhosted.org/packages/0c/39/ffe829a5b8ecb40a518724a997794657fdc354ada5e8fe8e64d998c0bac9/scikit_learn-1.9.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:38c3dcb9a1ffb85505ec53d54c7b4aea0cff70050425a7760c2af661ac85df05", size = 8789690, upload-time = "2026-06-02T11:53:47.461Z" }, + { url = "https://files.pythonhosted.org/packages/1f/88/8dab5de10c638c083772a6be83a3d8106ced492f74a928c8693638e5bb50/scikit_learn-1.9.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da76d09304a4706db7cc1e3ebaa3b6b98a67365cc11d2996c4f1e58ba47df714", size = 9087723, upload-time = "2026-06-02T11:53:50.702Z" }, + { url = "https://files.pythonhosted.org/packages/20/3f/7917ca72464038f6240ec70c29f94862d08a34a74291ae4d4ec5eb8186a0/scikit_learn-1.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:5808d98f15c6bf6d9d96d2348c1997392a5888ce7097e664105f930c4bca1277", size = 8184330, upload-time = "2026-06-02T11:53:53.396Z" }, + { url = "https://files.pythonhosted.org/packages/78/c7/15739eb2f61fda3c54639e9942414e5a19ad8a8d1f5a3266afad7cb7df80/scikit_learn-1.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:d77f54c017633791bc0225a43e2f8d03745fdcfe4880268fcc4df15f505dec2e", size = 7840653, upload-time = "2026-06-02T11:53:56.035Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/c9a35cf59b20a86fec24d306f1547b78dec194b08d367ce2a3e4854169d9/scikit_learn-1.9.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9656acd4e93f74e0b66c8a36c88830a99252dfa900044d36bc2212ae89a47162", size = 8713289, upload-time = "2026-06-02T11:53:58.788Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a7/552a7821597c632b907f7bfe8f36f9f572777af8ef8a48353041cf8e091a/scikit_learn-1.9.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:24360002ae845e7866522b0a5bbf690802e7bc388cac8663502e78aa98598aa2", size = 8245141, upload-time = "2026-06-02T11:54:01.694Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/f4a0c4fe9711154cddabf913471153af79056382ddc612cfe5ee0ff4b72e/scikit_learn-1.9.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5162ad10a418c8a282dde04c9aa06965de3e9a65f33c1440c0ae69bb1a09d913", size = 8847671, upload-time = "2026-06-02T11:54:04.448Z" }, + { url = "https://files.pythonhosted.org/packages/f0/af/4d72d9e475ac83719160c662619e4bf7b95c19507cd582e7d0167a3c3dae/scikit_learn-1.9.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fea2cc5677ab49d6f5bade978c866da44957b712d92e9635e8b4f723013c3cb", size = 9118104, upload-time = "2026-06-02T11:54:07.205Z" }, + { url = "https://files.pythonhosted.org/packages/a2/d5/6a58eea2cb9abbb9b3f2bb8b2cfb3243d1152d69f442d256c7af71304769/scikit_learn-1.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:64fa347efc1c839c487433e40c5144d38c336e8a2b59c81aa8660373945c2673", size = 8290674, upload-time = "2026-06-02T11:54:10.087Z" }, + { url = "https://files.pythonhosted.org/packages/65/5b/d4c879cf358f1187141cf90ced473f087183489090244f50c124a2ee478b/scikit_learn-1.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:1b944b6db288f6b926e3650026ddafb988929de95d11fc2cc5fa117773c9ba42", size = 7978807, upload-time = "2026-06-02T11:54:12.769Z" }, + { url = "https://files.pythonhosted.org/packages/8a/43/bfae3121ec67ae09150d453c442c7c1cc166e9aefe056e6ab3b7728a5cfc/scikit_learn-1.9.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4ccacf04ca5f4b492158a5f28afe0ace43f81b2571e4b9a66d34848b46128949", size = 9031941, upload-time = "2026-06-02T11:54:15.436Z" }, + { url = "https://files.pythonhosted.org/packages/75/b0/20a4546eb17f3b25d3c66df15810411c14ed5065bcfab50b53c96fb627b2/scikit_learn-1.9.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:ee1a8db2c18c08e34c7412d4b10be1cac214cd4ea7dc9715a6a327eb49a37c96", size = 8613528, upload-time = "2026-06-02T11:54:18.842Z" }, + { url = "https://files.pythonhosted.org/packages/18/3c/e440e039bb82cd19004edaaad00acbde0fb9b461083c3ecf37941c557312/scikit_learn-1.9.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:147e9329ef0e39f75d4cffa02b2aa48d827832684926cd5210d9a2cb5c57246b", size = 8855050, upload-time = "2026-06-02T11:54:21.699Z" }, + { url = "https://files.pythonhosted.org/packages/43/26/b341b8dab5998da6270a3a42c2152c578501354d36f944b5856757035ef8/scikit_learn-1.9.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bad8f8b9950321b54c965fdcbac6c6c55e79e16646b49977bcf3668d3870a1a", size = 9097190, upload-time = "2026-06-02T11:54:24.454Z" }, + { url = "https://files.pythonhosted.org/packages/fb/de/b650b4d69b84468cfa2e28a3ff7b8103743029e6446ce1a97fe060ef688c/scikit_learn-1.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:78fc56eafd4edb9575d2d8950d1dd152061abb573341a1cb7e099fc40f6c6666", size = 8963204, upload-time = "2026-06-02T11:54:27.428Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f3/ff83d76d7418112e5a61326443cdda87be3545dd8d6599c95b2481a4419e/scikit_learn-1.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:051075bda8b7aab87b1906ab3d4740a1e1224a19d7b3781a576736edc94e76aa", size = 8222661, upload-time = "2026-06-02T11:54:30.192Z" }, +] + +[[package]] +name = "scipy" +version = "1.15.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" }, + { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" }, + { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, + { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, + { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, + { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, + { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, + { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, + { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, + { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" }, + { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" }, + { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" }, + { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" }, + { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" }, + { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" }, + { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" }, + { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" }, + { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" }, + { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" }, + { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, +] +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 = [ + { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" }, + { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" }, + { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" }, + { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" }, + { url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" }, + { url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +] + +[[package]] +name = "scipy" +version = "1.18.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version >= '3.13' and python_full_version < '3.15'", + "python_full_version == '3.12.*'", +] +dependencies = [ + { 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/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/19/ca10ead60b0acc80b2b833c2c4a4f2ff753d0f58b811f70d911c7e94a25c/scipy-1.18.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7bd21faaf5a1a3b2eff922d02db5f191b99a6518db9078a8fb23169f6d22259a", size = 31056519, upload-time = "2026-06-19T14:59:45.203Z" }, + { url = "https://files.pythonhosted.org/packages/96/72/1e6442a00cd2924d361aa1b642ab6373ec35c6fabf311a760be9f76e0f13/scipy-1.18.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:265915e79107de9f946b855e50d7470d5893ec3f54b342e1aa6201cbdcd8bb6b", size = 28681889, upload-time = "2026-06-19T14:59:48.103Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2d/11dd93d21e147a73ba22bd75c0b9208d3a2e0ec76d53170ce7d9029b1015/scipy-1.18.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9ab7b758be6940954a713ee466e2043e9f6e2ed965c1fce5c91039f4be3d90a9", size = 20423580, upload-time = "2026-06-19T14:59:50.665Z" }, + { url = "https://files.pythonhosted.org/packages/9c/01/93552f75e0d2a7dd115a45e59209c51e8d514daff02fc887d2623be06fe1/scipy-1.18.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:97b6cddaaee0a779ef6b5ca83c9604b27cc16b2b8fc22c142652df8793319fb8", size = 23054441, upload-time = "2026-06-19T14:59:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/3c/23/21f5e703643d66f21faa6b4c73195bfcad70c55efcb4f1ab327cd7c4101a/scipy-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:52a96e21517c7292375c0e27dd796a811f03fcea5fd4d108fdfea8145dcf17ab", size = 33968720, upload-time = "2026-06-19T14:59:56.415Z" }, + { url = "https://files.pythonhosted.org/packages/dd/aa/1b939f6c67ed68635bb538e6752d3dacc02f66535182e939a89581a44e9c/scipy-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f55797419e16e7f30cf88ffb3113ce0467f00cfe3f70d5c281730b21769bfc2", size = 35287115, upload-time = "2026-06-19T14:59:59.411Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ff/eec46be7e9234208f801062b53e1983085eddebd693f6c9bfb03b459830d/scipy-1.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ad033410e2e0672ffdc1042110cef20e1c46f8fd0616cee1d44d8d58fad8fc11", size = 35577989, upload-time = "2026-06-19T15:00:02.235Z" }, + { url = "https://files.pythonhosted.org/packages/84/ca/210d4759c7210bb7d269437421959b39a33434e2776b60c5cb8a763bb30a/scipy-1.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a55985d54c769c872e64b7f4c8a81cc30ef700cc04296abbbf3705439c126de", size = 37421717, upload-time = "2026-06-19T15:00:05.102Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/9a9edb45345bd6744da5ddfb6628e5d5185920494c6a67ec45b6381004cb/scipy-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:71ccc8faa2dd16ac310233203474a8b5cb67f10dedd54a3116d34943f4b19132", size = 36597428, upload-time = "2026-06-19T15:00:08.112Z" }, + { url = "https://files.pythonhosted.org/packages/99/0e/33f32a2a58987e26aec0f7df252cbbad1e90ae77bdbc76f40dd4ed0cf0ea/scipy-1.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:d88363fd9d8fbd3511bd273f1a49efb2a540773ddf92a91d57498ce7dd7f3e76", size = 24351481, upload-time = "2026-06-19T15:00:11.103Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/9c0136c2de7ae0779b7b366447766cec6d9f0702c56bb8ffeb04c8fd3af4/scipy-1.18.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:09143f676d157d9f546d663504ef9c1becb819824f1afc018814176411942446", size = 31036107, upload-time = "2026-06-19T15:00:14.03Z" }, + { url = "https://files.pythonhosted.org/packages/02/73/0291a64843270f4efb86cdcf2ee0f2048631b65ec6b405398b2b4dbf11bf/scipy-1.18.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5efe260f69417b97ddae455bfb5a95e8359f7f66ad7fa9522a60feb66f169520", size = 28663303, upload-time = "2026-06-19T15:00:16.819Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0f/10ffa0b697a572f4e0d48b92a88895d366422f019f723e7e14a84c050dac/scipy-1.18.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:68363b7eaacd8b5dd426df56d782cc156468ac79a127a1b87ca597d6e2e82197", size = 20404960, upload-time = "2026-06-19T15:00:19.635Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d2/e896cea21ba8edd6c81d4c55b1ffcc717e79698dcbebf9641b4cfb4c6622/scipy-1.18.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:c5557d8be5da8e41353fcd4d21491fdbab83b062fc579e94dc09a7c8ab4f669b", size = 23034074, upload-time = "2026-06-19T15:00:22.107Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b2/e83ea34279a52c03374477c74006256ec78df65fc877baa4617d6de1d202/scipy-1.18.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d13bca67c096d89fb95ced0d8921807300fce0275643aef9533cc63a0773468", size = 33942038, upload-time = "2026-06-19T15:00:24.964Z" }, + { url = "https://files.pythonhosted.org/packages/f6/af/e8fe5fb136f51e2b01678b92cb4106d10d8cd68ec147ead2e7cb0ac75398/scipy-1.18.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a46f9273dbd0eb1cefba61c9b8648b4dfe3cbc14a080176f9a73e44b8336dc7f", size = 35266390, upload-time = "2026-06-19T15:00:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/3a/49/2c5cbb907b56695fc67517811d1db234dfd83381a84814ec220aded2794d/scipy-1.18.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5aba46108853ddfc77906b6557aac839d2b52e900c1d72a1180adaaab58d265f", size = 35551324, upload-time = "2026-06-19T15:00:31.014Z" }, + { url = "https://files.pythonhosted.org/packages/bb/73/eda39f7a2d306ff0ffc574afd13c0bbb6d10a603d9a413998ee269487a80/scipy-1.18.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b6f758e35f12757b5d95c00bc6de2438e229c2664b7a92e96f205959d9f2dfa4", size = 37404785, upload-time = "2026-06-19T15:00:34.072Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d2/ae881ee28d014f38e0ccbfd974a06a919ba9af34f1f74bf42b5301891d63/scipy-1.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:1afac4a847207c7ff8efd321734a50b06d0280b3b2a2c0fc2f413101747ad7c7", size = 36554943, upload-time = "2026-06-19T15:00:36.903Z" }, + { url = "https://files.pythonhosted.org/packages/70/3a/21154e2d54eb3639c6bf4dbae2e531c68356bfe95990daa30df33b30d556/scipy-1.18.0-cp313-cp313-win_arm64.whl", hash = "sha256:c5dbddf60e58c2312316d097271a8e73d40eaf2eabfa4d95ed7d3695bbf2ce7b", size = 24350911, upload-time = "2026-06-19T15:00:40.062Z" }, + { url = "https://files.pythonhosted.org/packages/78/b5/915a19b3de2f7430062b509653563db1633ddbb6f021b06731521115d4e2/scipy-1.18.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4c256ee70c0d1a8a2ace807e199ccd4e3f57037433842abb3fb36bc17eaa9578", size = 31036253, upload-time = "2026-06-19T15:00:43.216Z" }, + { url = "https://files.pythonhosted.org/packages/d7/88/b72def7262e150d16be13fca37a96481138d624e700340bc3362a7588929/scipy-1.18.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:2ef3abc54a4ffc53765374b0d5728532dfdd2585ed23f6b11c206a1f0b1b9af8", size = 28673758, upload-time = "2026-06-19T15:00:46.663Z" }, + { url = "https://files.pythonhosted.org/packages/91/02/2e636a61a525632c373cf6a9c24442a3ffb79e364d38e98b32042964ac32/scipy-1.18.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2a6af57bd9e4a75d70e4117e78a1bbee84f79ae3fbb6d0111005d6ebcc4cb8d", size = 20415514, upload-time = "2026-06-19T15:00:49.399Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b6/2135974442f6aba159d9d39d774a1c8cb19947016725d69fecc685df45bf/scipy-1.18.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:3f1ac564d3bf6c03d861d2cd87a1bea0da2887136f7fb1bf519c05a8971452d6", size = 23034398, upload-time = "2026-06-19T15:00:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e6/ba89ec5abf6ee9257c0d1ec985573f3ae32742c24bc03e016388a40b1b15/scipy-1.18.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40395a5fcd1abee49a5c7aaa98c29db393eedc835138560a588c47ec16156690", size = 33998032, upload-time = "2026-06-19T15:00:54.838Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c4/bc41eb19b0fd0db868f4132920879019318d80cc522ad8f2bca4611af808/scipy-1.18.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ca01e8ae69f1b18e9a58d91afead31be3cef0dd905a10249dac559ee15460a0", size = 35283333, upload-time = "2026-06-19T15:00:58.152Z" }, + { url = "https://files.pythonhosted.org/packages/53/a4/cbdeef6eb3830a8462a9d4ada814de5fc984345cc9ecf17cbec51a036f1e/scipy-1.18.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7a7f3b01647384dbc3a711e8c6778e0aabbe93959249fef5c7393396bcac0867", size = 35610216, upload-time = "2026-06-19T15:01:01.155Z" }, + { url = "https://files.pythonhosted.org/packages/80/4d/b2b82502b65f661d1b789c1665dcdf315d5f12194e06fc0b37946294ebae/scipy-1.18.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6aa94e78ec192a30063a5e72e561c28af769dc311190b24fe91774eff1969709", size = 37418960, upload-time = "2026-06-19T15:01:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/902d836831474b0ab5a37d16404f7bc5fafd9efba632890e271ba952635f/scipy-1.18.0-cp314-cp314-win_amd64.whl", hash = "sha256:2d8bbdc6c817f5b4006a54d799d4f5bab6f910193cbb9a1ff310833d4d270f61", size = 37288845, upload-time = "2026-06-19T15:01:07.822Z" }, + { url = "https://files.pythonhosted.org/packages/b6/43/8d73b337a3bdb14daa0314f0434210747c02d79d729ce1777574a817dcf6/scipy-1.18.0-cp314-cp314-win_arm64.whl", hash = "sha256:18e9575f1569b2c54174e6159d32942e03731177f63dce7975f0a0c88d102f5b", size = 24988971, upload-time = "2026-06-19T15:01:11.076Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b4/f11918b0508a2787031a0499a03fbe3546f3bb5ca05d01038c45b278c09a/scipy-1.18.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f351e0dd702687d12a402b867a1b4146a256923e1c38317cbc472f6372b94707", size = 31399325, upload-time = "2026-06-19T15:01:13.723Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d1/1f287b57c0ff0ee5185dff3946d92c8017d39b0e431f0ae79a3ff1859512/scipy-1.18.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7c7a51b33ce387193c97f228320cf8e87361daa1bba750638677729598b3e677", size = 29092110, upload-time = "2026-06-19T15:01:16.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1a/7b74eb6c392fdcb27d414c0e7558a6d0231eb3b6d73571f479bb81ea8794/scipy-1.18.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:84031d7b052a54fae2f8632e0ec802073d385476eb9a63079bce6e23ef9283d4", size = 20833811, upload-time = "2026-06-19T15:01:20.488Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ad/f3941716320a7b9cb4d68734a903b45fe16eff5fb7da7e16f2e619304979/scipy-1.18.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:56abf29a7c067dde59be8b9a22d606a4ea1b2f2a4b756d9d903c62818f5dacce", size = 23396644, upload-time = "2026-06-19T15:01:23.364Z" }, + { url = "https://files.pythonhosted.org/packages/22/22/1446b62ffe07f9719b7d9b1b6a4e05a772833ae8f441fe4c22c34c9b250f/scipy-1.18.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ad44305cfa24b1ba5803cbbebf033590ccbac1aa5d612d727b785325ab408b0", size = 34079318, upload-time = "2026-06-19T15:01:26.002Z" }, + { url = "https://files.pythonhosted.org/packages/56/3b/b87da667098bb470fa30c7011b0ba351ee976dd395c78798c66e941665a3/scipy-1.18.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:945c1761b93f38d7f99ae81ae80c63e621471608c7eeead563f6df025585cd58", size = 35324320, upload-time = "2026-06-19T15:01:28.881Z" }, + { url = "https://files.pythonhosted.org/packages/f8/a1/c7932f91909759b0267f75fdea34e91309f96b895757534b76a90b6b4344/scipy-1.18.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1a4441f15d620578772a49e5ab48c0ee1f7a0220e387110283062729136b2553", size = 35699541, upload-time = "2026-06-19T15:01:31.968Z" }, + { url = "https://files.pythonhosted.org/packages/f7/86/5185061a1fcc41d18c5dc2463969b3a3964b31d9ac67b2fb05d4c7ff7670/scipy-1.18.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9aac6192fac56bf2ca534389d24623f07b39ff83317d58287285e7fbd622ff76", size = 37472480, upload-time = "2026-06-19T15:01:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/31/8e/f04c68e39919a010d34f2ee1367fd705b0a25a02f609d755f0bfbc0a15fc/scipy-1.18.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e40baea28ae7f5475c779741e2d90b1247c78531207b49c7030e698ff81cee3f", size = 37365390, upload-time = "2026-06-19T15:01:38.091Z" }, + { 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 = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "skl2onnx" +version = "1.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "onnx" }, + { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scikit-learn", version = "1.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/39/a5015fefb613d5172541740540851a301c53392b57051cf4d313cb6d5718/skl2onnx-1.20.0.tar.gz", hash = "sha256:c74ea827d92ba186fe659695e8fc989cd97bfc320edce3d32b9936a5878da10a", size = 956369, upload-time = "2026-01-30T10:52:07.694Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/d3/b0db77025a4683ec1b9aafc301b78c7e2e2059a1e2543e918435f3d03582/skl2onnx-1.20.0-py3-none-any.whl", hash = "sha256:30cac34803d1776c14b336ae945e48ef28debfc339215acde1cc04b963ed3f7b", size = 317169, upload-time = "2026-01-30T10:52:05.824Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.51" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/f1/a7a892f18d4d224e6b26f706531eafccc41e37594d37d304786969ee13cb/sqlalchemy-2.0.51.tar.gz", hash = "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9", size = 9912201, upload-time = "2026-06-15T15:41:20.012Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/76/b3ea1d8842e7b62c718a88d302809003d65ed82011460ca48907dde658c4/sqlalchemy-2.0.51-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e8203d2fbd5c6254692ef0a72c740d75b2f3c7ca345404f4c1a4604813c77c0", size = 2162087, upload-time = "2026-06-15T16:05:15.795Z" }, + { url = "https://files.pythonhosted.org/packages/6c/22/f19552eb7876774d50cfd025337ef5d67acc10cd8f29adab7716cf47c352/sqlalchemy-2.0.51-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1af05726b3d0cdba1c55284bf408fd3b792e690fe2399bfb8304565551cda652", size = 3244579, upload-time = "2026-06-15T16:10:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/e4a2eb5a8ec5cd3c2a0615a2f15f0afca89ac039229599b9ed0c0ed28e5e/sqlalchemy-2.0.51-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e54ff2dd657f2e3e0fbf2b097db1182f7bfea263eca4353f00065bae2a67c3d", size = 3243515, upload-time = "2026-06-15T16:12:22.627Z" }, + { url = "https://files.pythonhosted.org/packages/74/c6/5900ec624fab3360aa2ec59b99bb2046dd79799e310bb78a0514eaa4038e/sqlalchemy-2.0.51-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1e47b1199c2e832e325eacabc8d32d2487f58c9358f97e9a00f5eb93c5680d84", size = 3195492, upload-time = "2026-06-15T16:10:38.097Z" }, + { url = "https://files.pythonhosted.org/packages/8f/41/2ee3c4e1ac4fd22309349823fe13f33febeab1a71db1d7e9d60293a07dcb/sqlalchemy-2.0.51-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c68568f3facf8f66fa76c60e0ced69b67666ffa9941d1d0a3756fda196049080", size = 3215782, upload-time = "2026-06-15T16:12:24.051Z" }, + { url = "https://files.pythonhosted.org/packages/ce/1c/3bd72c341f1cb5faed5a7457ea840228a46be51cfbaf31a9db72fc963f11/sqlalchemy-2.0.51-cp310-cp310-win32.whl", hash = "sha256:0592bdadf86ddcabfd72d9ab66ea8a5d8d2cc6be1cc51fa7e66c03868ac5eac1", size = 2122119, upload-time = "2026-06-15T16:13:26.915Z" }, + { url = "https://files.pythonhosted.org/packages/2a/63/b6dfdd646abf91c3bedb13727226a5e765e5f8365e898d43818e6672fa46/sqlalchemy-2.0.51-cp310-cp310-win_amd64.whl", hash = "sha256:740cf6f35351b1ac3d82369152acf1d51d37e3dcf85d4dc0a22ca01410eabe2a", size = 2145158, upload-time = "2026-06-15T16:13:28.386Z" }, + { url = "https://files.pythonhosted.org/packages/3a/69/a67c69e5f28fc9c99d6f7bd60bd50e91f2fed2423e3b30fb228fa00e51f3/sqlalchemy-2.0.51-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1aa10c0daee6705294d181daadaa793221e1a59ed55000a3fab1d42b088ce4ba", size = 2161838, upload-time = "2026-06-15T16:05:17.144Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/c8c22b8438bddc0a030157c6ec0f6ef97b3c38effa444bdab2a27af04090/sqlalchemy-2.0.51-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5b2ed6d828f1f09bd812861f4f59ca3bc3803f9df871f4555187f0faf018604", size = 3319402, upload-time = "2026-06-15T16:10:40.002Z" }, + { url = "https://files.pythonhosted.org/packages/90/54/44012d32fd77d991256d2ff793ba3807c51d40cb27a85b4796224f6744df/sqlalchemy-2.0.51-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:436728ce18a80f6951a1e11cc6112c2ede9faf20766f1a26195a7c441ca12dbd", size = 3319675, upload-time = "2026-06-15T16:12:25.658Z" }, + { url = "https://files.pythonhosted.org/packages/29/a5/de0592acaf5906cd7430874392d6f7e8b4a7c8437610953ee2d1501c0b44/sqlalchemy-2.0.51-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dc261707bf5739aea8a541593f3cc1d463c2701fb05fbcbba0ce031b69a21260", size = 3270777, upload-time = "2026-06-15T16:10:42.125Z" }, + { url = "https://files.pythonhosted.org/packages/cb/14/a44c90739c780b362238e4ac3cb19dd0ca40d13e6ddc5daa112166ddab4f/sqlalchemy-2.0.51-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a6d26094615306d116dd5e4a51b0304c99dd2356fc569eed6922a80a6bd3b265", size = 3293940, upload-time = "2026-06-15T16:12:27.156Z" }, + { url = "https://files.pythonhosted.org/packages/65/eb/fbd0f206a330e66f8c602a99c37c4e731f107faed62954b41b01f16dd9d9/sqlalchemy-2.0.51-cp311-cp311-win32.whl", hash = "sha256:ca8435d13829b92f4a97362d91975154a4015db3a2634154e1754e9a915e6b86", size = 2121183, upload-time = "2026-06-15T16:13:29.905Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fd/005bf80f3cf6e5c62b5dd68616280f51cd012c60840fa74781b3ed7b1623/sqlalchemy-2.0.51-cp311-cp311-win_amd64.whl", hash = "sha256:4a011ea4510683319ce4ed274b56ee05194b39b6da9d09ca7a39388f0fa84dcc", size = 2145796, upload-time = "2026-06-15T16:13:31.283Z" }, + { url = "https://files.pythonhosted.org/packages/d5/70/e868bc5412acd101a8280f25c95f10eeae0771c4eb806b02491142810ee8/sqlalchemy-2.0.51-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a", size = 2160291, upload-time = "2026-06-15T16:08:48.918Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1c/71ee0f8a6b9d7316a1ccd30430b4c62b6c2e36adc96017a4e3a72dce49d6/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e", size = 3343835, upload-time = "2026-06-15T16:19:42.613Z" }, + { url = "https://files.pythonhosted.org/packages/2b/7c/7ab9f9aadc5944fdd06612484ed7918fe376ad871a5f50404dc1536e0194/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9", size = 3358470, upload-time = "2026-06-15T16:26:38.011Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7d/ff77169fee6186de145a7f2b87006c39638391130abbab2b1f63ac6ea583/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389", size = 3289874, upload-time = "2026-06-15T16:19:45.212Z" }, + { url = "https://files.pythonhosted.org/packages/6f/3b/6c505903710d781b55bc3141ee34a062bf9745a6b5bc7333305b9ed63b33/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d", size = 3321692, upload-time = "2026-06-15T16:26:39.747Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b7/c5ffe50aa2f4d947c9250e1519d939260329a07fe6272edfccd784b3d007/sqlalchemy-2.0.51-cp312-cp312-win32.whl", hash = "sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5", size = 2119674, upload-time = "2026-06-15T16:23:09.543Z" }, + { url = "https://files.pythonhosted.org/packages/25/dc/46a65916af68a06ef6b972c6050ba4c8f97070fe3fb33097d34229d9bef6/sqlalchemy-2.0.51-cp312-cp312-win_amd64.whl", hash = "sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080", size = 2146670, upload-time = "2026-06-15T16:23:11.048Z" }, + { url = "https://files.pythonhosted.org/packages/54/fe/a210d52fd1a90ecfae8a78e9d8b27e18d733d60818a8bf250ff690b75120/sqlalchemy-2.0.51-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07", size = 2157184, upload-time = "2026-06-15T16:08:50.374Z" }, + { url = "https://files.pythonhosted.org/packages/17/6b/2dce8369b199cb855110e056032f94a9f66dacc2237d3d39c115a86eac56/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195", size = 3284735, upload-time = "2026-06-15T16:19:46.934Z" }, + { url = "https://files.pythonhosted.org/packages/53/ff/dbc495b8a14da840faffb353857a72d4190113cac33727906fb997047f0f/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f", size = 3302756, upload-time = "2026-06-15T16:26:41.336Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d5/fde8f4dddcf518ee15ab35a7c6a28acc32c8ba548d1d2aa451f96e6dbb0b/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400", size = 3232055, upload-time = "2026-06-15T16:19:49.286Z" }, + { url = "https://files.pythonhosted.org/packages/67/d1/43d3a0ac955a58601c24fa23038b1c55ee3a1ec02c0f96ebb1eae2bcf614/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d", size = 3269850, upload-time = "2026-06-15T16:26:43.017Z" }, + { url = "https://files.pythonhosted.org/packages/94/df/de669c7054cd47c4439ac34b1b2ee8b804a794791fbb10720e997a2c87c7/sqlalchemy-2.0.51-cp313-cp313-win32.whl", hash = "sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b", size = 2117721, upload-time = "2026-06-15T16:23:12.36Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8a/403c51d064196bae20a0bc2476577f83a3f8dd299719a97417086b7f2ec5/sqlalchemy-2.0.51-cp313-cp313-win_amd64.whl", hash = "sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5", size = 2143615, upload-time = "2026-06-15T16:23:13.906Z" }, + { url = "https://files.pythonhosted.org/packages/b1/49/a739be2e1d02a96a658eb71ab45d921c874249252358ad24a5bffdd02525/sqlalchemy-2.0.51-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491", size = 2158999, upload-time = "2026-06-15T16:08:51.759Z" }, + { url = "https://files.pythonhosted.org/packages/23/6b/2e0e38cf75c8780eca78d9b2e78164f8bcfd70125e5caa588ff5cbb9c9f4/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d", size = 3282539, upload-time = "2026-06-15T16:19:51.065Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a1/e77854cb5336fd37dc3c6ae3b71de242c98caac5725120be0b526b31cbd0/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54", size = 3287545, upload-time = "2026-06-15T16:26:44.735Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ab/9e17272fd4dac8df3b83c4fbe52b998a1c9d89a843c8c35ff29b74ff7364/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e", size = 3230929, upload-time = "2026-06-15T16:19:52.625Z" }, + { url = "https://files.pythonhosted.org/packages/02/3c/52f408ea701781caee975606beccc48845f2aee8711ac29843d612c0306c/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d", size = 3252888, upload-time = "2026-06-15T16:26:46.454Z" }, + { url = "https://files.pythonhosted.org/packages/24/16/3efd2ee6bc4ca4693a30a1dd17a91b606cae15d517d2a4746611d9b73ce8/sqlalchemy-2.0.51-cp314-cp314-win32.whl", hash = "sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8", size = 2120551, upload-time = "2026-06-15T16:23:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/7b/78/55b12e70f45bccc40d9e483925c065027b3b98ea4cbbdf6f8c2546feaf6c/sqlalchemy-2.0.51-cp314-cp314-win_amd64.whl", hash = "sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499", size = 2146318, upload-time = "2026-06-15T16:23:17.108Z" }, + { url = "https://files.pythonhosted.org/packages/21/db/a9574ed40fed418924b1b1a3e54f47ee3963053b3d3d325a0d36b41f2c08/sqlalchemy-2.0.51-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de", size = 2178920, upload-time = "2026-06-15T15:59:56.285Z" }, + { url = "https://files.pythonhosted.org/packages/bf/90/a1bb5c7cbba76b7bc1fbd586d0a5479a7bc9c27b4a8298f22ec9423b2bb3/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7", size = 3566534, upload-time = "2026-06-15T15:58:35.024Z" }, + { url = "https://files.pythonhosted.org/packages/15/4b/481f1fed30e0e9e8dd24aecbb49f29eb57fe7657ece5cf06ee9b84bb97d8/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72", size = 3535844, upload-time = "2026-06-15T16:02:43.973Z" }, + { url = "https://files.pythonhosted.org/packages/02/71/0aa64aeda645510af0a43f7d9ee70932f0d1dc4263aed34c50ee891d9df3/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23", size = 3475355, upload-time = "2026-06-15T15:58:36.592Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/6061db32316446135a3abae5f308d144ab988a34234726042da3e58b1c63/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522", size = 3486591, upload-time = "2026-06-15T16:02:45.346Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c9/f14fdf71bb8957e0c7e39db69bbdf12b5c80f4ef775fdfa127bf4e0d6760/sqlalchemy-2.0.51-cp314-cp314t-win32.whl", hash = "sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7", size = 2151313, upload-time = "2026-06-15T16:03:39.127Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c6/673e618e6f4f297e126d9b56ea2f6478708f6c1af4e3223835c22e2c3697/sqlalchemy-2.0.51-cp314-cp314t-win_amd64.whl", hash = "sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2", size = 2186280, upload-time = "2026-06-15T16:03:40.569Z" }, + { url = "https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", size = 1944334, upload-time = "2026-06-15T16:09:22.418Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "tqdm" +version = "4.70.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/3b/6c24bec5be5e743ffd99576daa5cc077722fc7d5bbc00bd133fa0c698dc6/tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220", size = 795438, upload-time = "2026-07-27T11:33:15.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/1c/01bfd571a64e7f270e6bab5e33777debe0edc56759233ce84f27dec92d14/tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953", size = 80184, upload-time = "2026-07-27T11:33:13.167Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, +] + +[[package]] +name = "xgboost" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*'", + "python_full_version < '3.11'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "nvidia-nccl-cu12", marker = "python_full_version < '3.12' and sys_platform == 'linux'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/bb/1eb0242409d22db725d7a88088e6cfd6556829fb0736f9ff69aa9f1e9455/xgboost-3.2.0.tar.gz", hash = "sha256:99b0e9a2a64896cdaf509c5e46372d336c692406646d20f2af505003c0c5d70d", size = 1263936, upload-time = "2026-02-10T11:03:05.542Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/49/6e4cdd877c24adf56cb3586bc96d93d4dcd780b5ea1efb32e1ee0de08bae/xgboost-3.2.0-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:2f661966d3e322536d9c448090a870fcba1e32ee5760c10b7c46bac7a342079a", size = 2507014, upload-time = "2026-02-10T10:50:57.44Z" }, + { url = "https://files.pythonhosted.org/packages/93/f1/c09ef1add609453aa3ba5bafcd0d1c1a805c1263c0b60138ec968f8ec296/xgboost-3.2.0-py3-none-macosx_12_0_arm64.whl", hash = "sha256:eabbd40d474b8dbf6cb3536325f9150b9e6f0db32d18de9914fb3227d0bef5b7", size = 2328527, upload-time = "2026-02-10T10:51:17.502Z" }, + { url = "https://files.pythonhosted.org/packages/96/9f/d9914a7b8df842832850b1a18e5f47aaa071c217cdd1da2ae9deb291018b/xgboost-3.2.0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:852eabc6d3b3702a59bf78dbfdcd1cb9c4d3a3b6e5ed1f8781d8b9512354fdd2", size = 131100954, upload-time = "2026-02-10T11:02:42.704Z" }, + { url = "https://files.pythonhosted.org/packages/79/98/679de17c2caa4fd3b0b4386ecf7377301702cb0afb22930a07c142fcb1d8/xgboost-3.2.0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:99b4a6bbcb47212fec5cf5fbe12347215f073c08967431b0122cfbd1ee70312c", size = 131748579, upload-time = "2026-02-10T10:54:40.424Z" }, + { url = "https://files.pythonhosted.org/packages/1f/3d/1661dd114a914a67e3f7ab66fa1382e7599c2a8c340f314ad30a3e2b4d08/xgboost-3.2.0-py3-none-win_amd64.whl", hash = "sha256:0d169736fd836fc13646c7ab787167b3a8110351c2c6bc770c755ee1618f0442", size = 101681668, upload-time = "2026-02-10T10:59:31.202Z" }, +] + +[[package]] +name = "xgboost" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version >= '3.13' and python_full_version < '3.15'", + "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 = "nvidia-nccl-cu13", marker = "python_full_version >= '3.12' and sys_platform == 'linux'" }, + { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/11/2b1de4cb9b7eeb042ca49c7d3b3ed2b77e7645ee6c0f99cf616716f3e8d7/xgboost-3.4.0.tar.gz", hash = "sha256:a6b5d114e7186b5a68c5b08b42297fc76c8c5d7292294220cdb12a6efd59977e", size = 1231819, upload-time = "2026-08-04T11:14:57.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/27/3172bd833a41815ffed3757be5077bbb66a34afb389393606518b1889741/xgboost-3.4.0-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:bc7aeee7c72e827f6406e8dc1f56c4c13d2ff2470fc6b4093b07c1e84b6c3920", size = 2541629, upload-time = "2026-08-04T11:13:50.234Z" }, + { url = "https://files.pythonhosted.org/packages/b6/0f/a4628c03bcc85b16dced3854af2bcb8f3bb8deac718c1bcb5e12740ec5d5/xgboost-3.4.0-py3-none-macosx_12_0_arm64.whl", hash = "sha256:7c328ab0fe446883271aab194ce1e4f49b37b4bd919a258a3ca74fb83e64029c", size = 2365529, upload-time = "2026-08-04T11:13:53.05Z" }, + { url = "https://files.pythonhosted.org/packages/69/49/49d43a6244877904eb3df40d064303114a7f921d4bf730ab55b169e1f9dc/xgboost-3.4.0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:1aaf7fb7af8ab306b1ef20f9576dbc1a06a7a42d2a366169650d024138d39638", size = 57196337, upload-time = "2026-08-04T11:14:09.981Z" }, + { url = "https://files.pythonhosted.org/packages/ae/1b/d631cf47b046ba9fae9c86bf2dba88bec3c2f42173d5216fe81fb9be729c/xgboost-3.4.0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:ba132153e3c696f090be808bc42b8e4248932ab9a4ee66441154c954789b507c", size = 57615296, upload-time = "2026-08-04T11:14:30.855Z" }, + { url = "https://files.pythonhosted.org/packages/89/97/838165ec9399aa5e6add044f05b64d877d45125adeda15869021b9e625ce/xgboost-3.4.0-py3-none-win_amd64.whl", hash = "sha256:23c62770c38fb9acd8b40d6c7df6de8fa077ef7afcb84aafd898faabbec639f9", size = 48935508, upload-time = "2026-08-04T11:14:50.954Z" }, + { url = "https://files.pythonhosted.org/packages/56/84/92c3af9c167815a15cfa09e5579d6cc604dc6f08b1ae061b3756274cf530/xgboost-3.4.0-py3-none-win_arm64.whl", hash = "sha256:9cdc68b992e55717438122174e2b3d9527c59882818fb954a329a62786012709", size = 2094191, upload-time = "2026-08-04T11:14:55.472Z" }, +]