diff --git a/examples/bench_ackley.py b/examples/bench_ackley.py new file mode 100644 index 0000000..74e6f4c --- /dev/null +++ b/examples/bench_ackley.py @@ -0,0 +1,21 @@ +"""here.""" + +from deephyper.hpo import CBO +from deephyper_benchmark.benchmarks.cbbo import AckleyBenchmark + + +def main(): + """Run example.""" + bench = AckleyBenchmark() + + search = CBO(bench.problem, bench.run_function) + results = search.search(25) + + print(results) + + cumul_regret = bench.scorer.cumul_regret(results.objective) + print(cumul_regret) + + +if __name__ == "__main__": + main() diff --git a/examples/bench_branin.py b/examples/bench_branin.py new file mode 100644 index 0000000..41fb347 --- /dev/null +++ b/examples/bench_branin.py @@ -0,0 +1,21 @@ +"""here.""" + +from deephyper.hpo import CBO +from deephyper_benchmark.benchmarks.cbbo import BraninBenchmark + + +def main(): + """Run example.""" + bench = BraninBenchmark() + + search = CBO(bench.problem, bench.run_function) + results = search.search(25) + + print(results) + + cumul_regret = bench.scorer.cumul_regret(results.objective) + print(cumul_regret) + + +if __name__ == "__main__": + main() diff --git a/examples/bench_combo.py b/examples/bench_combo.py deleted file mode 100644 index b8fdac5..0000000 --- a/examples/bench_combo.py +++ /dev/null @@ -1,26 +0,0 @@ -import logging - -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s - %(levelname)s - %(filename)s:%(funcName)s - %(message)s", - force=True, -) - -import deephyper_benchmark as dhb - -# dhb.install("ECP-Candle/Pilot1/Combo") - -dhb.load("ECP-Candle/Pilot1/Combo") - -# Run training with Testing scores -from deephyper_benchmark.lib.ecp_candle.pilot1.combo import model -res = model.run_pipeline(mode="test") -print(f"{res=}") - -# Run HPO-pipeline with default configuration of hyperparameters -from deephyper_benchmark.lib.ecp_candle.pilot1.combo import hpo -config = hpo.problem.default_configuration -res = hpo.run(config) -print(f"{res=}") - - diff --git a/examples/bench_dtlz.py b/examples/bench_dtlz.py index 24a527a..d48c023 100644 --- a/examples/bench_dtlz.py +++ b/examples/bench_dtlz.py @@ -1,50 +1,28 @@ -# Setup info-level logging -import logging -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s - %(levelname)s - %(filename)s:%(funcName)s - " + \ - "%(message)s", - force=True, -) - -# Set DTLZ problem environment variables -import os -os.environ["DEEPHYPER_BENCHMARK_NDIMS"] = "5" # 5 vars -os.environ["DEEPHYPER_BENCHMARK_NOBJS"] = "3" # 2 objs -os.environ["DEEPHYPER_BENCHMARK_DTLZ_PROB"] = "2" # DTLZ2 problem -os.environ["DEEPHYPER_BENCHMARK_DTLZ_OFFSET"] = "0.6" # [x_o, .., x_d]*=0.6 - -# Load DTLZ benchmark suite, nothing to install -import deephyper_benchmark as dhb -dhb.load("DTLZ") - - -# Necessary IF statement otherwise it will enter in a infinite loop -# when loading the 'run' function from a subprocess -if __name__ == "__main__": - from deephyper.problem import HpProblem - from deephyper.search.hps import CBO - - # Run HPO-pipeline with default configuration of hyperparameters - from deephyper_benchmark.lib.dtlz import hpo - from deephyper.evaluator import RunningJob, Evaluator - config = hpo.problem.default_configuration - print(config) - res = hpo.run(RunningJob(parameters=config)) - print(f"{res=}") - - # define the evaluator to distribute the computation - evaluator = Evaluator.create( - hpo.run, - method="process", - method_kwargs={ - "num_workers": 2, - }, - ) - - # define your search and execute it - search = CBO(hpo.problem, evaluator) - - # solve with 100 evals - results = search.search(max_evals=100) +"""here.""" + +import numpy as np +from deephyper.hpo import CBO +from deephyper_benchmark.benchmarks.dtlz import DTLZBenchmark + + +def main(): + """Run example.""" + nobj = 2 + bench = DTLZBenchmark(nobj=nobj) + + search = CBO(bench.problem, bench.run_function, acq_optimizer="sampling") + results = search.search(25) + print(results) + + obj = results[[f"objective_{i}" for i in range(nobj)]].values + print(np.shape(obj)) + hvi = bench.scorer.hypervolume_score(obj) + print(hvi) + + hvi_iter = bench.scorer.hypervolume(obj) + print(hvi_iter) + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index f692c9b..62d8017 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,6 @@ build-backend = "hatchling.build" name = "deephyper-benchmark" version = "0.0.2" dependencies = [ - # "deephyper", "deephyper>=0.9.3", ] requires-python = ">=3.10" @@ -29,12 +28,6 @@ classifiers = [ "Programming Language :: Python :: 3.13", ] -[project.optional-dependencies] - - -[project.scripts] - - [project.urls] Documentation = "http://deephyper.readthedocs.io" Changes = "https://github.com/deephyper/deephyper/releases" @@ -42,10 +35,6 @@ Forum = "https://github.com/deephyper/deephyper/discussions" GitHub = "https://github.com/deephyper/deephyper" Issues = "https://github.com/deephyper/deephyper/issues" -[tool.pytest.ini_options] -norecursedirs = ".git" - - [tool.coverage.paths] source = ["src/", "*/site-packages"] @@ -56,9 +45,15 @@ source = ["deephyper"] [tool.coverage.report] exclude_lines = ["if __name__ == '__main__':"] +[tool.pyright] +pythonVersion = "3.13" +venvPath = "." +venv = ".venv" + +[tool.pytest.ini_options] +norecursedirs = ".git" + [tool.ruff] -exclude = [ -] line-length = 100 [tool.ruff.lint] diff --git a/src/deephyper_benchmark/__init__.py b/src/deephyper_benchmark/__init__.py index 7672c1f..0c5a6ea 100644 --- a/src/deephyper_benchmark/__init__.py +++ b/src/deephyper_benchmark/__init__.py @@ -1,6 +1,13 @@ -"""DeepHyper-Benchmark package.""" +"""Public interface for deephyper_benchmark package.""" from ._benchmark import Benchmark, HPOBenchmark -from ._scorer import Scorer, HPOScorer +from ._scorer import Scorer, HPOScorer, MultiObjHPOScorer -__all__ = ["Benchmark", "HPOBenchmark", "Scorer", "HPOScorer"] + +__all__ = [ + "Benchmark", + "HPOBenchmark", + "Scorer", + "HPOScorer", + "MultiObjHPOScorer", +] diff --git a/src/deephyper_benchmark/_benchmark.py b/src/deephyper_benchmark/_benchmark.py index 3850d7f..219e311 100644 --- a/src/deephyper_benchmark/_benchmark.py +++ b/src/deephyper_benchmark/_benchmark.py @@ -2,15 +2,12 @@ class Benchmark(abc.ABC): - def __init__(self): - super().__init__() - self.refresh_settings() - - def refresh_settings(self): - """Refresh benchmark settings.""" + """Base class for benchmarks.""" class HPOBenchmark(Benchmark): + """Base class for Hyperparameter optimization benchmarks.""" + @property @abc.abstractmethod def problem(self): diff --git a/src/deephyper_benchmark/_scorer.py b/src/deephyper_benchmark/_scorer.py index cd0e695..acea947 100644 --- a/src/deephyper_benchmark/_scorer.py +++ b/src/deephyper_benchmark/_scorer.py @@ -1,5 +1,7 @@ import abc + import numpy as np +from deephyper.skopt.moo import hypervolume class Scorer(abc.ABC): @@ -7,7 +9,10 @@ class Scorer(abc.ABC): class HPOScorer(Scorer): - @abc.abstractmethod + def __init__(self, nparams: int, y_max: float): + self.nparams = nparams + self.y_max = y_max + def simple_regret(self, y: np.ndarray) -> np.ndarray: """Compute the regret of a list of given solution. @@ -17,8 +22,8 @@ def simple_regret(self, y: np.ndarray) -> np.ndarray: Returns: np.ndarray: An array of regret values. """ + return self.y_max - y - @abc.abstractmethod def cumul_regret(self, y: np.ndarray) -> np.ndarray: """Compute the cumulative regret of an array of ordered given solution. @@ -28,3 +33,51 @@ def cumul_regret(self, y: np.ndarray) -> np.ndarray: Returns: np.ndarray: An array of cumulative regret values. """ + return np.cumsum(self.simple_regret(y)) + + +class MultiObjHPOScorer(Scorer): + def __init__(self, nobj: int): + self.nobj = nobj + + def hypervolume_score(self, pts: np.ndarray): + """Calculate the hypervolume dominated by soln, wrt the Nadir point. + + Args: + pts (np.ndarray): A 2d array of objective values. + Each row is an objective value in the solution set. + + Returns: + float: The total hypervolume dominated by the current solution, + filtering out points worse than the Nadir point and using the + Nadir point as the reference. + + """ + if np.any(pts < 0): + filtered_pts = -pts.copy() + else: + filtered_pts = pts.copy() + nadir = self.nadir_point + for i in range(pts.shape[0]): + if np.any(filtered_pts[i, :] > nadir): + filtered_pts[i, :] = nadir + return hypervolume(filtered_pts, nadir) + + @property + @abc.abstractmethod + def nadir_point(self): + pass + + def hypervolume(self, y: np.ndarray) -> np.ndarray: + """Compute the regret of a list of given solution. + + Args: + y (np.ndarray): An array of solutions. + + Returns: + np.ndarray: An array of regret values. + """ + scores = np.zeros(y.shape[0]) + for i in range(1, y.shape[0] + 1): + scores[i - 1] = self.hypervolume_score(y[:i]) + return scores diff --git a/src/deephyper_benchmark/benchmarks/__init__.py b/src/deephyper_benchmark/benchmarks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/deephyper_benchmark/benchmarks/cbbo/README.md b/src/deephyper_benchmark/benchmarks/cbbo/README.md new file mode 100644 index 0000000..6ecf11d --- /dev/null +++ b/src/deephyper_benchmark/benchmarks/cbbo/README.md @@ -0,0 +1,20 @@ +# C-BBO: Continuous Black-Box Optimization + +> **Warning** +> Work in progress, this benchmark is not yet ready. + +Set of continuous function benchmarks. The `DEEPHYPER_BENCHMARK_NDIMS` environment variable defines the number of dimensions of the problem. + +| Function Name | Number of Dimensions | Comment | +| ------------- | --------------------- | ------------------------------------------- | +| ackley | $\infty$ (default 5) | Many local minima and single global optimum | +| branin | 2 | Three global optimum | +| cossin | 1 | Many local minima, good for visualisation. | +| easom | 2 | Almost flat everywhere | +| griewank | $\infty$ (default 5) | | +| hartmann6D | 6 | | +| levy | $\infty$ (default 5) | | +| michal | $\infty$ (default 2) | | +| rosen | $\infty$ (default 5) | | +| schwefel | $\infty$ (default 5) | | +| shekel | 4 | Many local minima with flat areas | \ No newline at end of file diff --git a/src/deephyper_benchmark/benchmarks/cbbo/__init__.py b/src/deephyper_benchmark/benchmarks/cbbo/__init__.py new file mode 100644 index 0000000..c85debc --- /dev/null +++ b/src/deephyper_benchmark/benchmarks/cbbo/__init__.py @@ -0,0 +1,25 @@ +"""Subpackage for the C-BBO benchmarks.""" + +from .ackley import AckleyBenchmark +from .branin import BraninBenchmark +from .easom import EasomBenchmark +from .griewank import GriewankBenchmark +from .hartmann6D import Hartmann6DBenchmark +from .levy import LevyBenchmark +from .michal import MichalBenchmark +from .rosen import RosenBenchmark +from .schwefel import SchwefelBenchmark +from .shekel import ShekelBenchmark + +__all__ = [ + "AckleyBenchmark", + "BraninBenchmark", + "EasomBenchmark", + "GriewankBenchmark", + "Hartmann6DBenchmark", + "LevyBenchmark", + "MichalBenchmark", + "RosenBenchmark", + "SchwefelBenchmark", + "ShekelBenchmark", +] diff --git a/src/deephyper_benchmark/benchmarks/cbbo/ackley.py b/src/deephyper_benchmark/benchmarks/cbbo/ackley.py new file mode 100644 index 0000000..0d9654d --- /dev/null +++ b/src/deephyper_benchmark/benchmarks/cbbo/ackley.py @@ -0,0 +1,84 @@ +"""here.""" + +import functools + +import numpy as np +from deephyper.hpo import HpProblem + +from deephyper_benchmark import HPOBenchmark, HPOScorer + +from .utils import run_function + + +def ackley(x, a=20, b=0.2, c=2 * np.pi): + """Ackley function. + + Description of the function: https://www.sfu.ca/~ssurjano/ackley.html + """ + d = len(x) + s1 = np.sum(x**2) + s2 = np.sum(np.cos(c * x)) + term1 = -a * np.exp(-b * np.sqrt(s1 / d)) + term2 = -np.exp(s2 / d) + y = term1 + term2 + a + np.exp(1) + return -y + + +class AckleyScorer(HPOScorer): + """A class defining performance evaluators for the Ackley problem.""" + + def __init__( + self, + nparams, + nslack, + offset=0.0, + ): + self.nparams = nparams + self.nslack = nslack + self.offset = offset + self.x_max = np.asarray([offset for _ in range(self.nparams)]) + self.y_max = 0.0 + + +class AckleyBenchmark(HPOBenchmark): + """Ackley benchmark. + + Args: + nparams (int, optional): the number of parameters in the problem. + offset (int, optional): the offset in the space of parameters. + nslack (int, optional): the number of additional slack parameters in the problem. + """ + + def __init__(self, nparams: int = 5, offset: int = -4.0, nslack: int = 0) -> None: + self.nparams = nparams + self.nslack = nslack + assert offset <= 32.768 and offset >= -32.768, ( + "offset must be in [-32.768, 32.768] to keep the same maximum value." + ) + self.offset = offset + + @property + def problem(self): # noqa: D102 + # The original range is simetric (-32.768, 32.768) but we make it less simetric to avoid + # Grid sampling or QMC sampling to directly hit the optimum... + domain = (-32.768 + self.offset, 32.768 + self.offset) + problem = HpProblem() + for i in range(self.nparams - self.nslack): + problem.add_hyperparameter(domain, f"x{i}") + + # Add slack/dummy dimensions (useful to test predicors which are sensitive + # to unimportant features) + for i in range( + self.nparams - self.nslack, + self.nparams, + ): + problem.add_hyperparameter(domain, f"z{i}") + return problem + + @property + def run_function(self): # noqa: D102 + return functools.partial(run_function, bb_func=ackley) + + @property + def scorer(self): # noqa: D102 + return AckleyScorer(self.nparams, self.nslack, self.offset) diff --git a/src/deephyper_benchmark/benchmarks/cbbo/branin.py b/src/deephyper_benchmark/benchmarks/cbbo/branin.py new file mode 100644 index 0000000..cca3531 --- /dev/null +++ b/src/deephyper_benchmark/benchmarks/cbbo/branin.py @@ -0,0 +1,54 @@ +"""here.""" + +import functools + +import numpy as np +from deephyper.hpo import HpProblem + +from deephyper_benchmark import HPOBenchmark, HPOScorer + +from .utils import run_function + + +def branin(x): + """Branin function. + + Description of the function: https://www.sfu.ca/~ssurjano/branin.html" + """ + assert len(x) == 2 + a = 1.0 + b = 5.1 / (4.0 * np.pi**2) + c = 5.0 / np.pi + r = 6.0 + s = 10.0 + t = 1.0 / (8.0 * np.pi) + y = a * (x[1] - b * x[0] ** 2 + c * x[0] - r) ** 2 + s * (1 - t) * np.cos(x[0]) + s + return -y + + +class BraninHPOScorer(HPOScorer): + """A class defining performance evaluators for the Ackley problem.""" + + def __init__(self): + self.nparams = 2 + self.x_max = np.array([[-np.pi, 12.275], [np.pi, 2.275], [9.42478, 2.475]]) + self.y_max = -0.397887 + + +class BraninBenchmark(HPOBenchmark): + """Branin benchmark.""" + + @property + def problem(self): # noqa: D102 + problem = HpProblem() + problem.add_hyperparameter((-5.0, 10.0), "x0") + problem.add_hyperparameter((0.0, 15.0), "x1") + return problem + + @property + def run_function(self): # noqa: D102 + return functools.partial(run_function, bb_func=branin) + + @property + def scorer(self): # noqa: D102 + return BraninHPOScorer() diff --git a/src/deephyper_benchmark/benchmarks/cbbo/cossin/hpo_benchmark.py b/src/deephyper_benchmark/benchmarks/cbbo/cossin/hpo_benchmark.py new file mode 100644 index 0000000..b4d0487 --- /dev/null +++ b/src/deephyper_benchmark/benchmarks/cbbo/cossin/hpo_benchmark.py @@ -0,0 +1,115 @@ +"""Module defining the problem and run-function of the benchmark.""" + +import os +import time + +import numpy as np + +from deephyper.hpo import HpProblem +from deephyper.evaluator import profile, RunningJob +from deephyper_benchmark import HPOBenchmark, HPOScorer + +__all__ = ["benchmark"] + + +def cossin(x): + x = np.sum(x) + y = np.cos(5 * x / 10) + 2 * np.sin(x / 10) + x / 100 + return y + + +@profile +def run_function(job: RunningJob, sleep=False, sleep_mean=60, sleep_noise=20, y_noise=0.0) -> dict: + config = job.parameters + + if sleep: + t_sleep = np.random.normal(loc=sleep_mean, scale=sleep_noise) + t_sleep = max(t_sleep, 0) + time.sleep(t_sleep) + + x = np.array([config[k] for k in config if "x" in k]) + x = np.asarray_chkfinite(x) # ValueError if any NaN or Inf + + y = cossin(x) + + if y_noise > 0.0: + rng = np.random.RandomState(int(job.id.split(".")[-1])) + eps = rng.normal(loc=0.0, scale=y_noise) + y += eps + + return y + + +class CosSinHPOScorer(HPOScorer): + """A class defining performance evaluators for the CosSin problem.""" + + def __init__( + self, + p_num, + p_num_slack, + ): + self.p_num = p_num + self.x_min = np.full((self.p_num,), 18.6738) + self.x_min[self.p_num - p_num_slack :] = np.nan + self.y_min = 3.1 + + def simple_regret(self, y: np.ndarray) -> np.ndarray: + """Compute the regret of a list of given solution. + + Args: + y (np.ndarray): An array of solutions. + + Returns: + np.ndarray: An array of regret values. + """ + return self.y_min - y + + def cumul_regret(self, y: np.ndarray) -> np.ndarray: + """Compute the cumulative regret of an array of ordered given solution. + + Args: + y (np.ndarray): An array of solutions. + + Returns: + np.ndarray: An array of cumulative regret values. + """ + return np.cumsum(self.simple_regret(y)) + + +class CosSinHPOBenchmark(HPOBenchmark): + def refresh_settings(self): + self.DEEPHYPER_BENCHMARK_NDIMS = int(os.environ.get("DEEPHYPER_BENCHMARK_NDIMS", 1)) + self.DEEPHYPER_BENCHMARK_OFFSET = float(os.environ.get("DEEPHYPER_BENCHMARK_OFFSET", 0.0)) + self.DEEPHYPER_BENCHMARK_NDIMS_SLACK = int( + os.environ.get("DEEPHYPER_BENCHMARK_NDIMS_SLACK", 0) + ) + + @property + def problem(self): + domain = ( + -50.0 - self.DEEPHYPER_BENCHMARK_OFFSET, + 50.0 - self.DEEPHYPER_BENCHMARK_OFFSET, + ) + problem = HpProblem() + for i in range(self.DEEPHYPER_BENCHMARK_NDIMS - self.DEEPHYPER_BENCHMARK_NDIMS_SLACK): + problem.add_hyperparameter(domain, f"x{i}") + + # Add slack/dummy dimensions (useful to test predicors which are sensitive + # to unimportant features) + for i in range( + self.DEEPHYPER_BENCHMARK_NDIMS - self.DEEPHYPER_BENCHMARK_NDIMS_SLACK, + self.DEEPHYPER_BENCHMARK_NDIMS, + ): + problem.add_hyperparameter(domain, f"z{i}") + return problem + + @property + def run_function(self): + return run_function + + @property + def scorer(self): + return CosSinHPOScorer(self.DEEPHYPER_BENCHMARK_NDIMS, self.DEEPHYPER_BENCHMARK_NDIMS_SLACK) + + +benchmark = CosSinHPOBenchmark() diff --git a/src/deephyper_benchmark/benchmarks/cbbo/easom.py b/src/deephyper_benchmark/benchmarks/cbbo/easom.py new file mode 100644 index 0000000..1b86480 --- /dev/null +++ b/src/deephyper_benchmark/benchmarks/cbbo/easom.py @@ -0,0 +1,52 @@ +"""here.""" + +import functools + +import numpy as np +from deephyper.hpo import HpProblem + +from deephyper_benchmark import HPOBenchmark, HPOScorer + +from .utils import run_function + + +def easom(x): + """Easom function. + + Description of the function: https://www.sfu.ca/~ssurjano/easom.html + """ + assert len(x) == 2 + y = -np.cos(x[0]) * np.cos(x[1]) * np.exp(-((x[0] - np.pi) ** 2 + (x[1] - np.pi) ** 2)) + return -y + + +class EasomScorer(HPOScorer): + """A class defining performance evaluators for the Easom problem.""" + + def __init__(self): + self.nparams = 2 + self.x_max = np.array([np.pi, np.pi]) + self.y_max = 1.0 + + +class EasomBenchmark(HPOBenchmark): + """Easom benchmark.""" + + def __init__(self) -> None: + self.nparams = 2 + + @property + def problem(self): # noqa: D102 + domain = (-100.0, 100.0) + problem = HpProblem() + for i in range(self.nparams): + problem.add_hyperparameter(domain, f"x{i}") + return problem + + @property + def run_function(self): # noqa: D102 + return functools.partial(run_function, bb_func=easom) + + @property + def scorer(self): # noqa: D102 + return EasomScorer() diff --git a/src/deephyper_benchmark/benchmarks/cbbo/griewank.py b/src/deephyper_benchmark/benchmarks/cbbo/griewank.py new file mode 100644 index 0000000..62ed818 --- /dev/null +++ b/src/deephyper_benchmark/benchmarks/cbbo/griewank.py @@ -0,0 +1,79 @@ +"""here.""" + +import functools + +import numpy as np +from deephyper.hpo import HpProblem + +from deephyper_benchmark import HPOBenchmark, HPOScorer + +from .utils import run_function + + +def griewank(x, fr=4000): # noqa: D103 + """Griewank function benchmark. + + Description of the function: https://www.sfu.ca/~ssurjano/griewank.html + """ + n = len(x) + j = np.arange(1.0, n + 1) + s = np.sum(x**2) + p = np.prod(np.cos(x / np.sqrt(j))) + y = s / fr - p + 1 + return -y + + +class GriewankScorer(HPOScorer): + """A class defining performance evaluators for the Griewank problem.""" + + def __init__( + self, + nparams, + nslack, + offset=0.0, + ): + self.nparams = nparams + self.nslack = nslack + self.offset = offset + self.x_max = np.asarray([offset for _ in range(self.nparams)]) + self.y_max = 0.0 + + +class GriewankBenchmark(HPOBenchmark): + """Griewank benchmark. + + Args: + nparams (int, optional): the number of parameters in the problem. + offset (int, optional): the offset in the space of parameters. + nslack (int, optional): the number of additional slack parameters in the problem. + """ + + def __init__(self, nparams: int = 5, offset: int = -4.0, nslack: int = 0) -> None: + self.nparams = nparams + assert offset <= 600.0 and offset >= -600.0, ( + "offset must be in [-600.0, 600.0] to keep the same maximum value." + ) + self.offset = offset + self.nslack = nslack + + @property + def problem(self): # noqa: D102 + domain = (-600.0 + self.offset, 600.0 + self.offset) + problem = HpProblem() + for i in range(self.nparams - self.nslack): + problem.add_hyperparameter(domain, f"x{i}") + + for i in range( + self.nparams - self.nslack, + self.nparams, + ): + problem.add_hyperparameter(domain, f"z{i}") + return problem + + @property + def run_function(self): # noqa: D102 + return functools.partial(run_function, bb_func=griewank) + + @property + def scorer(self): # noqa: D102 + return GriewankScorer(self.nparams, self.nslack, self.offset) diff --git a/src/deephyper_benchmark/benchmarks/cbbo/hartmann6D.py b/src/deephyper_benchmark/benchmarks/cbbo/hartmann6D.py new file mode 100644 index 0000000..baa716e --- /dev/null +++ b/src/deephyper_benchmark/benchmarks/cbbo/hartmann6D.py @@ -0,0 +1,80 @@ +"""here.""" + +import functools + +import numpy as np +from deephyper.hpo import HpProblem + +from deephyper_benchmark import HPOBenchmark, HPOScorer + +from .utils import run_function + + +def hartmann6D(x): # noqa: D103 + """Hartmann6D function benchmark. + + Description of the function: https://www.sfu.ca/~ssurjano/hart6.html + """ + alpha = np.array([1.0, 1.2, 3.0, 3.2]) + A = np.array( + [ + [10, 3, 17, 3.5, 1.7, 8], + [0.05, 10, 17, 0.1, 8, 14], + [3, 3.5, 1.7, 10, 17, 8], + [17, 8, 0.05, 10, 0.1, 14], + ] + ) + P = 1e-4 * np.array( + [ + [1312, 1696, 5569, 124, 8283, 5886], + [2329, 4135, 8307, 3736, 1004, 9991], + [2348, 1451, 3522, 2883, 3047, 6650], + [4047, 8828, 8732, 5743, 1091, 381], + ] + ) + X = np.array([x for _ in range(4)]) + inner = np.sum(np.multiply(A, np.square(X - P)), axis=1) + outer = np.sum(alpha * np.exp(-inner)) + y = -(2.58 + outer) / 1.94 + return -y + + +class Hartmann6DScorer(HPOScorer): + """A class defining performance evaluators for the Hartmann6D problem.""" + + def __init__(self): + self.nparams = 6 + self.x_max = np.asarray( + [ + 0.20169, + 0.150011, + 0.476874, + 0.275332, + 0.311652, + 0.6573, + ] + ) + self.y_max = 3.32237 + + +class Hartmann6DBenchmark(HPOBenchmark): + """Hartmann6D benchmark.""" + + def __init__(self) -> None: + self.nparams = 6 + + @property + def problem(self): # noqa: D102 + domain = (0.0, 1.0) + problem = HpProblem() + for i in range(self.nparams): + problem.add_hyperparameter(domain, f"x{i}") + return problem + + @property + def run_function(self): # noqa: D102 + return functools.partial(run_function, bb_func=hartmann6D) + + @property + def scorer(self): # noqa: D102 + return Hartmann6DScorer() diff --git a/src/deephyper_benchmark/benchmarks/cbbo/levy.py b/src/deephyper_benchmark/benchmarks/cbbo/levy.py new file mode 100644 index 0000000..27102f9 --- /dev/null +++ b/src/deephyper_benchmark/benchmarks/cbbo/levy.py @@ -0,0 +1,60 @@ +"""Module for Levy benchmark. + +Description of the function: https://www.sfu.ca/~ssurjano/levy.html +""" + +import functools + +import numpy as np +from deephyper.hpo import HpProblem + +from deephyper_benchmark import HPOBenchmark, HPOScorer + +from .utils import run_function + + +def levy(x): + """Levy benchmark function.""" + z = 1 + (x - 1) / 4 + y = ( + np.sin(np.pi * z[0]) ** 2 + + np.sum((z[:-1] - 1) ** 2 * (1 + 10 * np.sin(np.pi * z[:-1] + 1) ** 2)) + + (z[-1] - 1) ** 2 * (1 + np.sin(2 * np.pi * z[-1]) ** 2) + ) + return -y + + +class LevyScorer(HPOScorer): + """A class defining performance evaluators for the Levy problem.""" + + def __init__(self, nparams: int = 5): + self.nparams = nparams + self.x_max = np.ones(self.nparams) + self.y_max = 0.0 + + +class LevyBenchmark(HPOBenchmark): + """Levy benchmark.""" + + def __init__(self, nparams=5): + """Create a Levy benchmark.""" + self.nparams = nparams + + @property + def problem(self): + """Define the hyperparameter problem.""" + domain = (-10.0, 10.0) + problem = HpProblem() + for i in range(self.nparams): + problem.add_hyperparameter(domain, f"x{i}") + return problem + + @property + def run_function(self): + """Provide the run function for the hyperparameter benchmark.""" + return functools.partial(run_function, bb_func=levy) + + @property + def scorer(self): + """Provide the scorer for the hyperparameter benchmark.""" + return LevyScorer(self.nparams) diff --git a/src/deephyper_benchmark/benchmarks/cbbo/michal.py b/src/deephyper_benchmark/benchmarks/cbbo/michal.py new file mode 100644 index 0000000..af1a4c9 --- /dev/null +++ b/src/deephyper_benchmark/benchmarks/cbbo/michal.py @@ -0,0 +1,66 @@ +"""Module for Michal benchmark. + +Description of the function: https://www.sfu.ca/~ssurjano/michal.html +""" + +import functools + +import numpy as np +from deephyper.hpo import HpProblem + +from deephyper_benchmark import HPOBenchmark, HPOScorer + +from .utils import run_function + + +def michal(x, m=10): + """Michalewicz function benchmark.""" + ix2 = np.arange(1, len(x) + 1) * x**2 + y = -np.sum(np.sin(x) * np.power(np.sin(ix2 / np.pi), 2 * m)) + return -y + + +class MichalScorer(HPOScorer): + """Define performance evaluators for the Michal problem.""" + + def __init__(self, nparams=2): + assert nparams in [2, 5, 10], ( + "nparams should be in [2, 5, 10] otherwise the solution is unknown." + ) + self.nparams = nparams + if self.nparams == 2: + self.x_max = np.array([2.20, 1.57]) + self.y_max = 1.8013 + elif self.nparams == 5: + self.x_max = None + self.y_max = 4.687658 + elif self.nparams == 10: + self.x_max = None + self.y_max = 9.66015 + + +class MichalBenchmark(HPOBenchmark): + """Michal benchmark.""" + + def __init__(self, nparams: int = 5): + """Create a Michal benchmark.""" + self.nparams = nparams + + @property + def problem(self): + """Define the hyperparameter problem.""" + domain = (0.0, np.pi) + problem = HpProblem() + for i in range(self.nparams): + problem.add_hyperparameter(domain, f"x{i}") + return problem + + @property + def run_function(self): + """Provide the run function for the hyperparameter benchmark.""" + return functools.partial(run_function, bb_func=michal) + + @property + def scorer(self): + """Provide the scorer for the hyperparameter benchmark.""" + return MichalScorer(self.nparams) diff --git a/src/deephyper_benchmark/benchmarks/cbbo/rosen.py b/src/deephyper_benchmark/benchmarks/cbbo/rosen.py new file mode 100644 index 0000000..e5cbda1 --- /dev/null +++ b/src/deephyper_benchmark/benchmarks/cbbo/rosen.py @@ -0,0 +1,54 @@ +"""Module for Rosen benchmark. + +Description of the function: https://www.sfu.ca/~ssurjano/rosen.html +""" + +import functools + +import numpy as np +from deephyper.hpo import HpProblem +from scipy.optimize import rosen + +from deephyper_benchmark import HPOBenchmark, HPOScorer + +from .utils import run_function + + +def rosen_(x): # noqa: D103 + return -rosen(x) + + +class RosenScorer(HPOScorer): + """Define performance evaluators for the Rosen problem.""" + + def __init__(self, nparams: int = 5): + self.nparams = nparams + self.x_max = np.ones(self.nparams) + self.y_max = 0.0 + + +class RosenBenchmark(HPOBenchmark): + """Rosen benchmark.""" + + def __init__(self, nparams=5): + """Create a Rosen benchmark.""" + self.nparams = nparams + + @property + def problem(self): + """Define the hyperparameter problem.""" + domain = (-5.0, 10.0) + problem = HpProblem() + for i in range(self.nparams): + problem.add_hyperparameter(domain, f"x{i}") + return problem + + @property + def run_function(self): + """Provide the run function for the hyperparameter benchmark.""" + return functools.partial(run_function, bb_func=rosen_) + + @property + def scorer(self): + """Provide the scorer for the hyperparameter benchmark.""" + return RosenScorer(self.nparams) diff --git a/src/deephyper_benchmark/benchmarks/cbbo/schwefel.py b/src/deephyper_benchmark/benchmarks/cbbo/schwefel.py new file mode 100644 index 0000000..c0de78d --- /dev/null +++ b/src/deephyper_benchmark/benchmarks/cbbo/schwefel.py @@ -0,0 +1,53 @@ +"""Module for Schwefel benchmark. + +Description of the function: https://www.sfu.ca/~ssurjano/schwef.html +""" + +import functools +import numpy as np +from deephyper.hpo import HpProblem +from deephyper_benchmark import HPOBenchmark, HPOScorer +from .utils import run_function + + +def schwefel(x): + """Schwefel benchmark function.""" + n = len(x) + y = 418.9829 * n - sum(x * np.sin(np.sqrt(np.abs(x)))) + return -y + + +class SchwefelScorer(HPOScorer): + """Define performance evaluators for the Schwefel problem.""" + + def __init__(self, nparams=5): + self.nparams = nparams + self.x_max = np.full(self.nparams, fill_value=420.9687) + self.y_max = 0.0 + + +class SchwefelBenchmark(HPOBenchmark): + """Schwefel benchmark.""" + + def __init__(self, nparams: int = 5): + """Create a Schwefel benchmark.""" + self.nparams = nparams + + @property + def problem(self): + """Define the hyperparameter problem.""" + domain = (-500.0, 500.0) + problem = HpProblem() + for i in range(self.nparams): + problem.add_hyperparameter(domain, f"x{i}") + return problem + + @property + def run_function(self): + """Provide the run function for the hyperparameter benchmark.""" + return functools.partial(run_function, bb_func=schwefel) + + @property + def scorer(self): + """Provide the scorer for the hyperparameter benchmark.""" + return SchwefelScorer(self.nparams) diff --git a/src/deephyper_benchmark/benchmarks/cbbo/shekel.py b/src/deephyper_benchmark/benchmarks/cbbo/shekel.py new file mode 100644 index 0000000..e8fc92a --- /dev/null +++ b/src/deephyper_benchmark/benchmarks/cbbo/shekel.py @@ -0,0 +1,67 @@ +"""Module for Shekel benchmark. + +Description of the function: https://www.sfu.ca/~ssurjano/shekel.html +""" + +import functools + +import numpy as np +from deephyper.hpo import HpProblem + +from deephyper_benchmark import HPOBenchmark, HPOScorer + +from .utils import run_function + + +def shekel(x): + """Shekel benchmark function.""" + m = 10 + beta = 0.1 * np.array([1, 2, 2, 4, 4, 6, 3, 7, 5, 5]).T + + C = np.array( + [ + [4, 1, 8, 6, 3, 2, 5, 8, 6, 7], + [4, 1, 8, 6, 7, 9, 3, 1, 2, 3.6], + [4, 1, 8, 6, 3, 2, 5, 8, 6, 7], + [4, 1, 8, 6, 7, 9, 3, 1, 2, 3.6], + ] + ) + + y = -sum([1 / (np.sum((x - C[:, i]) ** 2) + beta[i]) for i in range(m)]) + return -y + + +class ShekelScorer(HPOScorer): + """Define performance evaluators for the Shekel problem.""" + + def __init__(self, nparams=4): + self.nparams = nparams + self.x_max = np.full(self.nparams, 4) + self.y_max = 10.5364 + + +class ShekelBenchmark(HPOBenchmark): + """Shekel benchmark.""" + + def __init__(self): + """Create a Shekel benchmark.""" + self.nparams = 4 + + @property + def problem(self): + """Define the hyperparameter problem.""" + domain = (0.0, 10.0) + problem = HpProblem() + for i in range(self.nparams): + problem.add_hyperparameter(domain, f"x{i}") + return problem + + @property + def run_function(self): + """Provide the run function for the hyperparameter benchmark.""" + return functools.partial(run_function, bb_func=shekel) + + @property + def scorer(self): + """Provide the scorer for the hyperparameter benchmark.""" + return ShekelScorer(self.nparams) diff --git a/src/deephyper_benchmark/benchmarks/cbbo/utils.py b/src/deephyper_benchmark/benchmarks/cbbo/utils.py new file mode 100644 index 0000000..b7c3233 --- /dev/null +++ b/src/deephyper_benchmark/benchmarks/cbbo/utils.py @@ -0,0 +1,22 @@ +"""here.""" + +import numpy as np +import time + +from deephyper.evaluator import profile +from deephyper.evaluator import RunningJob + + +@profile +def run_function(job: RunningJob, bb_func, sleep=False, sleep_mean=60, sleep_noise=20) -> dict: # noqa: D103 + config = job.parameters + + if sleep: + t_sleep = np.random.normal(loc=sleep_mean, scale=sleep_noise) + t_sleep = max(t_sleep, 0) + time.sleep(t_sleep) + + x = np.array([config[k] for k in config if "x" in k]) + x = np.asarray_chkfinite(x) # ValueError if any NaN or Inf + + return bb_func(x) diff --git a/src/deephyper_benchmark/benchmarks/dtlz/README.md b/src/deephyper_benchmark/benchmarks/dtlz/README.md new file mode 100644 index 0000000..5c09f18 --- /dev/null +++ b/src/deephyper_benchmark/benchmarks/dtlz/README.md @@ -0,0 +1,130 @@ + +# Modified Multiobjective DTLZ Test Suite + +✅ **Benchmark ready to be used.** + +This module contains objective function implementations of the DTLZ test +suite, derived from the implementations in +[ParMOO](https://github.com/parmoo/parmoo). + +------------------------------------------------------------------------------ + +For further reference, the DTLZ test suite was originally proposed in: + + Deb, Thiele, Laumanns, and Zitzler. "Scalable test problems for + evolutionary multiobjective optimization" in Evolutionary Multiobjective + Optimization, Theoretical Advances and Applications, Ch. 6 (pp. 105--145). + Springer-Verlag, London, UK, 2005. Abraham, Jain, and Goldberg (Eds). + +The original implementation was appropriate for testing randomized algorithms, +but for many deterministic algorithms, the global solutions represent either +best- or worst-case scenarios, so an configurable offset was introduced in: + + Chang. "Mathematical Software for Multiobjective Optimization Problems." + Ph.D. dissertation, Virginia Tech, Dept. of Computer Science, 2020. + +Note that the DTLZ problems are minimization problems. Since DeepHyper +maximizes, the implementation herein returns the negative value for each of +the DTLZ objectives. + +Our performance evaluator ``metrics`` scripts can evaluate either the +positive or negative solutions to estimate how well we have solved the +problem. + +------------------------------------------------------------------------------ + +The full list of public classes in this module includes the 7 unconstrained +DTLZ problems + * ``dtlz1``, + * ``dtlz2``, + * ``dtlz3``, + * ``dtlz4``, + * ``dtlz5``, + * ``dtlz6``, and + * ``dtlz7`` + +which are selected by setting the environment variable +``DEEPHYPER_BENCHMARK_DTLZ_PROB``. + +## Installation + +To use the benchmark follow this example set of instructions: + +```python + +# Set DTLZ problem environment variables before loading +import os +os.environ["DEEPHYPER_BENCHMARK_NDIMS"] = "5" # 5 vars +os.environ["DEEPHYPER_BENCHMARK_NOBJS"] = "3" # 2 objs +os.environ["DEEPHYPER_BENCHMARK_DTLZ_PROB"] = "2" # DTLZ2 problem +os.environ["DEEPHYPER_BENCHMARK_DTLZ_OFFSET"] = "0.6" # soln [x_o, .., x_n]=0.6 + +# Load DTLZ benchmark suite +import deephyper_benchmark as dhb +dhb.load("DTLZ") + +# Example of running one evaluation of DTLZ problem +from deephyper.evaluator import RunningJob +config = dtlz.hpo.problem.default_configuration # get a default config to test +res = dtlz.hpo.run(RunningJob(parameters=config)) + +``` + +## Configuration + +To configure the problem, set the following: + +- Environment variable `DEEPHYPER_BENCHMARK_PROB` with a value of `1`, `2`, ... `7` to select the DTLZ problem to run. Defaults to `2`. +- Environment variable `DEEPHYPER_BENCHMARK_NDIMS` with an integer value to set the number of input variables. Defaults to `5`. +- Environment variable `DEEPHYPER_BENCHMARK_NOBJS` with an integer value to set the number of objectives. Defaults to `2` +- Environment variable `DEEPHYPER_BENCHMARK_OFFSET` with a value between `0.0` and `1.0` to select the offset of the solution to the DTLZ problem. Defaults to `0.5` for DTLZ1, ..., DTLZ5 and `0.0` for DTLZ6 and DTLZ7. One may wish to adjust this value when comparing against deterministic blackbox solvers, which may sample the center and boundaries of the input space on specific schedules. +- Environment variable `DEEPHYPER_BENCHMARK_FAILURES` with value `0` or `1` to activate or deactivate failures. Defaults to `0`. + +## Metadata + +Since these problems are analytic, there is no metadata for this problem +beyond the standard DeepHyper metadata (timestamp information). + +## Evaluating Results + +Evaluating the performance of a multiobjective solver is nontrivial. +Typically, one should evaluate on two orthogonal bases: + 1. Quality of solutions -- What is the (average) error in the solutions + returned by the solver? + 2. Diversity of solutions -- How much of the true Pareto front is covered + by these solutions? + +To evaluate these two metrics, we use: + 1. Improved generational distance (GD+): Let $F_i$ be a point in the solution + set returned by a solver, + and let $Y_i$ be the nearest point to $F_i$ on the true Pareto front, + for $i=1,\ldots, n$. + Then the GD+ is $\sum_{i} D^+(F_i, Y_i) / n$. + Where $D^+$ denotes the improved distance function + $D^+(F, Y) = ||\max(F - Y, 0)||_2^2$, where the "max" is taken + componentwise. + **Note that this metric may not increase monotonically. Additionally, + it may be impossible to calculate for an arbitrary blackbox function, + and can only be calculated here since the solution known and easily + expressed algebraically for all of the DTLZ problems.** + 2. Hypervolume dominated: Let $F_i$ be defined as above, and let $R$ be + a pre-determined reference point such that all $F_i$ dominate $R$. + Then the hypervolume is given by the volume of the union of all + hyperboxes $B_i$ whose largest vertex is $F_i$ and smallest vertex + is $R$. The value (and usefulness) of the hypervolume metric is extremely + sensitive to the choice of $R$. Therefore, for this problem, we choose + $R$ to be the Nadir point for the true Pareto front. **Note that in order + to use the Nadir point as the reference point, we must throw out every + solution returned by the solver that is worse than the Nadir point. For + extremely difficult problems, this can result in zero hypervolume if no + solutions better than the Nadir point were found. This is most common + for DTLZ1, DTLZ3, and DTLZ7.** + +For a general problem, the two metrics listed above could be very difficult +to compute and many researchers will use the hypervolume with an overly +pessimistic reference point as a proxy for both quality and diversity. +However, in general, the hypervolume tends to promote diversity over quality. +For the DTLZ problems, since the shape of the true Pareto front is known, +we can calculate each of these metrics, and both the ``gdPlus(results)`` and +``hypervolume(results)`` functions are implemented in the ``dtlz.metrics`` +module. diff --git a/src/deephyper_benchmark/benchmarks/dtlz/__init__.py b/src/deephyper_benchmark/benchmarks/dtlz/__init__.py new file mode 100644 index 0000000..13c7f55 --- /dev/null +++ b/src/deephyper_benchmark/benchmarks/dtlz/__init__.py @@ -0,0 +1,5 @@ +"""here.""" + +from .dtlz import DTLZBenchmark + +__all__ = ["DTLZBenchmark"] diff --git a/src/deephyper_benchmark/benchmarks/dtlz/dtlz.py b/src/deephyper_benchmark/benchmarks/dtlz/dtlz.py new file mode 100644 index 0000000..08f4f21 --- /dev/null +++ b/src/deephyper_benchmark/benchmarks/dtlz/dtlz.py @@ -0,0 +1,250 @@ +"""here.""" + +import functools +import time +from typing import Optional + +import numpy as np +from deephyper.evaluator import RunningJob, profile +from deephyper.hpo import HpProblem +from deephyper.skopt.moo import pareto_front + +from deephyper_benchmark import HPOBenchmark, MultiObjHPOScorer + +from . import model as dtlz + + +@profile +def run_function( # noqa: D103 + job: RunningJob, + dtlz_obj, + nobj: int, + with_failures=False, + sleep=False, + sleep_mean=60, + sleep_noise=20, +) -> dict: + config = job.parameters + + if sleep: + t_sleep = np.random.normal(loc=sleep_mean, scale=sleep_noise) + t_sleep = max(t_sleep, 0) + time.sleep(t_sleep) + + x = np.array([config[k] for k in config if "x" in k]) + x = np.asarray_chkfinite(x) # ValueError if any NaN or Inf + ff = [-fi for fi in dtlz_obj(x)] + + if with_failures: + if any(xi < 0.25 for xi in x[nobj - 1 :]): + ff = ["F" for _ in ff] + + return ff + + +class DTLZScorer(MultiObjHPOScorer): + """A class defining performance evaluators for the DTLZ problems. + + Contains the following public methods: + + * `hypervolume(pts)` calculates the total hypervolume dominated by + the current solution, using the Nadir point as the reference point + and filtering out solutions that do not dominate the Nadir point, + * `nadirPt()` calculates the Nadir point for the current problem, + * `numPts(pts)` calculates the number of solution points that dominate + the Nadir point, and + * `gdPlus(pts)` calculates the RMSE where the error in each point is + approximated by the 2-norm distance to the nearest solution point. + + """ + + def __init__(self, prob_id: int, nobj: int): + """Read the current DTLZ problem defn from environment vars.""" + super().__init__(nobj) + self.prob_id = prob_id + + @property + def nadir_point(self): + """Calculate the Nadir point for the given problem definition.""" + if self.prob_id == 1: + return np.ones(self.nobj) * 0.5 + elif self.prob_id in [2, 3, 4, 5, 6]: + return np.ones(self.nobj) + elif self.prob_id == 7: + nadir = np.ones(self.nobj) + nadir[self.nobj - 1] = self.nobj * 2.0 + return nadir + else: + raise ValueError(f"DTLZ{self.prob_id} is not a valid problem") + + def num_points_dominate_nadir(self, pts): + """Calculate the number of solutions that dominate the Nadir point. + + Args: + pts (numpy.ndarra): A 2d array of objective values. + Each row is an objective value in the solution set. + + Returns: + int: The number of fi in pts such that all(fi < self.nadirPt). + + """ + if np.any(pts < 0): + pareto_pts = pareto_front(-pts) + else: + pareto_pts = pareto_front(pts) + return sum([all(fi <= self.nadir_point) for fi in pareto_pts]) + + def gdplus_score(self, pts): + """Calculate the p=1 generational distance for a given solution set. + + Args: + pts (numpy.ndarra): A 2d array of objective values. + Each row is an objective value in the solution set. + + Returns: + float: The p=1 generational distance over all points in pts. + + """ + if np.any(pts < 0): + pareto_pts = pareto_front(-pts) + else: + pareto_pts = pareto_front(pts) + if self.prob_id == "1": + dists = self._dtlz1Dist(pareto_pts) + elif self.prob_id in ["2", "3", "4", "5", "6"]: + dists = self._dtlz2Dist(pareto_pts) + elif self.prob_id == "7": + dists = self._dtlz7Dist(pareto_pts) + else: + raise ValueError(f"DTLZ{self.prob_id} is not a valid problem") + return np.sum(dists) / len(dists) + + def _dtlz1Dist(self, pts): + """Calculate the d+ from each fi to the nearest solution in DTLZ1. + + Args: + pts (numpy.ndarra): A 2d array of objective values. + Each row is an objective value in the solution set. + + Returns: + numpy.ndarray: A 1d array of distances to the nearest solution + point for DTLZ1. + """ + return np.array([np.linalg.norm(np.maximum(fi - (0.5 * fi / np.sum(fi)), 0)) for fi in pts]) + + def _dtlz2Dist(self, pts): + """Calculate the d+ from each fi to the nearest point on unit sphere. + + Note: Works for DTLZ2-6 + + Args: + pts (numpy.ndarra): A 2d array of objective values. + Each row is an objective value in the solution set. + + Returns: + numpy.ndarray: A 1d array of distances to the surface of the + unit sphere. + + """ + return np.array( + [np.linalg.norm(np.maximum(fi - (fi / np.linalg.norm(fi)), 0)) for fi in pts] + ) + + def _dtlz7Dist(self, pts): + """Calculate the d+ from each fi to the nearest soln in DTLZ7. + + Args: + pts (numpy.ndarra): A 2d array of objective values. + Each row is an objective value in the solution set. + + Returns: + numpy.ndarray: A 1d array of distances to the nearest solution + point to DTLZ7. + + """ + # Project each point onto DTLZ7 solution and calculate difference + pts_proj = [] + for fi in pts: + gx = 2.0 + hx = -np.sum( + fi[: self.nobj - 1] * (1.0 + np.sin(3.0 * np.pi * fi[: self.nobj - 1])) / gx + ) + float(self.nobj) + pts_proj.append(gx * hx) + return np.array([np.abs(np.maximum(fi[-1] - fj, 0)) for fi, fj in zip(pts, pts_proj)]) + + def gdplus(self, y: np.ndarray) -> np.ndarray: + """Compute the regret of a list of given solution. + + Args: + y (np.ndarray): An array of solutions. + + Returns: + np.ndarray: An array of regret values. + """ + scores = [] + for i in range(1, y.shape[0] + 1): + scores.append(self.gdplus_score(y[:i])) + scores = np.asarray(scores) + return scores + + +class DTLZBenchmark(HPOBenchmark): + """DTLZ benchmark. + + Args: + nparams (int, optional): the number of parameters in the problem. + offset (int, optional): the offset in the space of parameters. + nslack (int, optional): the number of additional slack parameters in the problem. + """ + + def __init__( + self, + prob_id: int = 2, + nparams: int = 5, + nobj: int = 2, + offset: Optional[int] = None, + with_failures: bool = False, + ) -> None: + self.nparams = nparams + self.nobj = nobj + self.offset = offset + self.with_failures = with_failures + # Read DTLZ problem name and acquire pointer + + self.prob_id = prob_id + self.prob_name = f"dtlz{self.prob_id}" + self.dtlz_class = getattr(dtlz, self.prob_name) + + # Read problem dims and definition (or read from ENV) + if self.prob_name in ["dtlz1", "dtlz2", "dtlz3", "dtlz4", "dtlz5"]: + if self.offset is None: + self.offset = 0.5 + elif self.prob_name in ["dtlz6", "dtlz7"]: + if self.offset is None: + self.offset = 0.0 + else: + raise ValueError(f"Invalid problem {self.prob_name}") + + self.dtlz_obj = self.dtlz_class(self.nparams, self.nobj, offset=self.offset) + + @property + def problem(self): # noqa: D102 + problem = HpProblem() + domain = (0.0, 1.0) + for i in range(self.nparams): + problem.add_hyperparameter(domain, f"x{i}") + return problem + + @property + def run_function(self): # noqa: D102 + run_function_ = functools.partial( + run_function, + dtlz_obj=self.dtlz_obj, + nobj=self.nobj, + with_failures=self.with_failures, + ) + return run_function_ + + @property + def scorer(self): # noqa: D102 + return DTLZScorer(self.prob_id, self.nobj) diff --git a/src/deephyper_benchmark/benchmarks/dtlz/model.py b/src/deephyper_benchmark/benchmarks/dtlz/model.py new file mode 100644 index 0000000..f7f22f4 --- /dev/null +++ b/src/deephyper_benchmark/benchmarks/dtlz/model.py @@ -0,0 +1,591 @@ +"""Objective function implementations of the DTLZ test suite. + +Derived from the implementations in ParMOO: + +Chang and Wild. "ParMOO: A Python library for parallel multiobjective +simulation optimization." Journal of Open Source Software 8(82):4468, 2023. + +------------------------------------------------------------------------------ + +For further references, the DTLZ test suite was originally proposed in: + +Deb, Thiele, Laumanns, and Zitzler. "Scalable test problems for +evolutionary multiobjective optimization" in Evolutionary Multiobjective +Optimization, Theoretical Advances and Applications, Ch. 6 (pp. 105--145). +Springer-Verlag, London, UK, 2005. Abraham, Jain, and Goldberg (Eds). + +The original implementation was appropriate for testing randomized algorithms, +but for many deterministic algorithms, the global solutions represent either +best- or worst-case scenarios, so an configurable offset was introduced in: + +Chang. "Mathematical Software for Multiobjective Optimization Problems." +Ph.D. dissertation, Virginia Tech, Dept. of Computer Science, 2020. + +------------------------------------------------------------------------------ + +The full list of public classes in this module includes the 7 unconstrained +DTLZ problems: + * ``dtlz1`` + * ``dtlz2`` + * ``dtlz3`` + * ``dtlz4`` + * ``dtlz5`` + * ``dtlz6`` + * ``dtlz7`` + +""" + +import numpy as np + + +class __dtlz_base__: + """Base class implements re-used constructor. + + Constructor for all DTLZ classes. + + Args: + num_des (int): The number of design variables. + + num_obj (int, optional): The number of objectives. + + offset (optional, float): The location of the global minimizers + is x_i = offset for i = num_objectives, ..., num_des. + The default offset is 0.5. + + """ + + def __init__(self, num_des, num_obj=3, offset=0.5): + self.n = num_des + self.o = num_obj + self.offset = offset + return + + def __call__(self, x): + raise NotImplementedError("The call method must be implemented...") + + +class __g1__(__dtlz_base__): + """Class defining 1 of 4 kernel functions used in the DTLZ problem suite. + + g1 = 100 ( (n - o + 1) + + sum_{i=o}^n ((x_i - offset)^2 - cos(20pi(x_i - offset))) ) + + Contains 2 methods: + * ``__init__(num_des, num_obj)`` + * ``__call__(x)`` + + The ``__init__`` method creates a new kernel. + + The ``__call__`` method performs an evaluation of the g1 kernel. + + """ + + def __call__(self, x): + """Define objective evaluation. + + Args: + x (numpy.array): A numpy.ndarray containing the design point + to evaluate. + + Returns: + float: The output of this objective for the input x. + + """ + return ( + 1 + + self.n + - self.o + + np.sum( + (x[self.o - 1 : self.n] - self.offset) ** 2 + - np.cos(20.0 * np.pi * (x[self.o - 1 : self.n] - self.offset)) + ) + ) * 100.0 + + +class __g2__(__dtlz_base__): + """Class defining 2 of 4 kernel functions used in the DTLZ problem suite. + + g2 = (x_o - offset)^2 + ... + (x_n - offset)^2 + + Contains 2 methods: + * ``__init__(num_des, num_obj)`` + * ``__call__(x)`` + + The ``__init__`` method inherits from the __dtlz_base__ ABC. + + The ``__call__`` method performs an evaluation of the g2 problem. + + """ + + def __call__(self, x): + """Define objective evaluation. + + Args: + x (numpy.array): A numpy.ndarray containing the design point + to evaluate. + + Returns: + float: The output of this objective for the input x. + + """ + return np.sum((x[self.o - 1 : self.n] - self.offset) ** 2) + + +class __g3__(__dtlz_base__): + """Class defining 3 of 4 kernel functions used in the DTLZ problem suite. + + g3 = |x_o - offset|^.1 + ... + |x_n - offset|^.1 + + Contains 2 methods: + * ``__init__(num_des, num_obj)`` + * ``__call__(x)`` + + The ``__init__`` method inherits from the __dtlz_base__ ABC. + + The ``__call__`` method performs an evaluation of the g3 problem. + + """ + + def __init__(self, num_des, num_obj=3, offset=0.0): + """Constructor for g3, with modified default offset. + + Args: + num_des (int): The number of design variables. + + num_obj (int, optional): The number of objectives. + + offset (optional, float): The location of the global minimizers + is x_i = offset for i = num_objectives, ..., num_des. + The default offset is 0.0. + + """ + super().__init__(num_des=num_des, num_obj=num_obj, offset=offset) + return + + def __call__(self, x): + """Define objective evaluation. + + Args: + x (numpy.array): A numpy.ndarray containing the design point + to evaluate. + + Returns: + float: The output of this objective for the input x. + + """ + return np.sum(np.abs(x[self.o - 1 : self.n] - self.offset) ** 0.1) + + +class __g4__(__dtlz_base__): + """Class defining 4 of 4 kernel functions used in the DTLZ problem suite. + + g4 = 1 + (9 * (|x_o - offset| + ... + |x_n - offset|) / (n + 1 - o)) + + Contains 2 methods: + * ``__init__(num_des, num_obj)`` + * ``__call__(x)`` + + The ``__init__`` method inherits from the __dtlz_base__ ABC. + + The ``__call__`` method performs an evaluation of the g4 problem. + + """ + + def __init__(self, num_des, num_obj=3, offset=0.0): + """Constructor for g4, with modified default offset. + + Args: + num_des (int): The number of design variables. + + num_obj (int, optional): The number of objectives. + + offset (optional, float): The location of the global minimizers + is x_i = offset for i = num_objectives, ..., num_des. + The default offset is 0.0. + + """ + super().__init__(num_des=num_des, num_obj=num_obj, offset=offset) + return + + def __call__(self, x): + """Define objective evaluation. + + Args: + x (numpy.array): A numpy.ndarray containing the design point + to evaluate. + + Returns: + float: The output of this objective for the input x. + + """ + return ( + 9 * np.sum(np.abs(x[self.o - 1 : self.n] - self.offset)) / float(self.n + 1 - self.o) + ) + 1.0 + + +class dtlz1(__dtlz_base__): + """Class defining the DTLZ1 problem with offset minimizer. + + DTLZ1 has a linear Pareto front, with all nondominated points + on the hyperplane F_1 + F_2 + ... + F_o = 0.5. + DTLZ1 has 11^k - 1 "local" Pareto fronts where k = n - o + 1, and + 1 "global" Pareto front. + + Contains 2 methods: + * ``__init__(num_des, num_obj)`` + * ``__call__(x)`` + + The ``__init__`` method inherits from the __dtlz_base__ ABC. + + The ``__call__`` method performs an evaluation of the DTLZ1 problem. + + """ + + def __call__(self, x): + """Define objective evaluation. + + Args: + x (numpy.ndarray): A numpy.ndarray containing the design point + to evaluate. + + Returns: + numpy float array: The output of this objective for the input x. + + """ + # Initialize kernel function + ker = __g1__(self.n, self.o, self.offset) + # Initialize output array + fx = np.zeros(self.o) + fx[:] = (1.0 + ker(x)) / 2.0 + # Calculate the output array + for i in range(self.o): + for j in range(self.o - 1 - i): + fx[i] *= x[j] + if i > 0: + fx[i] *= 1.0 - x[self.o - 1 - i] + return fx + + +class dtlz2(__dtlz_base__): + """Class defining the DTLZ2 problem with offset minimizer. + + DTLZ2 has a concave Pareto front, given by the unit sphere in + objective space, restricted to the positive orthant. + DTLZ2 has no "local" Pareto fronts, besides the true Pareto front. + + Contains 2 methods: + * ``__init__(num_des, num_obj)`` + * ``__call__(x)`` + + The ``__init__`` method inherits from the __dtlz_base__ ABC. + + The ``__call__`` method performs an evaluation of the DTLZ2 problem. + + """ + + def __call__(self, x): + """Define objective evaluation. + + Args: + x (numpy.ndarray): A numpy.ndarray containing the design point + to evaluate. + + Returns: + numpy float array: The output of this objective for the input x. + + """ + # Initialize kernel function + ker = __g2__(self.n, self.o, self.offset) + # Initialize output array + fx = np.zeros(self.o) + fx[:] = 1.0 + ker(x) + # Calculate the output array + for i in range(self.o): + for j in range(self.o - 1 - i): + fx[i] *= np.cos(np.pi * x[j] / 2) + if i > 0: + fx[i] *= np.sin(np.pi * x[self.o - 1 - i] / 2) + return fx + + +class dtlz3(__dtlz_base__): + """Class defining the DTLZ3 problem with offset minimizer. + + DTLZ3 has a concave Pareto front, given by the unit sphere in + objective space, restricted to the positive orthant. + DTLZ3 has 3^k - 1 "local" Pareto fronts where k = n - o + 1, and + 1 "global" Pareto front. + + Contains 2 methods: + * ``__init__(num_des, num_obj)`` + * ``__call__(x)`` + + The ``__init__`` method inherits from the __dtlz_base__ ABC. + + The ``__call__`` method performs an evaluation of the DTLZ3 problem. + + """ + + def __call__(self, x): + """Define objective evaluation. + + Args: + x (numpy.ndarray): A numpy.ndarray containing the design point + to evaluate. + + Returns: + numpy float array: The output of this objective for the input x. + + """ + # Initialize kernel function + ker = __g1__(self.n, self.o, self.offset) + # Initialize output array + fx = np.zeros(self.o) + fx[:] = 1.0 + ker(x) + # Calculate the output array + for i in range(self.o): + for j in range(self.o - 1 - i): + fx[i] *= np.cos(np.pi * x[j] / 2) + if i > 0: + fx[i] *= np.sin(np.pi * x[self.o - 1 - i] / 2) + return fx + + +class dtlz4(__dtlz_base__): + """Class defining the DTLZ4 problem with offset minimizer. + + DTLZ4 has a concave Pareto front, given by the unit sphere in + objective space, restricted to the positive orthant. + DTLZ4 has no "local" Pareto fronts, besides the true Pareto front, + but by tuning the optional parameter alpha, one can adjust the + solution density, making it harder for MOO algorithms to produce + a uniform distribution of solutions. + + Contains 2 methods: + * ``__init__(num_des, num_obj)`` + * ``__call__(x)`` + + The ``__init__`` method inherits from the __dtlz_base__ ABC. + + The ``__call__`` method performs an evaluation of the DTLZ4 problem. + + """ + + def __init__(self, num_des, num_obj=3, offset=0.5, alpha=100.0): + """Constructor for DTLZ7, with modified default offset. + + Args: + num_des (int): The number of design variables. + + num_obj (int, optional): The number of objectives. + + offset (optional, float): The location of the global minimizers + is x_i = offset for i = num_objectives, ..., num_des. + The default offset is 0.0. + + alpha (optional, float or int): The uniformity parameter used for + controlling the uniformity of the distribution of solutions + across the Pareto front. Must be greater than or equal to 1. + A value of 1 results in DTLZ2. Default value is 100.0. + + """ + super().__init__(num_des=num_des, num_obj=num_obj, offset=offset) + self.alpha = alpha + return + + def __call__(self, x): + """Define objective evaluation. + + Args: + x (numpy.ndarray): A numpy.ndarray containing the design point + to evaluate. + + Returns: + numpy float array: The output of this objective for the input x. + + """ + # Initialize kernel function + ker = __g2__(self.n, self.o, self.offset) + # Initialize output array + fx = np.zeros(self.o) + fx[:] = 1.0 + ker(x) + # Calculate the output array + for i in range(self.o): + for j in range(self.o - 1 - i): + fx[i] *= np.cos(np.pi * x[j] ** self.alpha / 2) + if i > 0: + fx[i] *= np.sin(np.pi * x[self.o - 1 - i] ** self.alpha / 2) + return fx + + +class dtlz5(__dtlz_base__): + """Class defining the DTLZ5 problem with offset minimizer. + + DTLZ5 has a lower-dimensional Pareto front embedded in the objective + space, given by an arc of the unit sphere in the positive orthant. + DTLZ5 has no "local" Pareto fronts, besides the true Pareto front. + + Contains 2 methods: + * ``__init__(num_des, num_obj)`` + * ``__call__(x)`` + + The ``__init__`` method inherits from the __dtlz_base__ ABC. + + The ``__call__`` method performs an evaluation of the DTLZ5 problem. + + """ + + def __call__(self, x): + """Define objective evaluation. + + Args: + x (numpy.ndarray): A numpy.ndarray containing the design point + to evaluate. + + Returns: + numpy float array: The output of this objective for the input x. + + """ + # Initialize kernel function + ker = __g2__(self.n, self.o, self.offset) + # Calculate theta values + theta = np.zeros(self.o) + g2x = ker(x) + theta[0] = x[0] + for i in range(1, self.o): + theta[i] = (1 + 2 * g2x * x[i]) / (2 * (1 + g2x)) + # Initialize output array + fx = np.zeros(self.o) + fx[:] = 1.0 + g2x + # Calculate the output array + for i in range(self.o): + for j in range(self.o - 1 - i): + fx[i] *= np.cos(np.pi * theta[j] / 2) + if i > 0: + fx[i] *= np.sin(np.pi * theta[self.o - 1 - i] / 2) + return fx + + +class dtlz6(__dtlz_base__): + """Class defining the DTLZ6 problem with offset minimizer. + + DTLZ6 has a lower-dimensional Pareto front embedded in the objective + space, given by an arc of the unit sphere in the positive orthant. + DTLZ6 has no "local" Pareto fronts, but tends to show very little + improvement until the algorithm is very close to its solution set. + + Contains 2 methods: + * ``__init__(num_des, num_obj)`` + * ``__call__(x)`` + + The ``__init__`` method inherits from the __dtlz_base__ ABC. + + The ``__call__`` method performs an evaluation of the DTLZ6 problem. + + """ + + def __init__(self, num_des, num_obj=3, offset=0.0): + """Constructor for DTLZ6, with modified default offset. + + Args: + num_des (int): The number of design variables. + + num_obj (int, optional): The number of objectives. + + offset (optional, float): The location of the global minimizers + is x_i = offset for i = num_objectives, ..., num_des. + The default offset is 0.0. + + """ + super().__init__(num_des=num_des, num_obj=num_obj, offset=offset) + return + + def __call__(self, x): + """Define objective evaluation. + + Args: + x (numpy.ndarray): A numpy.ndarray containing the design point + to evaluate. + + Returns: + numpy float array: The output of this objective for the input x. + + """ + # Initialize kernel function + ker = __g3__(self.n, self.o, self.offset) + # Calculate theta values + theta = np.zeros(self.o) + g3x = ker(x) + theta[0] = x[0] + for i in range(1, self.o): + theta[i] = (1 + 2 * g3x * x[i]) / (2 * (1 + g3x)) + # Initialize output array + fx = np.zeros(self.o) + fx[:] = 1.0 + g3x + # Calculate the output array + for i in range(self.o): + for j in range(self.o - 1 - i): + fx[i] *= np.cos(np.pi * theta[j] / 2) + if i > 0: + fx[i] *= np.sin(np.pi * theta[self.o - 1 - i] / 2) + return fx + + +class dtlz7(__dtlz_base__): + """Class defining the DTLZ7 problem with offset minimizer. + + DTLZ7 has a discontinuous Pareto front, with solutions on the + 2^(o-1) discontinuous nondominated regions of the surface: + + F_m = o - F_1 (1 + sin(3pi F_1)) - ... - F_{o-1} (1 + sin3pi F_{o-1}). + + Contains 2 methods: + * ``__init__(num_des, num_obj)`` + * ``__call__(x)`` + + The ``__init__`` method inherits from the __dtlz_base__ ABC. + + The ``__call__`` method performs an evaluation of the DTLZ7 problem. + + """ + + def __init__(self, num_des, num_obj=3, offset=0.0): + """Constructor for DTLZ7, with modified default offset. + + Args: + num_des (int): The number of design variables. + + num_obj (int, optional): The number of objectives. + + offset (optional, float): The location of the global minimizers + is x_i = offset for i = num_objectives, ..., num_des. + The default offset is 0.0. + + """ + super().__init__(num_des=num_des, num_obj=num_obj, offset=offset) + return + + def __call__(self, x): + """Define objective evaluation. + + Args: + x (numpy.ndarray): A numpy.ndarray containing the design point + to evaluate. + + Returns: + numpy float array: The output of this objective for the input x. + + """ + # Initialize kernel function + ker = __g4__(self.n, self.o, self.offset) + # Initialize first o-1 entries in the output array + fx = np.zeros(self.o) + fx[: self.o - 1] = x[: self.o - 1] + # Calculate kernel functions + gx = 1.0 + ker(x) + hx = -np.sum(x[: self.o - 1] * (1.0 + np.sin(3.0 * np.pi * x[: self.o - 1])) / gx) + float( + self.o + ) + # Calculate the last entry in the output array + fx[self.o - 1] = gx * hx + return fx diff --git a/src/deephyper_benchmark/lib/__init__.py b/src/deephyper_benchmark/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/deephyper_benchmark/lib/autotuning/Mochi/HEPnOS/Makefile b/src/deephyper_benchmark/lib/autotuning/Mochi/HEPnOS/Makefile new file mode 100644 index 0000000..beec148 --- /dev/null +++ b/src/deephyper_benchmark/lib/autotuning/Mochi/HEPnOS/Makefile @@ -0,0 +1,11 @@ + +VERSION=0.0.1 + +build: + mkdir -p build + wget https://github.com/hepnos/HEPnOS-Autotuning-analysis/archive/refs/heads/main.tar.gz -O build/hepnos-autotuning-analysis.tar.gz + tar -xzvf build/hepnos-autotuning-analysis.tar.gz -C build + rm -rf build/hepnos-autotuning-analysis.tar.gz + +clean: + rm -rf build/ \ No newline at end of file diff --git a/src/deephyper_benchmark/lib/autotuning/Mochi/HEPnOS/__init__.py b/src/deephyper_benchmark/lib/autotuning/Mochi/HEPnOS/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/deephyper_benchmark/lib/autotuning/Mochi/HEPnOS/benchmark.py b/src/deephyper_benchmark/lib/autotuning/Mochi/HEPnOS/benchmark.py new file mode 100644 index 0000000..4727358 --- /dev/null +++ b/src/deephyper_benchmark/lib/autotuning/Mochi/HEPnOS/benchmark.py @@ -0,0 +1,19 @@ +import os + +from deephyper_benchmark import * + +DIR = os.path.dirname(os.path.abspath(__file__)) + + +class HEPnOS(Benchmark): + """ + - Experimental data from the HEPnOS paper: https://github.com/hepnos/HEPnOS-Autotuning-analysis + - Experimental code from the HEPnOS paper: https://github.com/hepnos/HEPnOS-Autotuning/tree/dev-new-hepnos + - Paper from M. Dorier and R. Egele et al. "HPC Storage Service Autotuning Using Variational-Autoencoder-Guided Asynchronous Bayesian Optimization". https://arxiv.org/pdf/2210.00798 + """ + + version = "0.0.1" + + requires = { + "makefile": {"step": "install", "type": "cmd", "cmd": "make build"}, + } diff --git a/src/deephyper_benchmark/lib/autotuning/Mochi/HEPnOS/hpo.py b/src/deephyper_benchmark/lib/autotuning/Mochi/HEPnOS/hpo.py new file mode 100644 index 0000000..c5ed94d --- /dev/null +++ b/src/deephyper_benchmark/lib/autotuning/Mochi/HEPnOS/hpo.py @@ -0,0 +1,281 @@ +import os +import time + +import numpy as np +import pandas as pd +from deephyper.analysis.hpo import filter_failed_objectives +from deephyper.evaluator import RunningJob, profile +from deephyper.hpo import HpProblem +from sklearn.compose import ColumnTransformer +from sklearn.neighbors import KNeighborsRegressor +from sklearn.preprocessing import OneHotEncoder, FunctionTransformer +from sklearn.pipeline import Pipeline + +BUILD_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "build") +DATA_DIR = os.path.join( + BUILD_DIR, "HEPnOS-Autotuning-analysis-main", "results", "theta" +) + +DEEPHYPER_BENCHMARK_HEPNOS_MODEL = os.environ.get( + "DEEPHYPER_BENCHMARK_HEPNOS_MODEL", "RAND" +) +DEEPHYPER_BENCHMARK_HEPNOS_SCALE = int( + os.environ.get("DEEPHYPER_BENCHMARK_HEPNOS_SCALE", "4") +) +DEEPHYPER_BENCHMARK_HEPNOS_DISABLE_PEP = bool( + os.environ.get("DEEPHYPER_BENCHMARK_HEPNOS_DISABLE_PEP", "0") +) +DEEPHYPER_BENCHMARK_HEPNOS_MORE_PARAMS = bool( + os.environ.get("DEEPHYPER_BENCHMARK_HEPNOS_MORE_PARAMS", "0") +) + +NUMERIC_VARIABLES = [] +CATEGORICAL_VARIABLES = [] + +problem = HpProblem() + + +def add_parameter(name, domain, value_type, default_value, description=""): + problem.add_hyperparameter(domain, name, default_value=default_value) + if value_type in [int, float, bool]: + NUMERIC_VARIABLES.append(name) + else: + CATEGORICAL_VARIABLES.append(name) + + +def create_problem(): + add_parameter( + "busy_spin", + [0, 1], + int, + 0, + "Whether Mercury should busy-spin instead of block", + ) + add_parameter( + "hepnos_progress_thread", + [0, 1], + int, + 0, + "Whether to use a dedicated progress thread in HEPnOS", + ) + add_parameter( + "hepnos_num_rpc_threads", + (0, 63), + int, + 0, + "Number of threads used for serving RPC requests", + ) + add_parameter( + "hepnos_num_event_databases", + (1, 16), + int, + 1, + "Number of databases per process used to store events", + ) + add_parameter( + "hepnos_num_product_databases", + (1, 16), + int, + 1, + "Number of databases per process used to store products", + ) + add_parameter( + "hepnos_num_providers", + (1, 32), + int, + 1, + "Number of database providers per process", + ) + add_parameter( + "hepnos_pool_type", + ["fifo", "fifo_wait"], + str, + "fifo_wait", + "Thread-scheduling policity used by Argobots pools", + ) + add_parameter( + "hepnos_pes_per_node", + [1, 2, 4, 8, 16, 32], + int, + 2, + "Number of HEPnOS processes per node", + ) + add_parameter( + "loader_progress_thread", + [0, 1], + int, + 0, + "Whether to use a dedicated progress thread in the Dataloader", + ) + add_parameter( + "loader_batch_size", + (1, 2048, "log-uniform"), + int, + 512, + "Size of the batches of events sent by the Dataloader to HEPnOS", + ) + add_parameter( + "loader_pes_per_node", + [1, 2, 4, 8, 16], + int, + 2, + "Number of processes per node for the Dataloader", + ) + if DEEPHYPER_BENCHMARK_HEPNOS_MORE_PARAMS: + add_parameter( + "loader_async", + [0, 1], + int, + 0, + "Whether to use the HEPnOS AsyncEngine in the Dataloader", + ) + add_parameter( + "loader_async_threads", + (1, 63, "log-uniform"), + int, + 1, + "Number of threads for the AsyncEngine to use", + ) + + if DEEPHYPER_BENCHMARK_HEPNOS_DISABLE_PEP: + return + + add_parameter( + "pep_progress_thread", + [0, 1], + int, + 0, + "Whether to use a dedicated progress thread in the PEP step", + ) + add_parameter( + "pep_num_threads", + (1, 31), + int, + 4, + "Number of threads used for processing in the PEP step", + ) + add_parameter( + "pep_ibatch_size", + (8, 1024, "log-uniform"), + int, + 128, + "Batch size used when PEP processes are loading events from HEPnOS", + ) + add_parameter( + "pep_obatch_size", + (8, 1024, "log-uniform"), + int, + 128, + "Batch size used when PEP processes are exchanging events among themselves", + ) + add_parameter( + "pep_pes_per_node", + [1, 2, 4, 8, 16, 32], + int, + 8, + "Number of processes per node for the PEP step", + ) + + if DEEPHYPER_BENCHMARK_HEPNOS_MORE_PARAMS: + add_parameter( + "pep_no_preloading", + [0, 1], + int, + False, + "Whether to disable product-preloading in PEP", + ) + add_parameter( + "pep_no_rdma", + [0, 1], + int, + 0, + "Whether to disable RDMA in PEP", + ) + + +create_problem() + +CSV_SUFFIX = f"{str(DEEPHYPER_BENCHMARK_HEPNOS_DISABLE_PEP).lower()}-{str(DEEPHYPER_BENCHMARK_HEPNOS_MORE_PARAMS).lower()}" +CSV_SUFFIX = f"{DEEPHYPER_BENCHMARK_HEPNOS_MODEL}-{DEEPHYPER_BENCHMARK_HEPNOS_SCALE}-{CSV_SUFFIX}" + + +def load_data(): + dataframes, dataframes_with_failures = [], [] + for i in range(1, 6): + csv_path = os.path.join( + DATA_DIR, + f"exp-{CSV_SUFFIX}-{i}.csv", + ) + df = pd.read_csv(csv_path, index_col=None, header=0) + df, df_with_failures = filter_failed_objectives(df) + dataframes.append(df) + dataframes_with_failures.append(df_with_failures) + + dataframes = pd.concat(dataframes, axis=0, ignore_index=True) + dataframes_with_failures = pd.concat( + dataframes_with_failures, axis=0, ignore_index=True + ) + + # The objective is -log(time) initialy so we convert it back to time + dataframes["objective"] = dataframes["objective"] = np.exp( + -dataframes["objective"].values.astype(np.float32) + ) + dataframes_with_failures["objective"] = -1 + + dataframes = pd.concat( + [dataframes, dataframes_with_failures], axis=0, ignore_index=True + ) + + return dataframes + + +def create_model(): + df = load_data() + names = problem.hyperparameter_names + + X = df.loc[:, names] + y = df["objective"].values + + transformer = ColumnTransformer( + [ + ( + "onehot", + OneHotEncoder(handle_unknown="ignore"), + CATEGORICAL_VARIABLES, + ), + ("passthrough", FunctionTransformer(), NUMERIC_VARIABLES), + ] + ) + + model = KNeighborsRegressor(n_neighbors=1) + + pipeline = Pipeline([("transformer", transformer), ("model", model)]) + pipeline.fit(X, y) + + return pipeline + + +model = create_model() + + +@profile +def run(job: RunningJob) -> dict: + config = job.parameters.copy() + + df = pd.DataFrame([config]) + y_pred = -model.predict(df)[0] + + if y_pred > 0: + y_pred = "F" + + return {"objective": y_pred} + + +if __name__ == "__main__": + print(problem) + df = load_data() + print(df) + default_config = problem.default_configuration + print(f"{default_config=}") + result = run(RunningJob(parameters=default_config)) + print(f"{result=}") diff --git a/src/deephyper_benchmark/lib/autotuning/Mochi/HEPnOS/metrics.py b/src/deephyper_benchmark/lib/autotuning/Mochi/HEPnOS/metrics.py new file mode 100644 index 0000000..bc09e5a --- /dev/null +++ b/src/deephyper_benchmark/lib/autotuning/Mochi/HEPnOS/metrics.py @@ -0,0 +1,39 @@ +import os + +import numpy as np + +from .hpo import load_data, problem + +class PerformanceEvaluator: + """A class defining performance evaluators for the HEPnOS problem.""" + + def __init__(self): + """Read the current problem defn from environment vars.""" + + df = load_data() + df = df[df["objective"] > 0] + idx_max = df["objective"].argmin() + self.x_min = df[problem.hyperparameter_names].iloc[idx_max].to_dict() + self.y_min = df.iloc[idx_max]["objective"] + + def simple_regret(self, y: np.ndarray) -> np.ndarray: + """Compute the regret of a list of given solution. + + Args: + y (np.ndarray): An array of solutions. + + Returns: + np.ndarray: An array of regret values. + """ + return y - self.y_min + + def cumul_regret(self, y: np.ndarray) -> np.ndarray: + """Compute the cumulative regret of an array of ordered given solution. + + Args: + y (np.ndarray): An array of solutions. + + Returns: + np.ndarray: An array of cumulative regret values. + """ + return np.cumsum(self.simple_regret(y)) diff --git a/src/deephyper_benchmark/lib/autotuning/Mochi/README.md b/src/deephyper_benchmark/lib/autotuning/Mochi/README.md new file mode 100644 index 0000000..3f4bb65 --- /dev/null +++ b/src/deephyper_benchmark/lib/autotuning/Mochi/README.md @@ -0,0 +1,13 @@ +# Mochi Benchmarks + +From the Mochi documentation: + +> The Mochi project is a collaboration between Argonne National Laboratory, Los Alamos National Laboratory, Carnegie Mellon University, and the HDF Group. The objective of this project is to explore a software defined storage approach for composing storage services that provides new levels of functionality, performance, and reliability for science applications at extreme scale. + +- Documentation: https://mochi.readthedocs.io/en/latest/ + +## Installation + +```console +python -c "import deephyper_benchmark as dhb; dhb.install('AutoTuning/Mochi/HEPnOS');" +``` \ No newline at end of file diff --git a/src/deephyper_benchmark/lib/autotuning/README.md b/src/deephyper_benchmark/lib/autotuning/README.md new file mode 100644 index 0000000..bd2e296 --- /dev/null +++ b/src/deephyper_benchmark/lib/autotuning/README.md @@ -0,0 +1,3 @@ +# AutoTuning Benchmarks + +AutoTuning corresponds to the automated configurations of software to improve performance. For example, it can be the configuration of a database, a compilator, or parallelism. \ No newline at end of file diff --git a/src/deephyper_benchmark/lib/c_bbo/ackley/hpo_benchmark.py b/src/deephyper_benchmark/lib/c_bbo/ackley/hpo_benchmark.py index 9ef0ac3..d144a9d 100644 --- a/src/deephyper_benchmark/lib/c_bbo/ackley/hpo_benchmark.py +++ b/src/deephyper_benchmark/lib/c_bbo/ackley/hpo_benchmark.py @@ -47,7 +47,7 @@ def __init__( offset=0, ): self.p_num = p_num - self.x_max = np.full(self.p_num, fill_value=-offset) + self.x_max = np.full(self.p_num, fill_value=0.0) self.x_max[p_num - p_num_slack :] = np.nan self.y_max = 0.0 diff --git a/src/deephyper_benchmark/lib/c_bbo/easom/hpo_benchmark.py b/src/deephyper_benchmark/lib/c_bbo/easom/hpo_benchmark.py index 4c0509c..df5c9ff 100644 --- a/src/deephyper_benchmark/lib/c_bbo/easom/hpo_benchmark.py +++ b/src/deephyper_benchmark/lib/c_bbo/easom/hpo_benchmark.py @@ -37,7 +37,7 @@ class EasomHPOScorer(HPOScorer): """A class defining performance evaluators for the Ackley problem.""" def __init__(self, offset=0): - self.x_max = np.array([np.pi, np.pi]) - offset + self.x_max = np.array([np.pi, np.pi]) self.y_max = 1.0 def simple_regret(self, y: np.ndarray) -> np.ndarray: diff --git a/src/deephyper_benchmark/lib/c_bbo/schwefel/hpo_benchmark.py b/src/deephyper_benchmark/lib/c_bbo/schwefel/hpo_benchmark.py index 4f3cbec..903ba0c 100644 --- a/src/deephyper_benchmark/lib/c_bbo/schwefel/hpo_benchmark.py +++ b/src/deephyper_benchmark/lib/c_bbo/schwefel/hpo_benchmark.py @@ -38,8 +38,6 @@ class SchwefelHPOScorer(HPOScorer): def __init__( self, p_num, - p_num_slack, - offset=0, ): self.p_num = p_num self.x_max = np.full(self.p_num, fill_value=420.9687) diff --git a/src/deephyper_benchmark/lib/c_bbo/shekel/hpo_benchmark.py b/src/deephyper_benchmark/lib/c_bbo/shekel/hpo_benchmark.py index d1b881e..d0e10ab 100644 --- a/src/deephyper_benchmark/lib/c_bbo/shekel/hpo_benchmark.py +++ b/src/deephyper_benchmark/lib/c_bbo/shekel/hpo_benchmark.py @@ -45,7 +45,7 @@ class ShekelHPOScorer(HPOScorer): """A class defining performance evaluators for the Shekel problem.""" def __init__(self): - self.p_num = 10 + self.p_num = 4 self.x_max = np.full(self.p_num, fill_value=4.0) self.y_max = 10.5364 @@ -77,7 +77,7 @@ class ShekelHPOBenchmark(HPOBenchmark): def problem(self): domain = (0.0, 10.0) problem = HpProblem() - for i in range(10): + for i in range(4): problem.add_hyperparameter(domain, f"x{i}") return problem diff --git a/src/deephyper_benchmark/lib/dtlz/README.md b/src/deephyper_benchmark/lib/dtlz/README.md new file mode 100644 index 0000000..5c09f18 --- /dev/null +++ b/src/deephyper_benchmark/lib/dtlz/README.md @@ -0,0 +1,130 @@ + +# Modified Multiobjective DTLZ Test Suite + +✅ **Benchmark ready to be used.** + +This module contains objective function implementations of the DTLZ test +suite, derived from the implementations in +[ParMOO](https://github.com/parmoo/parmoo). + +------------------------------------------------------------------------------ + +For further reference, the DTLZ test suite was originally proposed in: + + Deb, Thiele, Laumanns, and Zitzler. "Scalable test problems for + evolutionary multiobjective optimization" in Evolutionary Multiobjective + Optimization, Theoretical Advances and Applications, Ch. 6 (pp. 105--145). + Springer-Verlag, London, UK, 2005. Abraham, Jain, and Goldberg (Eds). + +The original implementation was appropriate for testing randomized algorithms, +but for many deterministic algorithms, the global solutions represent either +best- or worst-case scenarios, so an configurable offset was introduced in: + + Chang. "Mathematical Software for Multiobjective Optimization Problems." + Ph.D. dissertation, Virginia Tech, Dept. of Computer Science, 2020. + +Note that the DTLZ problems are minimization problems. Since DeepHyper +maximizes, the implementation herein returns the negative value for each of +the DTLZ objectives. + +Our performance evaluator ``metrics`` scripts can evaluate either the +positive or negative solutions to estimate how well we have solved the +problem. + +------------------------------------------------------------------------------ + +The full list of public classes in this module includes the 7 unconstrained +DTLZ problems + * ``dtlz1``, + * ``dtlz2``, + * ``dtlz3``, + * ``dtlz4``, + * ``dtlz5``, + * ``dtlz6``, and + * ``dtlz7`` + +which are selected by setting the environment variable +``DEEPHYPER_BENCHMARK_DTLZ_PROB``. + +## Installation + +To use the benchmark follow this example set of instructions: + +```python + +# Set DTLZ problem environment variables before loading +import os +os.environ["DEEPHYPER_BENCHMARK_NDIMS"] = "5" # 5 vars +os.environ["DEEPHYPER_BENCHMARK_NOBJS"] = "3" # 2 objs +os.environ["DEEPHYPER_BENCHMARK_DTLZ_PROB"] = "2" # DTLZ2 problem +os.environ["DEEPHYPER_BENCHMARK_DTLZ_OFFSET"] = "0.6" # soln [x_o, .., x_n]=0.6 + +# Load DTLZ benchmark suite +import deephyper_benchmark as dhb +dhb.load("DTLZ") + +# Example of running one evaluation of DTLZ problem +from deephyper.evaluator import RunningJob +config = dtlz.hpo.problem.default_configuration # get a default config to test +res = dtlz.hpo.run(RunningJob(parameters=config)) + +``` + +## Configuration + +To configure the problem, set the following: + +- Environment variable `DEEPHYPER_BENCHMARK_PROB` with a value of `1`, `2`, ... `7` to select the DTLZ problem to run. Defaults to `2`. +- Environment variable `DEEPHYPER_BENCHMARK_NDIMS` with an integer value to set the number of input variables. Defaults to `5`. +- Environment variable `DEEPHYPER_BENCHMARK_NOBJS` with an integer value to set the number of objectives. Defaults to `2` +- Environment variable `DEEPHYPER_BENCHMARK_OFFSET` with a value between `0.0` and `1.0` to select the offset of the solution to the DTLZ problem. Defaults to `0.5` for DTLZ1, ..., DTLZ5 and `0.0` for DTLZ6 and DTLZ7. One may wish to adjust this value when comparing against deterministic blackbox solvers, which may sample the center and boundaries of the input space on specific schedules. +- Environment variable `DEEPHYPER_BENCHMARK_FAILURES` with value `0` or `1` to activate or deactivate failures. Defaults to `0`. + +## Metadata + +Since these problems are analytic, there is no metadata for this problem +beyond the standard DeepHyper metadata (timestamp information). + +## Evaluating Results + +Evaluating the performance of a multiobjective solver is nontrivial. +Typically, one should evaluate on two orthogonal bases: + 1. Quality of solutions -- What is the (average) error in the solutions + returned by the solver? + 2. Diversity of solutions -- How much of the true Pareto front is covered + by these solutions? + +To evaluate these two metrics, we use: + 1. Improved generational distance (GD+): Let $F_i$ be a point in the solution + set returned by a solver, + and let $Y_i$ be the nearest point to $F_i$ on the true Pareto front, + for $i=1,\ldots, n$. + Then the GD+ is $\sum_{i} D^+(F_i, Y_i) / n$. + Where $D^+$ denotes the improved distance function + $D^+(F, Y) = ||\max(F - Y, 0)||_2^2$, where the "max" is taken + componentwise. + **Note that this metric may not increase monotonically. Additionally, + it may be impossible to calculate for an arbitrary blackbox function, + and can only be calculated here since the solution known and easily + expressed algebraically for all of the DTLZ problems.** + 2. Hypervolume dominated: Let $F_i$ be defined as above, and let $R$ be + a pre-determined reference point such that all $F_i$ dominate $R$. + Then the hypervolume is given by the volume of the union of all + hyperboxes $B_i$ whose largest vertex is $F_i$ and smallest vertex + is $R$. The value (and usefulness) of the hypervolume metric is extremely + sensitive to the choice of $R$. Therefore, for this problem, we choose + $R$ to be the Nadir point for the true Pareto front. **Note that in order + to use the Nadir point as the reference point, we must throw out every + solution returned by the solver that is worse than the Nadir point. For + extremely difficult problems, this can result in zero hypervolume if no + solutions better than the Nadir point were found. This is most common + for DTLZ1, DTLZ3, and DTLZ7.** + +For a general problem, the two metrics listed above could be very difficult +to compute and many researchers will use the hypervolume with an overly +pessimistic reference point as a proxy for both quality and diversity. +However, in general, the hypervolume tends to promote diversity over quality. +For the DTLZ problems, since the shape of the true Pareto front is known, +we can calculate each of these metrics, and both the ``gdPlus(results)`` and +``hypervolume(results)`` functions are implemented in the ``dtlz.metrics`` +module. diff --git a/src/deephyper_benchmark/lib/dtlz/__init__.py b/src/deephyper_benchmark/lib/dtlz/__init__.py new file mode 100644 index 0000000..f102a9c --- /dev/null +++ b/src/deephyper_benchmark/lib/dtlz/__init__.py @@ -0,0 +1 @@ +__version__ = "0.0.1" diff --git a/src/deephyper_benchmark/lib/dtlz/benchmark.py b/src/deephyper_benchmark/lib/dtlz/benchmark.py new file mode 100644 index 0000000..2c12251 --- /dev/null +++ b/src/deephyper_benchmark/lib/dtlz/benchmark.py @@ -0,0 +1,6 @@ +from deephyper_benchmark import * + + +class DTLZBenchmark(Benchmark): + + version = "0.0.1" diff --git a/src/deephyper_benchmark/lib/dtlz/hpo.py b/src/deephyper_benchmark/lib/dtlz/hpo.py new file mode 100644 index 0000000..70cd6d3 --- /dev/null +++ b/src/deephyper_benchmark/lib/dtlz/hpo.py @@ -0,0 +1,61 @@ +import os +import time + +import numpy as np +from deephyper.evaluator import RunningJob, profile +from deephyper.hpo import HpProblem + +from . import model as dtlz + +# Read DTLZ problem name and acquire pointer +dtlz_prob = os.environ.get("DEEPHYPER_BENCHMARK_DTLZ_PROB", 2) +dtlz_prob_name = f"dtlz{dtlz_prob}" +dtlz_class_ptr = getattr(dtlz, dtlz_prob_name) + +# Read problem dims and definition (or read from ENV) +nb_dim = int(os.environ.get("DEEPHYPER_BENCHMARK_NDIMS", 5)) +nb_obj = int(os.environ.get("DEEPHYPER_BENCHMARK_NOBJS", 2)) +if dtlz_prob_name in ["dtlz1", "dtlz2", "dtlz3", "dtlz4", "dtlz5"]: + soln_offset = float(os.environ.get("DEEPHYPER_BENCHMARK_DTLZ_OFFSET", 0.5)) +elif dtlz_prob_name in ["dtlz6", "dtlz7"]: + soln_offset = float(os.environ.get("DEEPHYPER_BENCHMARK_DTLZ_OFFSET", 0.0)) +else: + raise ValueError(f"Invalid problem {dtlz_prob_name}") +domain = (0.0, 1.0) + +# Failures +DEEPHYPER_BENCHMARK_FAILURES = bool(int(os.environ.get("DEEPHYPER_BENCHMARK_FAILURES", 0))) + +# Create problem +problem = HpProblem() +dtlz_obj = dtlz_class_ptr(nb_dim, nb_obj, offset=soln_offset) +for i in range(nb_dim): + problem.add_hyperparameter(domain, f"x{i}") + + +@profile +def run(job: RunningJob, sleep=False, sleep_mean=60, sleep_noise=20) -> dict: + config = job.parameters + + if sleep: + t_sleep = np.random.normal(loc=sleep_mean, scale=sleep_noise) + t_sleep = max(t_sleep, 0) + time.sleep(t_sleep) + + x = np.array([config[k] for k in config if "x" in k]) + x = np.asarray_chkfinite(x) # ValueError if any NaN or Inf + ff = [-fi for fi in dtlz_obj(x)] + + if DEEPHYPER_BENCHMARK_FAILURES: + if any(xi < 0.25 for xi in x[nb_obj-1:]): + ff = ["F" for _ in ff] + + return ff + + +if __name__ == "__main__": + print(problem) + default_config = problem.default_configuration + print(f"{default_config=}") + result = run(RunningJob(parameters=default_config)) + print(f"{result=}") diff --git a/src/deephyper_benchmark/lib/dtlz/metrics.py b/src/deephyper_benchmark/lib/dtlz/metrics.py new file mode 100644 index 0000000..3d4a305 --- /dev/null +++ b/src/deephyper_benchmark/lib/dtlz/metrics.py @@ -0,0 +1,201 @@ +import os +import numpy as np +from deephyper.skopt.moo import pareto_front, hypervolume + + +class PerformanceEvaluator: + """ A class defining performance evaluators for the DTLZ problems. + + Contains the following public methods: + + * `__init__()` constructs a new instance by reading the problem defn + from environment variables, + * `hypervolume(pts)` calculates the total hypervolume dominated by + the current solution, using the Nadir point as the reference point + and filtering out solutions that do not dominate the Nadir point, + * `nadirPt()` calculates the Nadir point for the current problem, + * `numPts(pts)` calculates the number of solution points that dominate + the Nadir point, and + * `gdPlus(pts)` calculates the RMSE where the error in each point is + approximated by the 2-norm distance to the nearest solution point. + + """ + + def __init__(self): + """ Read the current DTLZ problem defn from environment vars. """ + + self.p_num = os.environ.get("DEEPHYPER_BENCHMARK_DTLZ_PROB", "2") + self.nobjs = int(os.environ.get("DEEPHYPER_BENCHMARK_NOBJS", 2)) + + def hypervolume(self, pts): + """ Calculate the hypervolume dominated by soln, wrt the Nadir point. + + Args: + pts (numpy.ndarra): A 2d array of objective values. + Each row is an objective value in the solution set. + + Returns: + float: The total hypervolume dominated by the current solution, + filtering out points worse than the Nadir point and using the + Nadir point as the reference. + + """ + + if np.any(pts < 0): + filtered_pts = -pts.copy() + else: + filtered_pts = pts.copy() + nadir = self.nadirPt() + for i in range(pts.shape[0]): + if np.any(filtered_pts[i, :] > nadir): + filtered_pts[i, :] = nadir + return hypervolume(filtered_pts, nadir) + + def nadirPt(self): + """ Calculate the Nadir point for the given problem definition. """ + + if self.p_num == "1": + return np.ones(self.nobjs) * 0.5 + elif self.p_num in ["2", "3", "4", "5", "6"]: + return np.ones(self.nobjs) + elif self.p_num == "7": + nadir = np.ones(self.nobjs) + nadir[self.nobjs - 1] = self.nobjs * 2.0 + return nadir + else: + raise ValueError(f"DTLZ{self.p_num} is not a valid problem") + + def numPts(self, pts): + """ Calculate the number of solutions that dominate the Nadir point. + + Args: + pts (numpy.ndarra): A 2d array of objective values. + Each row is an objective value in the solution set. + + Returns: + int: The number of fi in pts such that all(fi < self.nadirPt). + + """ + + if np.any(pts < 0): + pareto_pts = pareto_front(-pts) + else: + pareto_pts = pareto_front(pts) + return sum([all(fi <= self.nadirPt()) for fi in pareto_pts]) + + def gdPlus(self, pts): + """ Calculate the p=1 generational distance for a given solution set. + + Args: + pts (numpy.ndarra): A 2d array of objective values. + Each row is an objective value in the solution set. + + Returns: + float: The p=1 generational distance over all points in pts. + + """ + + if np.any(pts < 0): + pareto_pts = pareto_front(-pts) + else: + pareto_pts = pareto_front(pts) + if self.p_num == "1": + dists = self._dtlz1Dist(pareto_pts) + elif self.p_num in ["2", "3", "4", "5", "6"]: + dists = self._dtlz2Dist(pareto_pts) + elif self.p_num == "7": + dists = self._dtlz7Dist(pareto_pts) + else: + raise ValueError(f"DTLZ{self.p_num} is not a valid problem") + return np.sum(dists) / len(dists) + + def _dtlz1Dist(self, pts): + """ Calculate the d+ from each fi to the nearest solution in DTLZ1. + + Args: + pts (numpy.ndarra): A 2d array of objective values. + Each row is an objective value in the solution set. + + Returns: + numpy.ndarray: A 1d array of distances to the nearest solution + point for DTLZ1. + + + """ + + return np.array([np.linalg.norm( + np.maximum(fi - (0.5 * fi / np.sum(fi)), 0) + ) for fi in pts]) + + def _dtlz2Dist(self, pts): + """ Calculate the d+ from each fi to the nearest point on unit sphere. + + Note: Works for DTLZ2-6 + + Args: + pts (numpy.ndarra): A 2d array of objective values. + Each row is an objective value in the solution set. + + Returns: + numpy.ndarray: A 1d array of distances to the surface of the + unit sphere. + + """ + + return np.array([np.linalg.norm( + np.maximum(fi - (fi / np.linalg.norm(fi)), 0) + ) for fi in pts]) + + def _dtlz7Dist(self, pts): + """ Calculate the d+ from each fi to the nearest soln in DTLZ7. + + Args: + pts (numpy.ndarra): A 2d array of objective values. + Each row is an objective value in the solution set. + + Returns: + numpy.ndarray: A 1d array of distances to the nearest solution + point to DTLZ7. + + """ + + # Project each point onto DTLZ7 solution and calculate difference + pts_proj = [] + for fi in pts: + gx = 2.0 + hx = (-np.sum(fi[:self.nobjs-1] * + (1.0 + np.sin(3.0 * np.pi * fi[:self.nobjs-1])) / gx) + + float(self.nobjs)) + pts_proj.append(gx * hx) + return np.array([np.abs( + np.maximum(fi[-1] - fj, 0) + ) for fi, fj in zip(pts, pts_proj)]) + + +if __name__ == "__main__": + """ Driver code to test performance metrics. """ + + os.environ["DEEPHYPER_BENCHMARK_DTLZ_PROB"] = "1" # DTLZ1 problem + dtlz1_eval = PerformanceEvaluator() + s1 = np.array([[0.5, 0], [0, 0.5], [.25, .25], [0.2, 0.8]]) + os.environ["DEEPHYPER_BENCHMARK_DTLZ_PROB"] = "2" # DTLZ2 problem + dtlz2_eval = PerformanceEvaluator() + s2 = np.array([[1, 0], [0, 1], [1/np.sqrt(2), 1/np.sqrt(2)], [0.25, 2]]) + os.environ["DEEPHYPER_BENCHMARK_DTLZ_PROB"] = "7" # DTLZ7 problem + dtlz7_eval = PerformanceEvaluator() + s7 = np.array([[0, 4], [1, 3], [.5, 4], [0.5, 6]]) + + assert abs(dtlz1_eval.hypervolume(s1) - .0625) < 1.0e-8 + assert np.all(np.abs(dtlz1_eval.nadirPt() - 0.5) < 1.0e-8) + assert dtlz1_eval.numPts(s1) == 3 + assert abs(dtlz1_eval.gdPlus(s1)) < 1.0e-8 + + assert abs(dtlz2_eval.hypervolume(s2) - (1.5 - np.sqrt(2))) < 1.0e-8 + assert np.all(np.abs(dtlz2_eval.nadirPt() - 1) < 1.0e-8) + assert dtlz2_eval.numPts(s2) == 3 + assert abs(dtlz2_eval.gdPlus(s2)) < 1.0e-8 + + assert abs(dtlz7_eval.hypervolume(s7)) < 1.0e-8 + assert np.all(np.abs(dtlz7_eval.nadirPt() - np.array([1, 4])) < 1.0e-8) + assert dtlz7_eval.numPts(s7) == 2 + assert abs(dtlz7_eval.gdPlus(s7)) < 1.0e-8 diff --git a/src/deephyper_benchmark/lib/dtlz/model.py b/src/deephyper_benchmark/lib/dtlz/model.py new file mode 100644 index 0000000..f7f22f4 --- /dev/null +++ b/src/deephyper_benchmark/lib/dtlz/model.py @@ -0,0 +1,591 @@ +"""Objective function implementations of the DTLZ test suite. + +Derived from the implementations in ParMOO: + +Chang and Wild. "ParMOO: A Python library for parallel multiobjective +simulation optimization." Journal of Open Source Software 8(82):4468, 2023. + +------------------------------------------------------------------------------ + +For further references, the DTLZ test suite was originally proposed in: + +Deb, Thiele, Laumanns, and Zitzler. "Scalable test problems for +evolutionary multiobjective optimization" in Evolutionary Multiobjective +Optimization, Theoretical Advances and Applications, Ch. 6 (pp. 105--145). +Springer-Verlag, London, UK, 2005. Abraham, Jain, and Goldberg (Eds). + +The original implementation was appropriate for testing randomized algorithms, +but for many deterministic algorithms, the global solutions represent either +best- or worst-case scenarios, so an configurable offset was introduced in: + +Chang. "Mathematical Software for Multiobjective Optimization Problems." +Ph.D. dissertation, Virginia Tech, Dept. of Computer Science, 2020. + +------------------------------------------------------------------------------ + +The full list of public classes in this module includes the 7 unconstrained +DTLZ problems: + * ``dtlz1`` + * ``dtlz2`` + * ``dtlz3`` + * ``dtlz4`` + * ``dtlz5`` + * ``dtlz6`` + * ``dtlz7`` + +""" + +import numpy as np + + +class __dtlz_base__: + """Base class implements re-used constructor. + + Constructor for all DTLZ classes. + + Args: + num_des (int): The number of design variables. + + num_obj (int, optional): The number of objectives. + + offset (optional, float): The location of the global minimizers + is x_i = offset for i = num_objectives, ..., num_des. + The default offset is 0.5. + + """ + + def __init__(self, num_des, num_obj=3, offset=0.5): + self.n = num_des + self.o = num_obj + self.offset = offset + return + + def __call__(self, x): + raise NotImplementedError("The call method must be implemented...") + + +class __g1__(__dtlz_base__): + """Class defining 1 of 4 kernel functions used in the DTLZ problem suite. + + g1 = 100 ( (n - o + 1) + + sum_{i=o}^n ((x_i - offset)^2 - cos(20pi(x_i - offset))) ) + + Contains 2 methods: + * ``__init__(num_des, num_obj)`` + * ``__call__(x)`` + + The ``__init__`` method creates a new kernel. + + The ``__call__`` method performs an evaluation of the g1 kernel. + + """ + + def __call__(self, x): + """Define objective evaluation. + + Args: + x (numpy.array): A numpy.ndarray containing the design point + to evaluate. + + Returns: + float: The output of this objective for the input x. + + """ + return ( + 1 + + self.n + - self.o + + np.sum( + (x[self.o - 1 : self.n] - self.offset) ** 2 + - np.cos(20.0 * np.pi * (x[self.o - 1 : self.n] - self.offset)) + ) + ) * 100.0 + + +class __g2__(__dtlz_base__): + """Class defining 2 of 4 kernel functions used in the DTLZ problem suite. + + g2 = (x_o - offset)^2 + ... + (x_n - offset)^2 + + Contains 2 methods: + * ``__init__(num_des, num_obj)`` + * ``__call__(x)`` + + The ``__init__`` method inherits from the __dtlz_base__ ABC. + + The ``__call__`` method performs an evaluation of the g2 problem. + + """ + + def __call__(self, x): + """Define objective evaluation. + + Args: + x (numpy.array): A numpy.ndarray containing the design point + to evaluate. + + Returns: + float: The output of this objective for the input x. + + """ + return np.sum((x[self.o - 1 : self.n] - self.offset) ** 2) + + +class __g3__(__dtlz_base__): + """Class defining 3 of 4 kernel functions used in the DTLZ problem suite. + + g3 = |x_o - offset|^.1 + ... + |x_n - offset|^.1 + + Contains 2 methods: + * ``__init__(num_des, num_obj)`` + * ``__call__(x)`` + + The ``__init__`` method inherits from the __dtlz_base__ ABC. + + The ``__call__`` method performs an evaluation of the g3 problem. + + """ + + def __init__(self, num_des, num_obj=3, offset=0.0): + """Constructor for g3, with modified default offset. + + Args: + num_des (int): The number of design variables. + + num_obj (int, optional): The number of objectives. + + offset (optional, float): The location of the global minimizers + is x_i = offset for i = num_objectives, ..., num_des. + The default offset is 0.0. + + """ + super().__init__(num_des=num_des, num_obj=num_obj, offset=offset) + return + + def __call__(self, x): + """Define objective evaluation. + + Args: + x (numpy.array): A numpy.ndarray containing the design point + to evaluate. + + Returns: + float: The output of this objective for the input x. + + """ + return np.sum(np.abs(x[self.o - 1 : self.n] - self.offset) ** 0.1) + + +class __g4__(__dtlz_base__): + """Class defining 4 of 4 kernel functions used in the DTLZ problem suite. + + g4 = 1 + (9 * (|x_o - offset| + ... + |x_n - offset|) / (n + 1 - o)) + + Contains 2 methods: + * ``__init__(num_des, num_obj)`` + * ``__call__(x)`` + + The ``__init__`` method inherits from the __dtlz_base__ ABC. + + The ``__call__`` method performs an evaluation of the g4 problem. + + """ + + def __init__(self, num_des, num_obj=3, offset=0.0): + """Constructor for g4, with modified default offset. + + Args: + num_des (int): The number of design variables. + + num_obj (int, optional): The number of objectives. + + offset (optional, float): The location of the global minimizers + is x_i = offset for i = num_objectives, ..., num_des. + The default offset is 0.0. + + """ + super().__init__(num_des=num_des, num_obj=num_obj, offset=offset) + return + + def __call__(self, x): + """Define objective evaluation. + + Args: + x (numpy.array): A numpy.ndarray containing the design point + to evaluate. + + Returns: + float: The output of this objective for the input x. + + """ + return ( + 9 * np.sum(np.abs(x[self.o - 1 : self.n] - self.offset)) / float(self.n + 1 - self.o) + ) + 1.0 + + +class dtlz1(__dtlz_base__): + """Class defining the DTLZ1 problem with offset minimizer. + + DTLZ1 has a linear Pareto front, with all nondominated points + on the hyperplane F_1 + F_2 + ... + F_o = 0.5. + DTLZ1 has 11^k - 1 "local" Pareto fronts where k = n - o + 1, and + 1 "global" Pareto front. + + Contains 2 methods: + * ``__init__(num_des, num_obj)`` + * ``__call__(x)`` + + The ``__init__`` method inherits from the __dtlz_base__ ABC. + + The ``__call__`` method performs an evaluation of the DTLZ1 problem. + + """ + + def __call__(self, x): + """Define objective evaluation. + + Args: + x (numpy.ndarray): A numpy.ndarray containing the design point + to evaluate. + + Returns: + numpy float array: The output of this objective for the input x. + + """ + # Initialize kernel function + ker = __g1__(self.n, self.o, self.offset) + # Initialize output array + fx = np.zeros(self.o) + fx[:] = (1.0 + ker(x)) / 2.0 + # Calculate the output array + for i in range(self.o): + for j in range(self.o - 1 - i): + fx[i] *= x[j] + if i > 0: + fx[i] *= 1.0 - x[self.o - 1 - i] + return fx + + +class dtlz2(__dtlz_base__): + """Class defining the DTLZ2 problem with offset minimizer. + + DTLZ2 has a concave Pareto front, given by the unit sphere in + objective space, restricted to the positive orthant. + DTLZ2 has no "local" Pareto fronts, besides the true Pareto front. + + Contains 2 methods: + * ``__init__(num_des, num_obj)`` + * ``__call__(x)`` + + The ``__init__`` method inherits from the __dtlz_base__ ABC. + + The ``__call__`` method performs an evaluation of the DTLZ2 problem. + + """ + + def __call__(self, x): + """Define objective evaluation. + + Args: + x (numpy.ndarray): A numpy.ndarray containing the design point + to evaluate. + + Returns: + numpy float array: The output of this objective for the input x. + + """ + # Initialize kernel function + ker = __g2__(self.n, self.o, self.offset) + # Initialize output array + fx = np.zeros(self.o) + fx[:] = 1.0 + ker(x) + # Calculate the output array + for i in range(self.o): + for j in range(self.o - 1 - i): + fx[i] *= np.cos(np.pi * x[j] / 2) + if i > 0: + fx[i] *= np.sin(np.pi * x[self.o - 1 - i] / 2) + return fx + + +class dtlz3(__dtlz_base__): + """Class defining the DTLZ3 problem with offset minimizer. + + DTLZ3 has a concave Pareto front, given by the unit sphere in + objective space, restricted to the positive orthant. + DTLZ3 has 3^k - 1 "local" Pareto fronts where k = n - o + 1, and + 1 "global" Pareto front. + + Contains 2 methods: + * ``__init__(num_des, num_obj)`` + * ``__call__(x)`` + + The ``__init__`` method inherits from the __dtlz_base__ ABC. + + The ``__call__`` method performs an evaluation of the DTLZ3 problem. + + """ + + def __call__(self, x): + """Define objective evaluation. + + Args: + x (numpy.ndarray): A numpy.ndarray containing the design point + to evaluate. + + Returns: + numpy float array: The output of this objective for the input x. + + """ + # Initialize kernel function + ker = __g1__(self.n, self.o, self.offset) + # Initialize output array + fx = np.zeros(self.o) + fx[:] = 1.0 + ker(x) + # Calculate the output array + for i in range(self.o): + for j in range(self.o - 1 - i): + fx[i] *= np.cos(np.pi * x[j] / 2) + if i > 0: + fx[i] *= np.sin(np.pi * x[self.o - 1 - i] / 2) + return fx + + +class dtlz4(__dtlz_base__): + """Class defining the DTLZ4 problem with offset minimizer. + + DTLZ4 has a concave Pareto front, given by the unit sphere in + objective space, restricted to the positive orthant. + DTLZ4 has no "local" Pareto fronts, besides the true Pareto front, + but by tuning the optional parameter alpha, one can adjust the + solution density, making it harder for MOO algorithms to produce + a uniform distribution of solutions. + + Contains 2 methods: + * ``__init__(num_des, num_obj)`` + * ``__call__(x)`` + + The ``__init__`` method inherits from the __dtlz_base__ ABC. + + The ``__call__`` method performs an evaluation of the DTLZ4 problem. + + """ + + def __init__(self, num_des, num_obj=3, offset=0.5, alpha=100.0): + """Constructor for DTLZ7, with modified default offset. + + Args: + num_des (int): The number of design variables. + + num_obj (int, optional): The number of objectives. + + offset (optional, float): The location of the global minimizers + is x_i = offset for i = num_objectives, ..., num_des. + The default offset is 0.0. + + alpha (optional, float or int): The uniformity parameter used for + controlling the uniformity of the distribution of solutions + across the Pareto front. Must be greater than or equal to 1. + A value of 1 results in DTLZ2. Default value is 100.0. + + """ + super().__init__(num_des=num_des, num_obj=num_obj, offset=offset) + self.alpha = alpha + return + + def __call__(self, x): + """Define objective evaluation. + + Args: + x (numpy.ndarray): A numpy.ndarray containing the design point + to evaluate. + + Returns: + numpy float array: The output of this objective for the input x. + + """ + # Initialize kernel function + ker = __g2__(self.n, self.o, self.offset) + # Initialize output array + fx = np.zeros(self.o) + fx[:] = 1.0 + ker(x) + # Calculate the output array + for i in range(self.o): + for j in range(self.o - 1 - i): + fx[i] *= np.cos(np.pi * x[j] ** self.alpha / 2) + if i > 0: + fx[i] *= np.sin(np.pi * x[self.o - 1 - i] ** self.alpha / 2) + return fx + + +class dtlz5(__dtlz_base__): + """Class defining the DTLZ5 problem with offset minimizer. + + DTLZ5 has a lower-dimensional Pareto front embedded in the objective + space, given by an arc of the unit sphere in the positive orthant. + DTLZ5 has no "local" Pareto fronts, besides the true Pareto front. + + Contains 2 methods: + * ``__init__(num_des, num_obj)`` + * ``__call__(x)`` + + The ``__init__`` method inherits from the __dtlz_base__ ABC. + + The ``__call__`` method performs an evaluation of the DTLZ5 problem. + + """ + + def __call__(self, x): + """Define objective evaluation. + + Args: + x (numpy.ndarray): A numpy.ndarray containing the design point + to evaluate. + + Returns: + numpy float array: The output of this objective for the input x. + + """ + # Initialize kernel function + ker = __g2__(self.n, self.o, self.offset) + # Calculate theta values + theta = np.zeros(self.o) + g2x = ker(x) + theta[0] = x[0] + for i in range(1, self.o): + theta[i] = (1 + 2 * g2x * x[i]) / (2 * (1 + g2x)) + # Initialize output array + fx = np.zeros(self.o) + fx[:] = 1.0 + g2x + # Calculate the output array + for i in range(self.o): + for j in range(self.o - 1 - i): + fx[i] *= np.cos(np.pi * theta[j] / 2) + if i > 0: + fx[i] *= np.sin(np.pi * theta[self.o - 1 - i] / 2) + return fx + + +class dtlz6(__dtlz_base__): + """Class defining the DTLZ6 problem with offset minimizer. + + DTLZ6 has a lower-dimensional Pareto front embedded in the objective + space, given by an arc of the unit sphere in the positive orthant. + DTLZ6 has no "local" Pareto fronts, but tends to show very little + improvement until the algorithm is very close to its solution set. + + Contains 2 methods: + * ``__init__(num_des, num_obj)`` + * ``__call__(x)`` + + The ``__init__`` method inherits from the __dtlz_base__ ABC. + + The ``__call__`` method performs an evaluation of the DTLZ6 problem. + + """ + + def __init__(self, num_des, num_obj=3, offset=0.0): + """Constructor for DTLZ6, with modified default offset. + + Args: + num_des (int): The number of design variables. + + num_obj (int, optional): The number of objectives. + + offset (optional, float): The location of the global minimizers + is x_i = offset for i = num_objectives, ..., num_des. + The default offset is 0.0. + + """ + super().__init__(num_des=num_des, num_obj=num_obj, offset=offset) + return + + def __call__(self, x): + """Define objective evaluation. + + Args: + x (numpy.ndarray): A numpy.ndarray containing the design point + to evaluate. + + Returns: + numpy float array: The output of this objective for the input x. + + """ + # Initialize kernel function + ker = __g3__(self.n, self.o, self.offset) + # Calculate theta values + theta = np.zeros(self.o) + g3x = ker(x) + theta[0] = x[0] + for i in range(1, self.o): + theta[i] = (1 + 2 * g3x * x[i]) / (2 * (1 + g3x)) + # Initialize output array + fx = np.zeros(self.o) + fx[:] = 1.0 + g3x + # Calculate the output array + for i in range(self.o): + for j in range(self.o - 1 - i): + fx[i] *= np.cos(np.pi * theta[j] / 2) + if i > 0: + fx[i] *= np.sin(np.pi * theta[self.o - 1 - i] / 2) + return fx + + +class dtlz7(__dtlz_base__): + """Class defining the DTLZ7 problem with offset minimizer. + + DTLZ7 has a discontinuous Pareto front, with solutions on the + 2^(o-1) discontinuous nondominated regions of the surface: + + F_m = o - F_1 (1 + sin(3pi F_1)) - ... - F_{o-1} (1 + sin3pi F_{o-1}). + + Contains 2 methods: + * ``__init__(num_des, num_obj)`` + * ``__call__(x)`` + + The ``__init__`` method inherits from the __dtlz_base__ ABC. + + The ``__call__`` method performs an evaluation of the DTLZ7 problem. + + """ + + def __init__(self, num_des, num_obj=3, offset=0.0): + """Constructor for DTLZ7, with modified default offset. + + Args: + num_des (int): The number of design variables. + + num_obj (int, optional): The number of objectives. + + offset (optional, float): The location of the global minimizers + is x_i = offset for i = num_objectives, ..., num_des. + The default offset is 0.0. + + """ + super().__init__(num_des=num_des, num_obj=num_obj, offset=offset) + return + + def __call__(self, x): + """Define objective evaluation. + + Args: + x (numpy.ndarray): A numpy.ndarray containing the design point + to evaluate. + + Returns: + numpy float array: The output of this objective for the input x. + + """ + # Initialize kernel function + ker = __g4__(self.n, self.o, self.offset) + # Initialize first o-1 entries in the output array + fx = np.zeros(self.o) + fx[: self.o - 1] = x[: self.o - 1] + # Calculate kernel functions + gx = 1.0 + ker(x) + hx = -np.sum(x[: self.o - 1] * (1.0 + np.sin(3.0 * np.pi * x[: self.o - 1])) / gx) + float( + self.o + ) + # Calculate the last entry in the output array + fx[self.o - 1] = gx * hx + return fx diff --git a/src/deephyper_benchmark/lib/ecp_candle/Pilot1/Combo/Makefile b/src/deephyper_benchmark/lib/ecp_candle/Pilot1/Combo/Makefile new file mode 100644 index 0000000..29e6a6b --- /dev/null +++ b/src/deephyper_benchmark/lib/ecp_candle/Pilot1/Combo/Makefile @@ -0,0 +1,11 @@ + +VERSION=0.5.1 + +build: + mkdir -p build + wget https://github.com/ECP-CANDLE/Benchmarks/archive/refs/tags/v$(VERSION).tar.gz -O build/ecp-candle-benchmarks.tar.gz + tar -xzvf build/ecp-candle-benchmarks.tar.gz -C build + mv build/Benchmarks-$(VERSION) build/Benchmarks + +clean: + rm -rf build/ \ No newline at end of file diff --git a/src/deephyper_benchmark/lib/ecp_candle/Pilot1/Combo/README.md b/src/deephyper_benchmark/lib/ecp_candle/Pilot1/Combo/README.md new file mode 100644 index 0000000..ba8f255 --- /dev/null +++ b/src/deephyper_benchmark/lib/ecp_candle/Pilot1/Combo/README.md @@ -0,0 +1,54 @@ + +# ECP-Candle Benchmark - Pilot1/Combo + +This benchmark is based on materials from the [ECP-Candle/Benchmarks](https://github.com/ECP-CANDLE/Benchmarks) repository. More precisely it is a simplified replicate of the [Pilot1/Combo](https://github.com/ECP-CANDLE/Benchmarks/tree/master/Pilot1/Combo) benchmark. + + +## Installation + +To use the benchmark follow this example set of instructions: + +```python +from deephyper_benchmark import * + +install("ECP-Candle/Pilot1/Combo") + +load("ECP-Candle/Pilot1/Combo") + +from deephyper_benchmark.lib.ecp_candle.pilot1.combo import hpo +``` + +## Configuration + +Different parameters can be set to configure this benchmark. + +- Environment variable `DEEPHYPER_BENCHMARK_MAX_EPOCHS` sets the maximum number of training epochs. Defaults to `50`. +- Environment variable `DEEPHYPER_BENCHMARK_TIMEOUT` sets the maximum duration of training in secondes. Defaults to `1800` (i.e., 30 minutes). +- Environment variable `DEEPHYPER_BENCHMARK_MOO` with value `0` or `1` to select if the task should be run with single or multiple objectives. **Defaults to `0` for single-objective**. The objectives are `valid_r2`, `-num_parameters_train`, `-duration_batch_inference`. + +## Metadata + +The current set of returned metadata is: + +- [x] `num_parameters` +- [x] `num_parameters_train` +- [x] `duration_train`: time in secondes taken to train the model (`model.fit(...)`). +- [x] `duration_batch_inference`: time in secondes taken to predict one batch. +- [x] `budget` +- [x] `stopped` +- [x] `train_mse` +- [x] `train_mae` +- [x] `train_r2` +- [x] `train_corr` +- [x] `valid_mse` +- [x] `valid_mae` +- [x] `valid_r2` +- [x] `valid_corr` +- [x] `test_mse` +- [x] `test_mae` +- [x] `test_r2` +- [x] `test_corr` +- [ ] `flops` +- [ ] `latency` +- [x] `lc_train_mse` +- [x] `lc_valid_mse` \ No newline at end of file diff --git a/src/deephyper_benchmark/lib/ecp_candle/Pilot1/Combo/__init__.py b/src/deephyper_benchmark/lib/ecp_candle/Pilot1/Combo/__init__.py new file mode 100644 index 0000000..7dd6fda --- /dev/null +++ b/src/deephyper_benchmark/lib/ecp_candle/Pilot1/Combo/__init__.py @@ -0,0 +1 @@ +__version__ = "0.5.1" \ No newline at end of file diff --git a/src/deephyper_benchmark/lib/ecp_candle/Pilot1/Combo/benchmark.py b/src/deephyper_benchmark/lib/ecp_candle/Pilot1/Combo/benchmark.py new file mode 100644 index 0000000..4f7fb4d --- /dev/null +++ b/src/deephyper_benchmark/lib/ecp_candle/Pilot1/Combo/benchmark.py @@ -0,0 +1,28 @@ +import os + +from deephyper_benchmark import * + +DIR = os.path.dirname(os.path.abspath(__file__)) + + +class ECPCandlePilot1Combo(Benchmark): + version = "0.5.1" + + requires = { + "makefile": {"step": "install", "type": "cmd", "cmd": "make build"}, + "py-pip-requirements": { + "step": "install", + "type": "pip", + "args": "install -r " + os.path.join(DIR, "requirements.txt"), + }, + "pkg-candle": { + "step": "load", + "type": "pythonpath", + "path": f"{DIR}/build/Benchmarks/common", + }, + "pkg-combo": { + "step": "load", + "type": "pythonpath", + "path": f"{DIR}/build/Benchmarks/Pilot1/Combo", + }, + } diff --git a/src/deephyper_benchmark/lib/ecp_candle/Pilot1/Combo/combo_default_model.yaml b/src/deephyper_benchmark/lib/ecp_candle/Pilot1/Combo/combo_default_model.yaml new file mode 100644 index 0000000..47e4ce1 --- /dev/null +++ b/src/deephyper_benchmark/lib/ecp_candle/Pilot1/Combo/combo_default_model.yaml @@ -0,0 +1,31 @@ +cell_features: ['expression'] +drug_features: ['descriptors'] +dense: [1000, 1000, 1000] +dense_feature_layers: [1000, 1000, 1000] +activation: 'relu' +loss: 'mse' +optimizer: 'adam' +scaling: 'std' +dropout: 0 +epochs: 1 +batch_size: 32 +valid_split: 0.2 +test_split: 0.2 +max_val_loss: 1.0 +learning_rate: 0.001 +base_lr: 0.001 +residual: False +reduce_lr: False +reduce_lr_factor: 0.5 +reduce_lr_patience: 5 +warmup_lr: False +early_stopping: False +early_stopping_patience: 5 +batch_normalization: False +feature_subsample: 0 +rng_seed: 2017 +save_path: 'save/combo' +gen: False +use_combo_score: False +verbose : True +timeout: 1800 diff --git a/src/deephyper_benchmark/lib/ecp_candle/Pilot1/Combo/hpo.py b/src/deephyper_benchmark/lib/ecp_candle/Pilot1/Combo/hpo.py new file mode 100644 index 0000000..429607d --- /dev/null +++ b/src/deephyper_benchmark/lib/ecp_candle/Pilot1/Combo/hpo.py @@ -0,0 +1,160 @@ +import copy +import os +import traceback + +from deephyper.evaluator import profile +from deephyper.hpo import HpProblem +from deephyper.stopper.integration.tensorflow import TFKerasStopperCallback + +from .model import run_pipeline + + +DEEPHYPER_BENCHMARK_MAX_EPOCHS = int( + os.environ.get("DEEPHYPER_BENCHMARK_MAX_EPOCHS", 50) +) +DEEPHYPER_BENCHMARK_TIMEOUT = int(os.environ.get("DEEPHYPER_BENCHMARK_TIMEOUT", 1800)) +DEEPHYPER_BENCHMARK_MOO = bool(int(os.environ.get("DEEPHYPER_BENCHMARK_MOO", 0))) + + +problem = HpProblem() + +# Model hyperparameters +ACTIVATIONS = [ + "elu", + "gelu", + "hard_sigmoid", + "linear", + "relu", + "selu", + "sigmoid", + "softplus", + "softsign", + "swish", + "tanh", +] +default_dense = [1000, 1000, 1000] +default_dense_feature_layers = [1000, 1000, 1000] + +for i in range(len(default_dense)): + problem.add_hyperparameter( + (10, 1024, "log-uniform"), + f"dense_{i}", + default_value=default_dense[i], + ) + + problem.add_hyperparameter( + (10, 1024, "log-uniform"), + f"dense_feature_layers_{i}", + default_value=default_dense_feature_layers[i], + ) + +problem.add_hyperparameter(ACTIVATIONS, "activation", default_value="relu") + +# Optimization hyperparameters +problem.add_hyperparameter( + [ + "sgd", + "rmsprop", + "adagrad", + "adadelta", + "adam", + ], + "optimizer", + default_value="sgd", +) + +problem.add_hyperparameter((0, 0.5), "dropout", default_value=0.0) +problem.add_hyperparameter((8, 512, "log-uniform"), "batch_size", default_value=32) + +problem.add_hyperparameter( + (1e-5, 1e-2, "log-uniform"), "learning_rate", default_value=0.001 +) +problem.add_hyperparameter((1e-5, 1e-2, "log-uniform"), "base_lr", default_value=0.001) +problem.add_hyperparameter([True, False], "residual", default_value=False) + +problem.add_hyperparameter([True, False], "early_stopping", default_value=False) +problem.add_hyperparameter((5, 20), "early_stopping_patience", default_value=5) + +problem.add_hyperparameter([True, False], "reduce_lr", default_value=False) +problem.add_hyperparameter((0.1, 1.0), "reduce_lr_factor", default_value=0.5) +problem.add_hyperparameter((5, 20), "reduce_lr_patience", default_value=5) + +problem.add_hyperparameter([True, False], "warmup_lr", default_value=False) +problem.add_hyperparameter([True, False], "batch_normalization", default_value=False) + +problem.add_hyperparameter( + ["mse", "mae", "logcosh", "mape", "msle", "huber"], "loss", default_value="mse" +) + +problem.add_hyperparameter(["std", "minmax", "maxabs"], "scaling", default_value="std") + + +def remap_hyperparameters(config: dict): + """Transform input configurations of hyperparameters to the format accepted by the candle benchmark.""" + dense = [] + dense_feature_layers = [] + for i in range(len(default_dense)): + key = f"dense_{i}" + dense.append(config.pop(key)) + + key = f"dense_feature_layers_{i}" + dense_feature_layers.append(config.pop(key)) + + config["dense"] = dense + config["dense_feature_layers"] = dense_feature_layers + + +@profile +def run(job, optuna_trial=None): + config = copy.deepcopy(job.parameters) + + params = { + "epochs": DEEPHYPER_BENCHMARK_MAX_EPOCHS, + "timeout": DEEPHYPER_BENCHMARK_TIMEOUT, + "verbose": False, + } + if len(config) > 0: + remap_hyperparameters(config) + params.update(config) + + if optuna_trial is None: + stopper_callback = TFKerasStopperCallback(job, monitor="val_r2", mode="max") + else: + from deephyper_benchmark.integration.optuna import KerasPruningCallback + + stopper_callback = KerasPruningCallback(optuna_trial, "val_r2") + + try: + score = run_pipeline(params, mode="valid", stopper_callback=stopper_callback) + except Exception as e: + print(traceback.format_exc()) + score = {"objective": "F"} + keys = "m:num_parameters,m:num_parameters_train,m:duration_train,m:duration_batch_inference,m:budget,m:stopped,m:train_mse,m:train_mae,m:train_r2,m:train_corr,m:valid_mse,m:valid_mae,m:valid_r2,m:valid_corr,m:test_mse,m:test_mae,m:test_r2,m:test_corr,m:lc_train_mse,m:lc_valid_mse,m:lc_train_mae,m:lc_valid_mae,m:lc_train_r2,m:lc_valid_r2" + metadata = {k.strip("m:"): None for k in keys.split(",")} + score["metadata"] = metadata + + # Handle multi-objective optimization (all maximized) + if DEEPHYPER_BENCHMARK_MOO: + if score["objective"] == "F": + score["objective"] = ["F", "F", "F"] + else: + score["objective"] = [ + score["objective"], + -score["metadata"]["num_parameters_train"], + -score["metadata"]["duration_batch_inference"], + ] + + return score + + +def evaluate(config): + """Evaluate an hyperparameter configuration on training/validation and testing data.""" + + params = { + "epochs": 100, + "timeout": 60 * 60, + "verbose": True, + } # 60 minutes per model + remap_hyperparameters(config) + params.update(config) + run_pipeline(params, mode="test") diff --git a/src/deephyper_benchmark/lib/ecp_candle/Pilot1/Combo/model.py b/src/deephyper_benchmark/lib/ecp_candle/Pilot1/Combo/model.py new file mode 100644 index 0000000..5234495 --- /dev/null +++ b/src/deephyper_benchmark/lib/ecp_candle/Pilot1/Combo/model.py @@ -0,0 +1,782 @@ +from __future__ import division, print_function + +import collections +import logging +import os +import time +import warnings + +import yaml + +try: + from yaml import CLoader as Loader +except ImportError: + from yaml import Loader + +HERE = os.path.dirname(os.path.abspath(__file__)) +DEFAULT_CONFIG = os.path.join(HERE, "combo_default_model.yaml") + +#! must be placed before "import candle" +from tensorflow import keras +from tensorflow.keras import backend as K +from tensorflow.keras import optimizers +from tensorflow.keras.callbacks import ( + Callback, + EarlyStopping, + LearningRateScheduler, + ReduceLROnPlateau, +) +from tensorflow.keras.layers import Dense, Dropout, Input +from tensorflow.keras.models import Model +from tensorflow.keras.utils import get_custom_objects + +import candle +import combo +import NCI60 +import numpy as np +import pandas as pd +import tensorflow as tf +from scipy.stats.stats import pearsonr +from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score + +from deephyper_benchmark.integration.keras import count_params +from deephyper_benchmark.utils.json_utils import array_to_json + + +logger = logging.getLogger(__name__) + +np.set_printoptions(precision=4) +tf.compat.v1.disable_eager_execution() + + +def verify_path(path): + folder = os.path.dirname(path) + if folder and not os.path.exists(folder): + os.makedirs(folder) + + +def extension_from_parameters(args): + """Construct string for saving model with annotation of parameters""" + ext = "" + ext += ".A={}".format(args.activation) + ext += ".B={}".format(args.batch_size) + ext += ".E={}".format(args.epochs) + ext += ".O={}".format(args.optimizer) + # ext += '.LEN={}'.format(args.maxlen) + ext += ".LR={}".format(args.learning_rate) + ext += ".CF={}".format("".join([x[0] for x in sorted(args.cell_features)])) + ext += ".DF={}".format("".join([x[0] for x in sorted(args.drug_features)])) + if args.feature_subsample > 0: + ext += ".FS={}".format(args.feature_subsample) + if args.dropout > 0: + ext += ".DR={}".format(args.dropout) + if args.warmup_lr: + ext += ".wu_lr" + if args.reduce_lr: + ext += ".re_lr" + if args.residual: + ext += ".res" + if args.use_landmark_genes: + ext += ".L1000" + if args.gen: + ext += ".gen" + if args.use_combo_score: + ext += ".scr" + if args.use_mean_growth: + ext += ".mg" + for i, n in enumerate(args.dense): + if n > 0: + ext += ".D{}={}".format(i + 1, n) + if args.dense_feature_layers != args.dense: + for i, n in enumerate(args.dense): + if n > 0: + ext += ".FD{}={}".format(i + 1, n) + + return ext + + +def discretize(y, bins=5): + percentiles = [100 / bins * (i + 1) for i in range(bins - 1)] + thresholds = [np.percentile(y, x) for x in percentiles] + classes = np.digitize(y, thresholds) + return classes + + +class ComboDataLoader(object): + """Load merged drug response, drug descriptors and cell line essay data""" + + def __init__( + self, + seed, + valid_split=0.2, + test_split=0.2, + shuffle=True, + cell_features=["expression"], + drug_features=["descriptors"], + response_url=None, + use_landmark_genes=False, + use_combo_score=False, + use_mean_growth=False, + preprocess_rnaseq=None, + exclude_cells=[], + exclude_drugs=[], + feature_subsample=None, + scaling="std", + scramble=False, + ): + """Initialize data merging drug response, drug descriptors and cell line essay. + Shuffle and split training and validation set + + Parameters + ---------- + seed: integer + seed for random generation + val_split : float, optional (default 0.2) + fraction of data to use in validation + cell_features: list of strings from 'expression', 'expression_5platform', 'mirna', 'proteome', 'all', 'categorical' (default ['expression']) + use one or more cell line feature sets: gene expression, microRNA, proteome + use 'all' for ['expression', 'mirna', 'proteome'] + use 'categorical' for one-hot encoded cell lines + drug_features: list of strings from 'descriptors', 'latent', 'all', 'categorical', 'noise' (default ['descriptors']) + use dragon7 descriptors, latent representations from Aspuru-Guzik's SMILES autoencoder + trained on NSC drugs, or both; use random features if set to noise + use 'categorical' for one-hot encoded drugs + shuffle : True or False, optional (default True) + if True shuffles the merged data before splitting training and validation sets + scramble: True or False, optional (default False) + if True randomly shuffle dose response data as a control + feature_subsample: None or integer (default None) + number of feature columns to use from cellline expressions and drug descriptors + use_landmark_genes: True or False + only use LINCS1000 landmark genes + use_combo_score: bool (default False) + use combination score in place of percent growth (stored in 'GROWTH' column) + use_mean_growth: bool (default False) + use mean aggregation instead of min on percent growth + scaling: None, 'std', 'minmax' or 'maxabs' (default 'std') + type of feature scaling: 'maxabs' to [-1,1], 'maxabs' to [-1, 1], 'std' for standard normalization + """ + + self._random_state = np.random.RandomState(seed) + + df = NCI60.load_combo_response( + response_url=response_url, + use_combo_score=use_combo_score, + use_mean_growth=use_mean_growth, + fraction=True, + exclude_cells=exclude_cells, + exclude_drugs=exclude_drugs, + ) + logger.info("Loaded {} unique (CL, D1, D2) response sets.".format(df.shape[0])) + + if "all" in cell_features: + self.cell_features = ["expression", "mirna", "proteome"] + else: + self.cell_features = cell_features + + if "all" in drug_features: + self.drug_features = ["descriptors", "latent"] + else: + self.drug_features = drug_features + + for fea in self.cell_features: + if fea == "expression" or fea == "rnaseq": + self.df_cell_expr = NCI60.load_cell_expression_rnaseq( + ncols=feature_subsample, + scaling=scaling, + use_landmark_genes=use_landmark_genes, + preprocess_rnaseq=preprocess_rnaseq, + ) + df = df.merge(self.df_cell_expr[["CELLNAME"]], on="CELLNAME") + elif fea == "expression_u133p2": + self.df_cell_expr = NCI60.load_cell_expression_u133p2( + ncols=feature_subsample, + scaling=scaling, + use_landmark_genes=use_landmark_genes, + ) + df = df.merge(self.df_cell_expr[["CELLNAME"]], on="CELLNAME") + elif fea == "expression_5platform": + self.df_cell_expr = NCI60.load_cell_expression_5platform( + ncols=feature_subsample, + scaling=scaling, + use_landmark_genes=use_landmark_genes, + ) + df = df.merge(self.df_cell_expr[["CELLNAME"]], on="CELLNAME") + elif fea == "mirna": + self.df_cell_mirna = NCI60.load_cell_mirna( + ncols=feature_subsample, scaling=scaling + ) + df = df.merge(self.df_cell_mirna[["CELLNAME"]], on="CELLNAME") + elif fea == "proteome": + self.df_cell_prot = NCI60.load_cell_proteome( + ncols=feature_subsample, scaling=scaling + ) + df = df.merge(self.df_cell_prot[["CELLNAME"]], on="CELLNAME") + elif fea == "categorical": + df_cell_ids = df[["CELLNAME"]].drop_duplicates() + cell_ids = df_cell_ids["CELLNAME"].map(lambda x: x.replace(":", ".")) + df_cell_cat = pd.get_dummies(cell_ids) + df_cell_cat.index = df_cell_ids["CELLNAME"] + self.df_cell_cat = df_cell_cat.reset_index() + + for fea in self.drug_features: + if fea == "descriptors": + self.df_drug_desc = NCI60.load_drug_descriptors( + ncols=feature_subsample, scaling=scaling + ) + df = df[ + df["NSC1"].isin(self.df_drug_desc["NSC"]) + & df["NSC2"].isin(self.df_drug_desc["NSC"]) + ] + elif fea == "latent": + self.df_drug_auen = NCI60.load_drug_autoencoded_AG( + ncols=feature_subsample, scaling=scaling + ) + df = df[ + df["NSC1"].isin(self.df_drug_auen["NSC"]) + & df["NSC2"].isin(self.df_drug_auen["NSC"]) + ] + elif fea == "categorical": + df_drug_ids = df[["NSC1"]].drop_duplicates() + df_drug_ids.columns = ["NSC"] + drug_ids = df_drug_ids["NSC"] + df_drug_cat = pd.get_dummies(drug_ids) + df_drug_cat.index = df_drug_ids["NSC"] + self.df_drug_cat = df_drug_cat.reset_index() + elif fea == "noise": + ids1 = df[["NSC1"]].drop_duplicates().rename(columns={"NSC1": "NSC"}) + ids2 = df[["NSC2"]].drop_duplicates().rename(columns={"NSC2": "NSC"}) + df_drug_ids = pd.concat([ids1, ids2]).drop_duplicates() + noise = np.random.normal(size=(df_drug_ids.shape[0], 500)) + df_rand = pd.DataFrame( + noise, + index=df_drug_ids["NSC"], + columns=["RAND-{:03d}".format(x) for x in range(500)], + ) + self.df_drug_rand = df_rand.reset_index() + + logger.info( + "Filtered down to {} rows with matching information.".format(df.shape[0]) + ) + + ids1 = df[["NSC1"]].drop_duplicates().rename(columns={"NSC1": "NSC"}) + ids2 = df[["NSC2"]].drop_duplicates().rename(columns={"NSC2": "NSC"}) + df_drug_ids = pd.concat([ids1, ids2]).drop_duplicates().reset_index(drop=True) + + n_drugs = df_drug_ids.shape[0] + n_valid_drugs = int(n_drugs * valid_split) + n_test_drugs = int(n_drugs * test_split) + n_train_drugs = n_drugs - n_valid_drugs - n_test_drugs + + logger.info("Unique cell lines: {}".format(df["CELLNAME"].nunique())) + logger.info("Unique drugs: {}".format(n_drugs)) + + if shuffle: + df = df.sample(frac=1.0, random_state=seed).reset_index(drop=True) + df_drug_ids = df_drug_ids.sample(frac=1.0, random_state=seed).reset_index( + drop=True + ) + + self.df_response = df + self.df_drug_ids = df_drug_ids + + self.train_drug_ids = df_drug_ids["NSC"][:n_train_drugs] + self.valid_drug_ids = df_drug_ids["NSC"][ + n_train_drugs : n_train_drugs + n_valid_drugs + ] + self.test_drug_ids = df_drug_ids["NSC"][ + n_train_drugs + n_valid_drugs : n_train_drugs + n_valid_drugs + n_test_drugs + ] + + if scramble: + growth = df[["GROWTH"]] + random_growth = growth.iloc[ + self._random_state.permutation(np.arange(growth.shape[0])) + ].reset_index() + self.df_response[["GROWTH"]] = random_growth["GROWTH"] + logger.warn("Randomly shuffled dose response growth values.") + + logger.info("Distribution of dose response:") + logger.info(self.df_response[["GROWTH"]].describe()) + + self.total = df.shape[0] + self.n_valid = int(self.total * valid_split) + self.n_test = int(self.total * test_split) + self.n_train = self.total - self.n_valid - self.n_test + logger.info( + "Rows in train: {}, valid: {}, test: {}".format( + self.n_train, self.n_valid, self.n_test + ) + ) + + self.cell_df_dict = { + "expression": "df_cell_expr", + "expression_5platform": "df_cell_expr", + "expression_u133p2": "df_cell_expr", + "rnaseq": "df_cell_expr", + "mirna": "df_cell_mirna", + "proteome": "df_cell_prot", + "categorical": "df_cell_cat", + } + + self.drug_df_dict = { + "descriptors": "df_drug_desc", + "latent": "df_drug_auen", + "categorical": "df_drug_cat", + "noise": "df_drug_rand", + } + + self.input_features = collections.OrderedDict() + self.feature_shapes = {} + for fea in self.cell_features: + feature_type = "cell." + fea + feature_name = "cell." + fea + df_cell = getattr(self, self.cell_df_dict[fea]) + self.input_features[feature_name] = feature_type + self.feature_shapes[feature_type] = (df_cell.shape[1] - 1,) + + for drug in ["drug1", "drug2"]: + for fea in self.drug_features: + feature_type = "drug." + fea + feature_name = drug + "." + fea + df_drug = getattr(self, self.drug_df_dict[fea]) + self.input_features[feature_name] = feature_type + self.feature_shapes[feature_type] = (df_drug.shape[1] - 1,) + + logger.info("Input features shapes:") + for k, v in self.input_features.items(): + logger.info(" {}: {}".format(k, self.feature_shapes[v])) + + self.input_dim = sum( + [np.prod(self.feature_shapes[x]) for x in self.input_features.values()] + ) + logger.info("Total input dimensions: {}".format(self.input_dim)) + + def load_data_all(self, switch_drugs=False): + df_all = self.df_response + y_all = df_all["GROWTH"].values + x_all_list = [] + + for fea in self.cell_features: + df_cell = getattr(self, self.cell_df_dict[fea]) + df_x_all = pd.merge( + df_all[["CELLNAME"]], df_cell, on="CELLNAME", how="left" + ) + x_all_list.append(df_x_all.drop(["CELLNAME"], axis=1).values) + + drugs = ["NSC1", "NSC2"] + if switch_drugs: + drugs = ["NSC2", "NSC1"] + + for drug in drugs: + for fea in self.drug_features: + df_drug = getattr(self, self.drug_df_dict[fea]) + df_x_all = pd.merge( + df_all[[drug]], df_drug, left_on=drug, right_on="NSC", how="left" + ) + x_all_list.append(df_x_all.drop([drug, "NSC"], axis=1).values) + + return x_all_list, y_all, df_all + + def load_data_by_index(self, train_index, valid_index, test_index): + x_all_list, y_all, df_all = self.load_data_all() + + x_train_list = [x[train_index] for x in x_all_list] + x_valid_list = [x[valid_index] for x in x_all_list] + x_test_list = [x[test_index] for x in x_all_list] + + y_train = y_all[train_index] + y_valid = y_all[valid_index] + y_test = y_all[test_index] + + df_train = df_all.iloc[train_index, :] + df_valid = df_all.iloc[valid_index, :] + df_test = df_all.iloc[test_index, :] + + return ( + x_train_list, + y_train, + x_valid_list, + y_valid, + x_test_list, + y_test, + df_train, + df_valid, + df_test, + ) + + def load_data(self): + train_index = range(self.n_train) + valid_index = range(self.n_train, self.n_train + self.n_valid) + test_index = range( + self.n_train + self.n_valid, self.n_train + self.n_valid + self.n_test + ) + return self.load_data_by_index(train_index, valid_index, test_index) + + +def test_loader(loader): + ( + x_train_list, + y_train, + x_val_list, + y_val, + x_test_list, + y_test, + _, + _, + _, + ) = loader.load_data() + print("x_train shapes:") + for x in x_train_list: + print(x.shape) + print("y_train shape:", y_train.shape) + + print("x_valid shapes:") + for x in x_val_list: + print(x.shape) + print("y_valid shape:", y_val.shape) + + print("x_test shapes:") + for x in x_test_list: + print(x.shape) + print("y_test shape:", y_test.shape) + + +def r2(y_true, y_pred): + SS_res = K.sum(K.square(y_true - y_pred)) + SS_tot = K.sum(K.square(y_true - K.mean(y_true))) + return 1 - SS_res / (SS_tot + K.epsilon()) + + +def mae(y_true, y_pred): + return keras.metrics.mean_absolute_error(y_true, y_pred) + + +def mse(y_true, y_pred): + return keras.metrics.mean_squared_error(y_true, y_pred) + + +def evaluate_prediction(y_true, y_pred): + mse = mean_squared_error(y_true, y_pred) + mae = mean_absolute_error(y_true, y_pred) + r2 = r2_score(y_true, y_pred) + corr, _ = pearsonr(y_true, y_pred) + return {"mse": mse, "mae": mae, "r2": r2, "corr": corr} + + +def log_evaluation(metric_outputs, description="Comparing y_true and y_pred:"): + logger.info(description) + for metric, value in metric_outputs.items(): + logger.info(" {}: {:.4f}".format(metric, value)) + + +class LoggingCallback(Callback): + def __init__(self, print_fcn=print): + Callback.__init__(self) + self.print_fcn = print_fcn + + def on_epoch_end(self, epoch, logs={}): + msg = "[Epoch: %i] %s" % ( + epoch, + ", ".join("%s: %f" % (k, v) for k, v in sorted(logs.items())), + ) + self.print_fcn(msg) + + +class PermanentDropout(Dropout): + def __init__(self, rate, **kwargs): + super(PermanentDropout, self).__init__(rate, **kwargs) + self.uses_learning_phase = False + + def call(self, x, mask=None): + if 0.0 < self.rate < 1.0: + noise_shape = self._get_noise_shape(x) + x = K.dropout(x, self.rate, noise_shape) + return x + + +class ModelRecorder(Callback): + def __init__(self, save_all_models=False): + Callback.__init__(self) + self.save_all_models = save_all_models + get_custom_objects()["PermanentDropout"] = PermanentDropout + + def on_train_begin(self, logs={}): + self.val_losses = [] + self.best_val_loss = np.Inf + self.best_model = None + + def on_epoch_end(self, epoch, logs={}): + val_loss = logs.get("val_loss") + self.val_losses.append(val_loss) + if val_loss < self.best_val_loss: + self.best_model = keras.models.clone_model(self.model) + self.best_val_loss = val_loss + + +def build_feature_model( + input_shape, + name="", + dense_layers=[1000, 1000], + activation="relu", + residual=False, + dropout_rate=0, + permanent_dropout=True, +): + x_input = Input(shape=input_shape) + h = x_input + for i, layer in enumerate(dense_layers): + x = h + h = Dense(layer, activation=activation)(h) + if dropout_rate > 0: + if permanent_dropout: + h = PermanentDropout(dropout_rate)(h) + else: + h = Dropout(dropout_rate)(h) + if residual: + try: + h = keras.layers.add([h, x]) + except ValueError: + pass + model = Model(x_input, h, name=name) + return model + + +def build_model(loader, args, verbose=False): + input_models = {} + dropout_rate = args.dropout + permanent_dropout = True + for fea_type, shape in loader.feature_shapes.items(): + box = build_feature_model( + input_shape=shape, + name=fea_type, + dense_layers=args.dense_feature_layers, + dropout_rate=dropout_rate, + permanent_dropout=permanent_dropout, + ) + if verbose: + box.summary() + input_models[fea_type] = box + + inputs = [] + encoded_inputs = [] + for fea_name, fea_type in loader.input_features.items(): + shape = loader.feature_shapes[fea_type] + fea_input = Input(shape, name="input." + fea_name) + inputs.append(fea_input) + input_model = input_models[fea_type] + encoded = input_model(fea_input) + encoded_inputs.append(encoded) + + merged = keras.layers.concatenate(encoded_inputs) + + h = merged + for i, layer in enumerate(args.dense): + x = h + h = Dense(layer, activation=args.activation)(h) + if dropout_rate > 0: + if permanent_dropout: + h = PermanentDropout(dropout_rate)(h) + else: + h = Dropout(dropout_rate)(h) + if args.residual: + try: + h = keras.layers.add([h, x]) + except ValueError: + pass + output = Dense(1)(h) + + return Model(inputs, output) + + +def initialize_parameters(default_model="combo_default_model.txt"): + # Build benchmark object + comboBmk = combo.BenchmarkCombo( + combo.file_path, + default_model, + "keras", + prog="combo_baseline", + desc="Build neural network based models to predict tumor response to drug pairs.", + ) + + # Initialize parameters + gParameters = candle.finalize_parameters(comboBmk) + + return gParameters + + +def yaml_load(path): + with open(path, "r") as f: + yaml_data = yaml.load(f, Loader=Loader) + return yaml_data + + +def run_pipeline(config: dict = None, mode="valid", stopper_callback=None): + # Default Config from original Benchmark + params = initialize_parameters() + + # Default Config from our Benchmark + params.update(yaml_load(DEFAULT_CONFIG)) + params.pop("val_split") + + # Ingest input configuration + if config: + params.update(config) + + args = candle.ArgumentStruct(**params) + seed = args.rng_seed + candle.set_seed(seed) + + loader = ComboDataLoader( + seed=args.rng_seed, + valid_split=args.valid_split, + test_split=args.test_split, + cell_features=args.cell_features, + drug_features=args.drug_features, + use_mean_growth=args.use_mean_growth, + response_url=args.response_url, + use_landmark_genes=args.use_landmark_genes, + preprocess_rnaseq=args.preprocess_rnaseq, + exclude_cells=args.exclude_cells, + exclude_drugs=args.exclude_drugs, + use_combo_score=args.use_combo_score, + scaling=args.scaling, + ) + + # test_loader(loader) + + model = build_model(loader, args, verbose=args.verbose) + + def warmup_scheduler(epoch): + lr = args.learning_rate or base_lr * args.batch_size / 100 + if epoch <= 5: + K.set_value(model.optimizer.lr, (base_lr * (5 - epoch) + lr * epoch) / 5) + logger.debug("Epoch {}: lr={}".format(epoch, K.get_value(model.optimizer.lr))) + return K.get_value(model.optimizer.lr) + + model = build_model(loader, args) + + optimizer = optimizers.deserialize({"class_name": args.optimizer, "config": {}}) + base_lr = args.base_lr or K.get_value(optimizer.lr) + if args.learning_rate: + K.set_value(optimizer.lr, args.learning_rate) + + model.compile(loss=args.loss, optimizer=optimizer, metrics=[mse, mae, r2]) + + # calculate trainable and non-trainable params + params.update(candle.compute_trainable_params(model)) + + early_stopping = EarlyStopping( + monitor="val_loss", patience=args.early_stopping_patience + ) + + timeout_monitor = candle.TerminateOnTimeOut(params["timeout"]) + + reduce_lr = ReduceLROnPlateau( + monitor="val_loss", + factor=args.reduce_lr_factor, + patience=args.reduce_lr_patience, + min_lr=0.00001, + ) + warmup_lr = LearningRateScheduler(warmup_scheduler) + + callbacks = [timeout_monitor] + if args.early_stopping: + callbacks.append(early_stopping) + if args.reduce_lr: + callbacks.append(reduce_lr) + if args.warmup_lr: + callbacks.append(warmup_lr) + + if stopper_callback: + callbacks.append(stopper_callback) + + ( + x_train_list, + y_train, + x_valid_list, + y_valid, + x_test_list, + y_test, + _, + _, + _, + ) = loader.load_data() + training_data = (x_train_list, y_train) + validation_data = (x_valid_list, y_valid) + + num_parameters_info = count_params(model) + + # Training + timestamp_duration = time.time() + history = model.fit( + *training_data, + batch_size=args.batch_size, + shuffle=args.shuffle, + epochs=args.epochs, + validation_data=validation_data, + callbacks=callbacks, + verbose=args.verbose, + ).history + duration_train = time.time() - timestamp_duration + + + timestamp_duration = time.time() + y_train_pred = model.predict(x_train_list, batch_size=args.batch_size).flatten() + duration_batch_inference = (time.time() - timestamp_duration) / np.ceil(len(y_train_pred) / args.batch_size) + + scores_train = evaluate_prediction(y_train, y_train_pred) + y_valid_pred = model.predict(x_valid_list, batch_size=args.batch_size).flatten() + scores_valid = evaluate_prediction(y_valid, y_valid_pred) + y_test_pred = model.predict(x_test_list, batch_size=args.batch_size).flatten() + scores_test = evaluate_prediction(y_test, y_test_pred) + + all_scores = {} + all_scores.update({f"train_{k}": float(v) for k, v in scores_train.items()}) + all_scores.update({f"valid_{k}": float(v) for k, v in scores_valid.items()}) + all_scores.update({f"test_{k}": float(v) for k, v in scores_test.items()}) + + if K.backend() == "tensorflow": + K.clear_session() + + objective = all_scores["valid_r2"] # validation R2 + objective = max(-1, objective) + + # collect learning curves + lc_train_mse = array_to_json( + np.asarray([[i + 1, l] for i, l in enumerate(history["mse"])]) + ) + lc_valid_mse = array_to_json( + np.asarray([[i + 1, l] for i, l in enumerate(history["val_mse"])]) + ) + + lc_train_mae = array_to_json( + np.asarray([[i + 1, l] for i, l in enumerate(history["mae"])]) + ) + lc_valid_mae = array_to_json( + np.asarray([[i + 1, l] for i, l in enumerate(history["val_mae"])]) + ) + + lc_train_r2 = array_to_json( + np.asarray([[i + 1, l] for i, l in enumerate(history["r2"])]) + ) + lc_valid_r2 = array_to_json( + np.asarray([[i + 1, l] for i, l in enumerate(history["val_r2"])]) + ) + + metadata = { + "num_parameters": num_parameters_info["num_parameters"], + "num_parameters_train": num_parameters_info["num_parameters_train"], + "duration_train": duration_train, + "duration_batch_inference": duration_batch_inference, + "budget": len(history["loss"]), + "stopped": len(history["loss"]) < args.epochs, + "lc_train_mse": lc_train_mse, + "lc_valid_mse": lc_valid_mse, + "lc_train_mae": lc_train_mae, + "lc_valid_mae": lc_valid_mae, + "lc_train_r2": lc_train_r2, + "lc_valid_r2": lc_valid_r2, + } + metadata.update(all_scores) + return {"objective": objective, "metadata": metadata} diff --git a/src/deephyper_benchmark/lib/ecp_candle/Pilot1/Combo/requirements.txt b/src/deephyper_benchmark/lib/ecp_candle/Pilot1/Combo/requirements.txt new file mode 100644 index 0000000..8c168b6 --- /dev/null +++ b/src/deephyper_benchmark/lib/ecp_candle/Pilot1/Combo/requirements.txt @@ -0,0 +1,5 @@ +astropy +patsy +statsmodels +numpy<=1.22 # for compatibility with numba.cuda +numba~=0.55.2 \ No newline at end of file diff --git a/src/deephyper_benchmark/lib/fnobench/README.md b/src/deephyper_benchmark/lib/fnobench/README.md new file mode 100644 index 0000000..fd26692 --- /dev/null +++ b/src/deephyper_benchmark/lib/fnobench/README.md @@ -0,0 +1,64 @@ +# FNOBench: Fourier Neural Operator Benchmark + +> **Warning** +> Work in progress, this benchmark is not yet ready. + +This is a simplified way of using deephyper to conduct the hyperparameter tuning for the TFNO model to solve the darcy flow. The FNO and the TFNO are models for training neural operators and they can be designed using the neural operator package. + +The Deep Hyper is a package that automates the design of neural networks using Hyperparameter Tuning and Neural Architecture Search. + +## Installation + +To use the benchmark follow this example set of instructions: + +```console +python -c "import deephyper_benchmark as dhb; dhb.install('FNOBench');" +``` + +Then load the benchmark from Python: + +```python +import deephyper_benchmark as dhb +dhb.load("FNOBench") +from deephyper_benchmark.lib.fnobench import hpo +``` + +## Configuration + +... + +## Metadata + +The current set of returned metadata is: + +- [x] `num_parameters` +- [ ] `num_parameters_train` +- [x] `duration_train` +- [ ] `duration_batch_inference` +- [x] `budget` +- [ ] `stopped` +- [x] `train_loss`: the training loss which is the H1Loss +- [x] `valid_loss`: the validation loss which is the LpLoss +- [ ] `flops` +- [ ] `latency` +- [ ] `lc_train_loss` +- [ ] `lc_valid_loss` + +## Other Details + +### Training Procedure + +We especially edit the Trainer class from the neural operator package to include and return the validation loss and to optimize it. + +We also specifically edit the function that loads the data to accommodate the validation data and the test data. Note that the training and testing data file has been included, and is available for download + +### Optimized Losses + +The losses employed by the neural operator package are the H1Loss and the LpLoss and we stick with those in this benchmark for the sake of consistency. + +### Data Splitting Policy + + +The data we use for this test is the darcy flow test, and we have it in the data folder, it is generated by the load_darcy_flow_small function. The actual function that +loads and splits the data into train, validation and test data is the load_darcy_pt, and how it achieves this is to load the data from the file +darcy_train and split it in this form, the train data takes 80% of the data, while the validation data takes 20% and saves it into (x_train, y_train); which is loaded into the trainloader, and (x_valid, y_valid); which is loaded into the validation_loader. Note that we train our model in a particular resolution (16), but we can carry out the test in other resolutions, (for this project we test in 16, 32). To get the test data, we already have the test data in the file darcy_test, and we load it similar to how we load the train data, but here we do not split the test data at all, so we have (x_test, y_test) loaded into the testloaders; testloaderS because we load for resolution 16 and resolution 32. \ No newline at end of file diff --git a/src/deephyper_benchmark/lib/fnobench/__init__.py b/src/deephyper_benchmark/lib/fnobench/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/deephyper_benchmark/lib/fnobench/benchmark.py b/src/deephyper_benchmark/lib/fnobench/benchmark.py new file mode 100644 index 0000000..f9e85a6 --- /dev/null +++ b/src/deephyper_benchmark/lib/fnobench/benchmark.py @@ -0,0 +1,17 @@ +import os + +from deephyper_benchmark import * + +DIR = os.path.dirname(os.path.abspath(__file__)) + + +class FNOBenchmark(Benchmark): + version = "0.0.1" + + requires = { + "py-pip-requirements": { + "step": "install", + "type": "pip", + "args": "install -r " + os.path.join(DIR, "requirements.txt"), + }, + } diff --git a/src/deephyper_benchmark/lib/fnobench/data.py b/src/deephyper_benchmark/lib/fnobench/data.py new file mode 100644 index 0000000..b294151 --- /dev/null +++ b/src/deephyper_benchmark/lib/fnobench/data.py @@ -0,0 +1,227 @@ +from pathlib import Path + +import torch +from neuralop.datasets.tensor_dataset import TensorDataset +from neuralop.datasets.transforms import PositionalEmbedding +from neuralop.utils import UnitGaussianNormalizer + + +# this function returns the train_loader, validation_loader, test_loader and output encoder for the darcy_flow dataset +# it was originally written in the neuralop codes but we slightly edit it to include the validation_loader +# which is 0.2% of the whole training_data +def load_darcy_flow_small( + n_train, + n_tests, + batch_size, + test_batch_sizes, + test_resolutions=[16, 32], + grid_boundaries=[[0, 1], [0, 1]], + positional_encoding=True, + encode_input=False, + encode_output=True, + encoding="channel-wise", + channel_dim=1, +): + """Loads a small Darcy-Flow dataset + + Training contains 1000 samples in resolution 16x16. + Testing contains 100 samples at resolution 16x16 and + 50 samples at resolution 32x32. + + Parameters + ---------- + n_train : int + n_tests : int + batch_size : int + test_batch_sizes : int list + test_resolutions : int list, default is [16, 32], + grid_boundaries : int list, default is [[0,1],[0,1]], + positional_encoding : bool, default is True + encode_input : bool, default is False + encode_output : bool, default is True + encoding : 'channel-wise' + channel_dim : int, default is 1 + where to put the channel dimension, defaults size is batch, channel, height, width + + Returns + ------- + training_dataloader, testing_dataloaders + + training_dataloader : torch DataLoader + testing_dataloaders : dict (key: DataLoader) + """ + for res in test_resolutions: + if res not in [16, 32]: + raise ValueError( + f"Only 32 and 64 are supported for test resolution, but got {test_resolutions=}" + ) + path = Path(__file__).resolve().parent.joinpath("data") + return load_darcy_pt( + str(path), + n_train=n_train, + n_tests=n_tests, + batch_size=batch_size, + test_batch_sizes=test_batch_sizes, + test_resolutions=test_resolutions, + train_resolution=16, + grid_boundaries=grid_boundaries, + positional_encoding=positional_encoding, + encode_input=encode_input, + encode_output=encode_output, + encoding=encoding, + channel_dim=channel_dim, + ) + + +def load_darcy_pt( + data_path, + n_train, + n_tests, + batch_size, + test_batch_sizes, + test_resolutions=[32], + train_resolution=32, + grid_boundaries=[[0, 1], [0, 1]], + positional_encoding=True, + encode_input=False, + encode_output=True, + encoding="channel-wise", + channel_dim=1, +): + """Load the Navier-Stokes dataset""" + n_val = int(0.2 * n_train) + data = torch.load( + Path(data_path).joinpath(f"darcy_train_{train_resolution}.pt").as_posix() + ) + x_train = ( + data["x"][0 : n_train - n_val, :, :] + .unsqueeze(channel_dim) + .type(torch.float32) + .clone() + ) + y_train = data["y"][0 : n_train - n_val, :, :].unsqueeze(channel_dim).clone() + x_val = ( + data["x"][n_train - n_val : n_train, :, :] + .unsqueeze(channel_dim) + .type(torch.float32) + .clone() + ) + y_val = data["y"][n_train - n_val : n_train, :, :].unsqueeze(channel_dim).clone() + del data + + idx = test_resolutions.index(train_resolution) + test_resolutions.pop(idx) + n_test = n_tests.pop(idx) + test_batch_size = test_batch_sizes.pop(idx) + + data = torch.load( + Path(data_path).joinpath(f"darcy_test_{train_resolution}.pt").as_posix() + ) + x_test = data["x"][:n_test, :, :].unsqueeze(channel_dim).type(torch.float32).clone() + y_test = data["y"][:n_test, :, :].unsqueeze(channel_dim).clone() + del data + + if encode_input: + if encoding == "channel-wise": + reduce_dims = list(range(x_train.ndim)) + elif encoding == "pixel-wise": + reduce_dims = [0] + + input_encoder = UnitGaussianNormalizer(x_train, reduce_dim=reduce_dims) + x_train = input_encoder.encode(x_train) + x_test = input_encoder.encode(x_test.contiguous()) + else: + input_encoder = None + + if encode_output: + if encoding == "channel-wise": + reduce_dims = list(range(y_train.ndim)) + # reduce_d = list(range(y_val.ndim)) + elif encoding == "pixel-wise": + reduce_dims = [0] + # reduce_d=[0] + + output_encoder = UnitGaussianNormalizer(y_train, reduce_dim=reduce_dims) + y_train = output_encoder.encode(y_train) + else: + output_encoder = None + + train_db = TensorDataset( + x_train, + y_train, + transform_x=PositionalEmbedding(grid_boundaries, 0) + if positional_encoding + else None, + ) + train_loader = torch.utils.data.DataLoader( + train_db, + batch_size=batch_size, + shuffle=True, + num_workers=0, + pin_memory=True, + persistent_workers=False, + ) + val_db = TensorDataset( + x_val, + y_val, + transform_x=PositionalEmbedding(grid_boundaries, 0) + if positional_encoding + else None, + ) + val_loader = torch.utils.data.DataLoader( + val_db, + batch_size=batch_size, + shuffle=False, + num_workers=0, + pin_memory=True, + persistent_workers=False, + ) + test_db = TensorDataset( + x_test, + y_test, + transform_x=PositionalEmbedding(grid_boundaries, 0) + if positional_encoding + else None, + ) + test_loader = torch.utils.data.DataLoader( + test_db, + batch_size=test_batch_size, + shuffle=False, + num_workers=0, + pin_memory=True, + persistent_workers=False, + ) + test_loaders = {train_resolution: test_loader} + for res, n_test, test_batch_size in zip( + test_resolutions, n_tests, test_batch_sizes + ): + print( + f"Loading test db at resolution {res} with {n_test} samples and batch-size={test_batch_size}" + ) + data = torch.load(Path(data_path).joinpath(f"darcy_test_{res}.pt").as_posix()) + x_test = ( + data["x"][:n_test, :, :].unsqueeze(channel_dim).type(torch.float32).clone() + ) + y_test = data["y"][:n_test, :, :].unsqueeze(channel_dim).clone() + del data + if input_encoder is not None: + x_test = input_encoder.encode(x_test) + + test_db = TensorDataset( + x_test, + y_test, + transform_x=PositionalEmbedding(grid_boundaries, 0) + if positional_encoding + else None, + ) + test_loader = torch.utils.data.DataLoader( + test_db, + batch_size=test_batch_size, + shuffle=False, + num_workers=0, + pin_memory=True, + persistent_workers=False, + ) + test_loaders[res] = test_loader + + return train_loader, val_loader, test_loaders, output_encoder diff --git a/src/deephyper_benchmark/lib/fnobench/data/darcy_test_16.pt b/src/deephyper_benchmark/lib/fnobench/data/darcy_test_16.pt new file mode 100644 index 0000000..5ad70d6 Binary files /dev/null and b/src/deephyper_benchmark/lib/fnobench/data/darcy_test_16.pt differ diff --git a/src/deephyper_benchmark/lib/fnobench/data/darcy_test_32.pt b/src/deephyper_benchmark/lib/fnobench/data/darcy_test_32.pt new file mode 100644 index 0000000..582350c Binary files /dev/null and b/src/deephyper_benchmark/lib/fnobench/data/darcy_test_32.pt differ diff --git a/src/deephyper_benchmark/lib/fnobench/data/darcy_train_16.pt b/src/deephyper_benchmark/lib/fnobench/data/darcy_train_16.pt new file mode 100644 index 0000000..95c2597 Binary files /dev/null and b/src/deephyper_benchmark/lib/fnobench/data/darcy_train_16.pt differ diff --git a/src/deephyper_benchmark/lib/fnobench/hpo.py b/src/deephyper_benchmark/lib/fnobench/hpo.py new file mode 100644 index 0000000..8d3f7a9 --- /dev/null +++ b/src/deephyper_benchmark/lib/fnobench/hpo.py @@ -0,0 +1,104 @@ +import torch +from deephyper.evaluator import profile +from deephyper.hpo import HpProblem + +from .model import build_and_train_model + +# now we define the hyperparameters with which we aim to get the best configuration for solving the problem +problem = HpProblem() +# this is for dimension 2 darcy flow, below we list the hyperparameters and where they are being used +# 1. n_modes_height and n_modes_width are the number of modes to keep in fourier layer along each dimension, if we are working with 3 dimensions, we have height, width, and length. It is called when you are defining the model e.g model =TFNO(n_modes=...),The dimensionality of the TFNO is inferred from ``len(n_modes)`` +problem.add_hyperparameter((2, 19), "n_modes_height", default_value=16) +problem.add_hyperparameter((2, 19), "n_modes_width", default_value=16) +# 2. hidden_channels: width of the FNO (i.e. number of channels). they go into the model definition +problem.add_hyperparameter((1, 128), "hidden_channels", default_value=32) +# 3. lifting_channels: number of hidden channels of the lifting block of the FNO, they go into the model definition +problem.add_hyperparameter((1, 1024), "lifting_channels", default_value=256) +# 4 projection_channels:number of hidden channels of the projection block of the FNO, they go into the model definition +problem.add_hyperparameter((1, 1024), "projection_channels", default_value=64) +# 5. use_mlp: Whether to use an MLP layer after each FNO block, they go into the model definition +problem.add_hyperparameter([True, False], "use_mlp", default_value=False) +# 6 mlp['dropout']: parameter of the MLP, they go into the model definition +problem.add_hyperparameter((0.0, 1.0), "mlp['dropout']", default_value=0) +# 7 mlp['expansion']: MLP parameter, they go into the model definition +problem.add_hyperparameter((0.0, 3.0), "mlp['expansion']", default_value=0.5) +# 8 rank Rank of the tensor factorization of the Fourier weights, they go into the model definition +problem.add_hyperparameter((0.0, 1.0), "rank", default_value=1.0) +# 9 factorization:Tensor factorization of the parameters weight to use, they go into the model definition +problem.add_hyperparameter( + ["tucker", "cp", "tt", "dense"], "factorization", default_value="dense" +) +# 10 learning_rate: this is the learning_rate, goes into the optimizer definition +problem.add_hyperparameter( + (1e-6, 1e-2, "log-uniform"), "opt_learning_rate", default_value=5e-3 +) +# 11 batch_size: the batch size for the training, goes into the train_loader for loading the datasel +problem.add_hyperparameter((2, 64), "data_batch_size", default_value=16) +# 12 weight_decay: this is the weight_decay, goes into the optimizer definition +problem.add_hyperparameter( + (1e-6, 1e-2, "log-uniform"), "opt_weight_decay", default_value=1e-4 +) +# 13 n_layers:Number of Fourier Layers +problem.add_hyperparameter((1, 8), "n_layers", default_value=4) +# 14 epochs: number of epochs, goes into the trainer +problem.add_hyperparameter((1, 1000), "opt_n_epochs", default_value=300) +# 15 scheduler_T_max: this goes into the scheduler, it's the max number of iterations in the scheduler +problem.add_hyperparameter([500], "opt_scheduler_T_max", default_value=500) +# 16 n_train: no of data samples to train the model, goes into the train_loader for loading the dataset +problem.add_hyperparameter([1000], "data_n_train", default_value=1000) +# the next set of hyperparameters are data related and we're keeping them constant for now,and for the particular problem +problem.add_hyperparameter([16], "data_train_resolution", default_value=16) +problem.add_hyperparameter([True], "data_positional_encoding", default_value=True) +problem.add_hyperparameter([True], "data_encode_input", default_value=True) +problem.add_hyperparameter([False], "data_encode_output", default_value=False) +problem.add_hyperparameter([666], "distributed_seed", default_value=666) +problem.add_hyperparameter([3], "data_channels", default_value=3) +# implementation is about how the factorization is done, +problem.add_hyperparameter( + ["factorized", "reconstructed"], "implementation", default_value="factorized" +) +# By default, None, otherwise tanh is used before FFT in the FNO block +problem.add_hyperparameter(["None", "tanh"], "stabilizer", default_value=None) +# if 'full', the FNO Block runs in full precision, +# if 'half', the FFT, contraction, and inverse FFT run in half precision +# if 'mixed', the contraction and inverse FFT run in half precision +problem.add_hyperparameter( + ["full", "half", "mixed"], "fno_block_precision", default_value="half" +) +# Type of skip connection to use, +problem.add_hyperparameter( + ["soft-gating", "identity", "linear"], "skip", default_value="linear" +) +# the scheduler_patience, used for only ReduceLROnPlateau, we're keeping it constant +problem.add_hyperparameter([5], "opt_scheduler_patience", default_value=5) +# type of scheduler to use +problem.add_hyperparameter( + ["StepLR", "CosineAnnealingLR", "ReduceLROnPlateau"], + "opt_scheduler", + default_value="StepLR", +) +# step_size for the optimizer +problem.add_hyperparameter((10, 100), "opt_step_size", default_value=60) +# gamma for the constant, we're keeping it constant +problem.add_hyperparameter([0.5], "opt_gamma", default_value=0.5) + + +@profile +def run(config): + # important to avoid memory explosion + torch.cuda.empty_cache() + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + + _, history = build_and_train_model(config, device, verbose=0) + score = { + "objective": -history["val_loss"][-1], + "metadata": { + "train_err": history["train_err"][-1], + "num_parameters": history["num_parameters"], + "budget": history["Epochs"][-1], + "val_loss": history["val_loss"][-1], + "duration_train": sum(history["train_time"]), + }, + } + + return score diff --git a/src/deephyper_benchmark/lib/fnobench/model.py b/src/deephyper_benchmark/lib/fnobench/model.py new file mode 100644 index 0000000..8c38e42 --- /dev/null +++ b/src/deephyper_benchmark/lib/fnobench/model.py @@ -0,0 +1,560 @@ +import sys +from timeit import default_timer + +import matplotlib.pyplot as plt +import neuralop.mpu.comm as comm +import numpy as np +import torch +import wandb +from neuralop import H1Loss, LpLoss +from neuralop.models import TFNO2d +from neuralop.training.patching import MultigridPatching2D +from neuralop.utils import count_params + +from .data import load_darcy_flow_small + + +# this is the trainer class that trains the neural operators, +# it is from the neuralop package, but we had to slightly +# modify it in order to +# 1. calculate the validation loss instead of the testing loss +# 2. return the history of the training, including the time for each epoch, the training loss for each epoch +# and the validation loss for each epoch +class Trainer: + def __init__( + self, + model, + n_epochs, + wandb_log=True, + device=None, + mg_patching_levels=0, + mg_patching_padding=0, + mg_patching_stitching=True, + log_test_interval=1, + log_output=False, + use_distributed=False, + verbose=True, + ): + """ + A general Trainer class to train neural-operators on given datasets + + Parameters + ---------- + model : nn.Module + n_epochs : int + wandb_log : bool, default is True + device : torch.device + mg_patching_levels : int, default is 0 + if 0, no multi-grid domain decomposition is used + if > 0, indicates the number of levels to use + mg_patching_padding : float, default is 0 + value between 0 and 1, indicates the fraction of size to use as padding on each side + e.g. for an image of size 64, padding=0.25 will use 16 pixels of padding on each side + mg_patching_stitching : bool, default is True + if False, the patches are not stitched back together and the loss is instead computed per patch + log_test_interval : int, default is 1 + how frequently to print updates + log_output : bool, default is False + if True, and if wandb_log is also True, log output images to wandb + use_distributed : bool, default is False + whether to use DDP + verbose : bool, default is True + """ + self.n_epochs = n_epochs + self.wandb_log = wandb_log + self.log_test_interval = log_test_interval + self.log_output = log_output + self.verbose = verbose + self.mg_patching_levels = mg_patching_levels + self.mg_patching_stitching = mg_patching_stitching + self.use_distributed = use_distributed + self.device = device + + if mg_patching_levels > 0: + self.mg_n_patches = 2**mg_patching_levels + if verbose: + print(f"Training on {self.mg_n_patches ** 2} multi-grid patches.") + sys.stdout.flush() + else: + self.mg_n_patches = 1 + mg_patching_padding = 0 + if verbose: + print(f"Training on regular inputs (no multi-grid patching).") + sys.stdout.flush() + + self.mg_patching_padding = mg_patching_padding + self.patcher = MultigridPatching2D( + model, + levels=mg_patching_levels, + padding_fraction=mg_patching_padding, + use_distributed=use_distributed, + stitching=mg_patching_stitching, + ) + + def train( + self, + train_loader, + validation_loader, + output_encoder, + model, + optimizer, + scheduler, + regularizer, + training_loss=None, + eval_losses=None, + ): + """Trains the given model on the given datasets""" + n_train = len(train_loader.dataset) + history = {"Epochs": [], "train_err": [], "train_time": [], "val_loss": []} + # if not isinstance(test_loaders, dict): + # test_loaders = dict(test=test_loaders) + + if self.verbose: + print(f"Training on {n_train} samples") + # print(f'Testing on {[len(loader.dataset) for loader in test_loaders.values()]} samples' + # f' on resolutions {[name for name in test_loaders]}.') + sys.stdout.flush() + + if training_loss is None: + training_loss = LpLoss(d=2) + + if eval_losses is None: # By default just evaluate on the training loss + eval_losses = dict(l2=training_loss) + + if output_encoder is not None: + output_encoder.to(self.device) + + if self.use_distributed: + is_logger = comm.get_world_rank() == 0 + else: + is_logger = True + + for epoch in range(self.n_epochs): + avg_loss = 0 + avg_lasso_loss = 0 + model.train() + t1 = default_timer() + train_err = 0.0 + + for idx, sample in enumerate(train_loader): + x, y = sample["x"], sample["y"] + + if epoch == 0 and idx == 0 and self.verbose and is_logger: + print(f"Training on raw inputs of size {x.shape=}, {y.shape=}") + + x, y = self.patcher.patch(x, y) + + if epoch == 0 and idx == 0 and self.verbose and is_logger: + print(f".. patched inputs of size {x.shape=}, {y.shape=}") + + x = x.to(self.device) + y = y.to(self.device) + + optimizer.zero_grad(set_to_none=True) + if regularizer: + regularizer.reset() + + out = model(x) + if epoch == 0 and idx == 0 and self.verbose and is_logger: + print(f"Raw outputs of size {out.shape=}") + + out, y = self.patcher.unpatch(out, y) + # Output encoding only works if output is stiched + if output_encoder is not None and self.mg_patching_stitching: + out = output_encoder.decode(out) + y = output_encoder.decode(y) + if epoch == 0 and idx == 0 and self.verbose and is_logger: + print(f".. Processed (unpatched) outputs of size {out.shape=}") + + loss = training_loss(out.float(), y) + + if regularizer: + loss += regularizer.loss + + loss.backward() + + optimizer.step() + train_err += loss.item() + + with torch.no_grad(): + avg_loss += loss.item() + if regularizer: + avg_lasso_loss += regularizer.loss + + if isinstance(scheduler, torch.optim.lr_scheduler.ReduceLROnPlateau): + scheduler.step(train_err) + else: + scheduler.step() + + epoch_train_time = default_timer() - t1 + del x, y + + train_err /= n_train + avg_loss /= self.n_epochs + history["Epochs"].append(epoch) + history["train_err"].append(train_err) + history["train_time"].append(epoch_train_time) + # if epoch % self.log_test_interval == 0: + + msg = f"[{epoch}] time={epoch_train_time:.2f}, avg_loss={avg_loss:.4f}, train_err={train_err:.4f}" + + values_to_log = dict( + train_err=train_err, time=epoch_train_time, avg_loss=avg_loss + ) + # for loader_name, loader in test_loaders.items(): + # if epoch == self.n_epochs - 1 and self.log_output: + # to_log_output = True + # else: + # to_log_output = False + + loader = validation_loader + loader_name = "" + errors = self.evaluate( + model, eval_losses, loader, output_encoder, log_prefix=loader_name + ) + + for loss_name, loss_value in errors.items(): + msg += f", {loss_name}={loss_value:.4f}" + values_to_log[loss_name] = loss_value + history["val_loss"].append(values_to_log[loss_name]) + if regularizer: + avg_lasso_loss /= self.n_epochs + msg += f", avg_lasso={avg_lasso_loss:.5f}" + + if self.verbose and is_logger: + print(msg) + sys.stdout.flush() + + # Wandb loging + if self.wandb_log and is_logger: + for pg in optimizer.param_groups: + lr = pg["lr"] + values_to_log["lr"] = lr + wandb.log(values_to_log, step=epoch, commit=True) + return history + + def evaluate( + self, model, loss_dict, data_loader, output_encoder=None, log_prefix="" + ): + """Evaluates the model on a dictionary of losses + + Parameters + ---------- + model : model to evaluate + loss_dict : dict of functions + each function takes as input a tuple (prediction, ground_truth) + and returns the corresponding loss + data_loader : data_loader to evaluate on + output_encoder : used to decode outputs if not None + log_prefix : str, default is '' + if not '', used as prefix in output dictionary + + Returns + ------- + errors : dict + dict[f'{log_prefix}_{loss_name}] = loss for loss in loss_dict + """ + model.eval() + + if self.use_distributed: + is_logger = comm.get_world_rank() == 0 + else: + is_logger = True + + errors = {f"{log_prefix}_{loss_name}": 0 for loss_name in loss_dict.keys()} + + n_samples = 0 + with torch.no_grad(): + for it, sample in enumerate(data_loader): + x, y = sample["x"], sample["y"] + + n_samples += x.size(0) + + x, y = self.patcher.patch(x, y) + y = y.to(self.device) + x = x.to(self.device) + + out = model(x) + + out, y = self.patcher.unpatch(out, y, evaluation=True) + + if output_encoder is not None: + out = output_encoder.decode(out) + + if (it == 0) and self.log_output and self.wandb_log and is_logger: + if out.ndim == 2: + img = out + else: + img = out.squeeze()[0] + wandb.log( + { + f"image_{log_prefix}": wandb.Image( + img.unsqueeze(-1).cpu().numpy() + ) + }, + commit=False, + ) + + for loss_name, loss in loss_dict.items(): + errors[f"{log_prefix}_{loss_name}"] += loss(out, y).item() + + del x, y, out + + for key in errors.keys(): + errors[key] /= n_samples + + return errors + + +# this function takes an input configuration and uses this to build a TFNO model in a format accepted by +# the TFNO benchmark and then trains the model +def build_and_train_model(config: dict, device="cpu", verbose: bool = 0): + """ + this function takes an input configuration and uses this to build a TFNO model in + a format accepted by the TFNO benchmark and then trains the model + """ + default_config = { + + # General + # For computing compression + # 'n_params_baseline': None, + + # Distributed computing + 'distributed_seed': 666, + + # FNO related + "lifting_channels": 256, + 'data_channels': 3, + 'n_modes_height': 16, + 'n_modes_width': 16, + 'hidden_channels': 32, + 'projection_channels': 64, + 'n_layers': 4, + 'skip': 'linear', + 'implementation': 'factorized', + + "use_mlp": False, + "mlp": {"dropout": 0, "expansion": 0.5}, + + 'factorization': None, + 'rank': 1.0, + + 'stabilizer': None, + 'fno_block_precision': 'half', + + # Optimizer + + 'opt_n_epochs': 5, + 'opt_learning_rate': 5e-3, + # 'opt_training_loss': 'h1', + 'opt_weight_decay': 1e-4, + + 'opt_scheduler_T_max': 500, # For cosine only, typically take n_epochs + 'opt_scheduler_patience': 5, # For ReduceLROnPlateau only + 'opt_scheduler': 'StepLR', # Or 'CosineAnnealingLR' OR 'ReduceLROnPlateau' + 'opt_step_size': 60, + 'opt_gamma': 0.5, + + # Dataset related + + 'data_batch_size': 16, + 'data_n_train': 1000, + 'data_train_resolution': 16, + 'data_n_tests': [100, 50], + 'data_test_resolutions': [16, 32], + 'data_test_batch_sizes': [16, 16], + 'data_positional_encoding': True, + + 'data_encode_input': True, + 'data_encode_output': False, + + # Patching + 'patching': {'levels': 0}, + 'patching_levels': 0, + 'patching_padding': 0, + 'patching_stitching': False, + } + + default_config.update(config) + config_name = 'default' + + + + # Loading the Darcy flow dataset + + (train_loader, validation_loader, test_loaders, output_encoder) = load_darcy_flow_small( + n_train=default_config['data_n_train'], batch_size=default_config['data_batch_size'], + positional_encoding=default_config['data_positional_encoding'], + test_resolutions=default_config['data_test_resolutions'], n_tests=default_config['data_n_tests'], + test_batch_sizes=default_config['data_test_batch_sizes'], + encode_input=default_config['data_encode_input'], encode_output=default_config['data_encode_output'], + ) + + model = TFNO2d(data_channels=default_config["data_channels"], n_modes_height=default_config['n_modes_height'], + n_modes_width=default_config["n_modes_width"], + n_layers=default_config["n_layers"], lifting_channels=default_config["lifting_channels"], + hidden_channels=default_config["hidden_channels"], + projection_channels=default_config["projection_channels"], + factorization=default_config["factorization"], rank=default_config["rank"], + use_mlp=default_config["use_mlp"], mlp=default_config["mlp"], skip=default_config["skip"], + implementation=default_config["implementation"], + stabilizer=default_config["stabilizer"], fno_block_precision=default_config["fno_block_precision"]) + model = model.to(device) + + # Log parameter count + n_params = count_params(model) + + + print(f'\nn_params: {n_params}') + sys.stdout.flush() + + + # Create the optimizer + optimizer = torch.optim.Adam(model.parameters(), + lr=default_config['opt_learning_rate'], + weight_decay=default_config['opt_weight_decay']) + + if default_config['opt_scheduler'] == 'ReduceLROnPlateau': + scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, factor=default_config['opt_gamma'], + patience=default_config['opt_scheduler_patience'], + mode='min') + elif default_config['opt_scheduler'] == 'CosineAnnealingLR': + scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=default_config['opt_scheduler_T_max']) + elif default_config['opt_scheduler'] == 'StepLR': + scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=default_config['opt_step_size'], + gamma=default_config['opt_gamma']) + else: + raise ValueError(f"Got { default_config['opt_scheduler']= }") + + # Creating the losses + l2loss = LpLoss(d=2, p=2) + h1loss = H1Loss(d=2) + train_loss = h1loss + eval_losses = dict(h1=train_loss) + # if default_config['opt_training_loss'] == 'l2': + # train_loss = l2loss + # eval_losses = dict(l2=train_loss) + # elif default_config['opt_training_loss'] == 'h1': + # train_loss = h1loss + # eval_losses = dict(h1=train_loss) + # else: + # raise ValueError(f"Got training_loss={default_config['opt_training_loss']} but expected one of ['l2', 'h1']") + # # eval_losses = {'h1': h1loss, 'l2': l2loss} + + + print('\n### MODEL ###\n', model) + print('\n### OPTIMIZER ###\n', optimizer) + print('\n### SCHEDULER ###\n', scheduler) + print('\n### LOSSES ###') + print(f'\n * Train: {train_loss}') + print(f'\n * Test: {eval_losses}') + print(f'\n### Beginning Training...\n') + sys.stdout.flush() + + trainer = Trainer(model, n_epochs=default_config['opt_n_epochs'], device=device, + mg_patching_levels=default_config['patching_levels'], + mg_patching_padding=default_config['patching_padding'], + mg_patching_stitching=default_config['patching_stitching'], wandb_log=False, log_test_interval=1, + log_output=True, use_distributed=False, verbose=True) + + history = trainer.train(train_loader, validation_loader, output_encoder, model, optimizer, scheduler, + regularizer=False, training_loss=train_loss, eval_losses=eval_losses, ) + + history["num_parameters"] = n_params + + return model, history + + + +def bestevaluate(best_config): + """we design the model based on the best hyperparameter configuration, + we then evaluate this based on the training/validation and testing data""" + + ( + train_loader, + validation_loader, + test_loaders, + output_encoder, + ) = load_darcy_flow_small( + n_train=best_config["data_n_train"], + batch_size=best_config["data_batch_size"], + positional_encoding=best_config["data_positional_encoding"], + test_resolutions=[16, 32], + n_tests=[100, 50], + test_batch_sizes=[32, 32], + encode_input=best_config["data_encode_input"], + encode_output=best_config["data_encode_output"], + ) + # the build_and_train_model function + # gives the training and validation losses + best_model, best_history = build_and_train_model(best_config, verbose=1) + # next we generate the testing losses + trainer = Trainer( + best_model, + n_epochs=best_config["opt_n_epochs"], + device=device, + mg_patching_levels=best_config["patching_levels"], + mg_patching_padding=best_config["patching_padding"], + mg_patching_stitching=best_config["patching_stitching"], + wandb_log=best_config["wandb_log"], + log_test_interval=best_config["wandb_log_test_interval"], + log_output=best_config["wandb_log_output"], + use_distributed=best_config["distributed_use_distributed"], + verbose=True, + ) + for loader_name, loader in test_loaders.items(): + l2loss = LpLoss(d=2, p=2) + h1loss = H1Loss(d=2) + eval_losses = {"h1": h1loss, "l2": l2loss} + errors = trainer.evaluate( + best_model, eval_losses, loader, output_encoder, log_prefix=loader_name + ) + for loss_name, loss_value in errors.items(): + best_history[loss_name] = loss_value + + test_samples = test_loaders[32].dataset + + fig = plt.figure(figsize=(7, 7)) + for index in range(3): + data = test_samples[index] + # Input x + x = data["x"] + # Ground-truth + y = data["y"] + # Model prediction + out = best_model(x.unsqueeze(0)) + + ax = fig.add_subplot(4, 4, index * 4 + 1) + ax.imshow(x[0], cmap="gray") + if index == 0: + ax.set_title("Input x") + plt.xticks([], []) + plt.yticks([], []) + + ax = fig.add_subplot(4, 4, index * 4 + 2) + ax.imshow(y.squeeze()) + if index == 0: + ax.set_title("Ground-truth y") + plt.xticks([], []) + plt.yticks([], []) + + ax = fig.add_subplot(4, 4, index * 4 + 3) + ax.imshow(out.squeeze().detach().numpy()) + if index == 0: + ax.set_title("Model prediction") + plt.xticks([], []) + plt.yticks([], []) + + ax = fig.add_subplot(4, 4, index * 4 + 4) + err = (np.abs(y.squeeze() - out.detach().numpy())) ** 2 + im = ax.imshow(err.squeeze(), cmap="plasma", aspect="auto") + plt.colorbar(im, label="$|y - \hat{y}|^2$") + if index == 0: + ax.set_title("Squared Error") + plt.xticks([], []) + plt.yticks([], []) + + fig.suptitle("Inputs, ground-truth output, prediction and absolute error.", y=0.98) + plt.tight_layout() + fig.show() + return best_history diff --git a/src/deephyper_benchmark/lib/fnobench/requirements.txt b/src/deephyper_benchmark/lib/fnobench/requirements.txt new file mode 100644 index 0000000..77a79ef --- /dev/null +++ b/src/deephyper_benchmark/lib/fnobench/requirements.txt @@ -0,0 +1,5 @@ +neuraloperator +tensorly +tensorly-torch +wandb +zarr \ No newline at end of file diff --git a/src/deephyper_benchmark/lib/hpobench/README.md b/src/deephyper_benchmark/lib/hpobench/README.md new file mode 100644 index 0000000..0ff9825 --- /dev/null +++ b/src/deephyper_benchmark/lib/hpobench/README.md @@ -0,0 +1,28 @@ +# HPOBench + +✅ **Benchmark ready to be used.** + +This set of benchmarks provides precomputed evaluations of neural networks for hyperparameter optimization. This is particularly useful to compare optimization schemes without training neural networks for real. + +## Installation + +To install this benchmark the following command can be run: + +```console +python -c "import deephyper_benchmark as dhb; dhb.install('HPOBench/tabular');" +``` + +For detailed information see the [NeurIPS paper describing the HPOBench benchmark](https://datasets-benchmarks-proceedings.neurips.cc/paper/2021/hash/93db85ed909c13838ff95ccfa94cebd9-Abstract-round2.html). + +## Configuration + +Different parameters can be set to configure this benchmark. + +- Environment variable `DEEPHYPER_BENCHMARK_SIMULATE_RUN_TIME` with boolean value in `[0, 1]` which set if the "recorded training time" of each training epoch should be simulated or not. **Defaults to `0`.** +- Environment variable `DEEPHYPER_BENCHMARK_PROP_REAL_RUN_TIME` with real value $> 0$. It represents the proportion of the "recorded training time" which should be used for simulation. For example, `1.0` (default) corresponds to 100% of the training time while `0.1` corresponds to 10% of the training time. **Defaults to `1.0`.** +- Environment variable `DEEPHYPER_BENCHMARK_TASK` to select the task on which to run the benchmark with values in: + - **`navalpropulsion` (Default)** + - `parkinsonstelemonitoring` + - `proteinstructure` + - `slicelocalization` +- Environment variable `DEEPHYPER_BENCHMARK_MOO` with value `0` or `1` to select if the task should be run with single or multiple objectives. **Defaults to `0` for single-objective**. \ No newline at end of file diff --git a/src/deephyper_benchmark/lib/hpobench/tabular/__init__.py b/src/deephyper_benchmark/lib/hpobench/tabular/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/deephyper_benchmark/lib/hpobench/tabular/benchmark.py b/src/deephyper_benchmark/lib/hpobench/tabular/benchmark.py new file mode 100644 index 0000000..6906ef8 --- /dev/null +++ b/src/deephyper_benchmark/lib/hpobench/tabular/benchmark.py @@ -0,0 +1,17 @@ +import os + +from deephyper_benchmark import * + +DIR = os.path.dirname(os.path.abspath(__file__)) + + +class HPOBenchTabularNavalPropulsion(Benchmark): + version = "0.0.1" + + requires = { + "bash-install": { + "step": "install", + "type": "cmd", + "cmd": os.path.join(DIR, "install.sh"), + }, + } diff --git a/src/deephyper_benchmark/lib/hpobench/tabular/constant_predictor.py b/src/deephyper_benchmark/lib/hpobench/tabular/constant_predictor.py new file mode 100644 index 0000000..4a42b3b --- /dev/null +++ b/src/deephyper_benchmark/lib/hpobench/tabular/constant_predictor.py @@ -0,0 +1,127 @@ +from typing import Tuple + +import numpy as np +import openml +from sklearn.dummy import DummyRegressor +from sklearn.model_selection import train_test_split +from sklearn.metrics import mean_squared_error + +# Reference of HPOBench Paper: https://arxiv.org/pdf/2109.06716.pdf +# Reference of original paper introducign these benchmarks: https://arxiv.org/pdf/1905.04970.pdf +# Trying to reproduce as much as possible results/setup from the original paper + +map_task_to_openmlid = { + # https://archive.ics.uci.edu/dataset/316/condition+based+maintenance+of+naval+propulsion+plants + "navalpropulsion": "44969", + # https://archive.ics.uci.edu/dataset/189/parkinsons+telemonitoring + "parkinsonstelemonitoring": "4531", + # https://archive.ics.uci.edu/dataset/265/physicochemical+properties+of+protein+tertiary+structure + "proteinstructure": "44963", + # https://archive.ics.uci.edu/dataset/206/relative+location+of+ct+slices+on+axial+axis + "slicelocalization": "42973", +} + +map_openmlid_to_task = {v: k for k, v in map_task_to_openmlid.items()} + +map_task_to_default_target_attribute = { + "navalpropulsion": ["gt_compressor_decay_state_coefficient"], + "parkinsonstelemonitoring": ["motor_UPDRS", "total_UPDRS"], + "proteinstructure": ["RMSD"], + "slicelocalization": ["reference"], +} + + +def load_from_openml(dataset_id: str) -> Tuple[np.ndarray, np.ndarray, dict]: + """Load dataset from openml library. + + Args: + dataset_id (str): the identifier of the dataset to be loaded from the OpenML database. + + Raises: + ValueError: if the dataset is not found. + + Returns: + (np.ndarray, np.ndarray, dict): X, y, metadata where X is the input array, y is the target array, and metadata is a dictionary containing additional information about the data. + """ + task_name = map_openmlid_to_task[dataset_id] + default_target_attribute = map_task_to_default_target_attribute[task_name] + + dataset = openml.datasets.get_dataset( + dataset_id, + download_data=True, + download_qualities=True, + download_features_meta_data=True, + ) + + X, y, categorical_indicator, _ = dataset.get_data() + y = X[default_target_attribute] + X = X.drop(default_target_attribute, axis=1) + y = y.values + + # Drop constant features + constant_features = [] + continuous_features = [] + for i in range(X.shape[1]): + values, _ = np.unique(X.values[:, i], return_counts=True) + if len(values) == 1: + constant_features.append(list(X.columns)[i]) + elif len(values) >= 10: + continuous_features.append(list(X.columns)[i]) + + X = X.drop(constant_features, axis=1) + + # Split into train and test + test_size = int(0.2 * len(X)) + X_train, X_test, y_train, y_test = train_test_split( + X, y, test_size=test_size, random_state=42 + ) + X_train, X_valid, y_train, y_valid = train_test_split( + X_train, y_train, test_size=test_size, random_state=42 + ) + + # Standard Normalization on features + m = np.mean(X_train[continuous_features], axis=0) + s = np.std(X_train[continuous_features], axis=0) + X_train[continuous_features] = (X_train[continuous_features] - m) / s + X_valid[continuous_features] = (X_valid[continuous_features] - m) / s + X_test[continuous_features] = (X_test[continuous_features] - m) / s + + # Standard Normalization on targets + m = np.mean(y_train, axis=0) + s = np.std(y_train, axis=0) + y_train = (y_train - m) / s + y_valid = (y_valid - m) / s + y_test = (y_test - m) / s + + return X_train.values, X_valid.values, X_test.values, y_train, y_valid, y_test + + +if __name__ == "__main__": + # Some tests + task = "navalpropulsion" + # task = "parkinsonstelemonitoring" + # task = "proteinstructure" + # task = "slicelocalization" + print(f"Task: {task}") + openmlid = map_task_to_openmlid[task] + X_train, X_valid, X_test, y_train, y_valid, y_test = load_from_openml(openmlid) + + print("Num samples:", X_train.shape[0] + X_valid.shape[0] + X_test.shape[0]) + print("X_train.shape", X_train.shape) + print("X_valid.shape", X_valid.shape) + print("X_test.shape", X_test.shape) + print("y_train.shape", y_train.shape) + print("y_valid.shape", y_valid.shape) + print("y_test.shape", y_test.shape) + + model = DummyRegressor(strategy="mean") + model.fit(X_train, y_train) + + # As the data are normalized these values are expected to be close to 1.0 + fold = ["train", "valid", "test"] + for i, (X, y) in enumerate( + zip([X_train, X_valid, X_test], [y_train, y_valid, y_test]) + ): + print(f"Fold: {fold[i]}") + y_pred = model.predict(X) + print(f" MSE: {mean_squared_error(y, y_pred)}") diff --git a/src/deephyper_benchmark/lib/hpobench/tabular/hpo.py b/src/deephyper_benchmark/lib/hpobench/tabular/hpo.py new file mode 100644 index 0000000..0c662ed --- /dev/null +++ b/src/deephyper_benchmark/lib/hpobench/tabular/hpo.py @@ -0,0 +1,139 @@ +import os +import time + +import numpy as np + +DIR = os.path.dirname(os.path.abspath(__file__)) + +import hpobench.benchmarks.nas.tabular_benchmarks + +from deephyper.evaluator import profile, RunningJob +from deephyper.hpo import HpProblem + +DEEPHYPER_BENCHMARK_SIMULATE_RUN_TIME = bool( + int(os.environ.get("DEEPHYPER_BENCHMARK_SIMULATE_RUN_TIME", 0)) +) +DEEPHYPER_BENCHMARK_PROP_REAL_RUN_TIME = float( + os.environ.get("DEEPHYPER_BENCHMARK_PROP_REAL_RUN_TIME", 1.0) +) +DEEPHYPER_BENCHMARK_TASK = os.environ.get("DEEPHYPER_BENCHMARK_TASK", "navalpropulsion") +DEEPHYPER_BENCHMARK_MOO = bool(int(os.environ.get("DEEPHYPER_BENCHMARK_MOO", 0))) + + +map_task_to_benchmark = { + "navalpropulsion": "NavalPropulsionBenchmark", + "parkinsonstelemonitoring": "ParkinsonsTelemonitoringBenchmark", + "proteinstructure": "ProteinStructureBenchmark", + "slicelocalization": "SliceLocalizationBenchmark", +} +data_path = os.path.join(DIR, "build/HPOBench/data/fcnet_tabular_benchmarks/") + + +benchmark_class = getattr( + hpobench.benchmarks.nas.tabular_benchmarks, + map_task_to_benchmark[DEEPHYPER_BENCHMARK_TASK], +) +b = benchmark_class(data_path=data_path) +config_space = b.get_configuration_space() + +problem = HpProblem(config_space=config_space) + + +@profile +def run(job: RunningJob, optuna_trial=None) -> dict: + # otherwise failure + config = job.parameters + + seed = int(job.id.split(".")[-1]) + rng = np.random.RandomState(seed) + run_index = int(rng.choice((0, 1, 2, 3))) + + benchmark_class = getattr( + hpobench.benchmarks.nas.tabular_benchmarks, + map_task_to_benchmark[DEEPHYPER_BENCHMARK_TASK], + ) + b = benchmark_class(data_path=data_path) + budget_space = b.get_fidelity_space().get("budget") + min_b, max_b = budget_space.lower, budget_space.upper + + # The objective is the coefficient of determination (R^2) + # As the target were standardized, the R^2 = 1 - MSE / MSE_baseline with MSE_baseline = 1.0 + # The objective is maximized in deephyper + eval_test = b.objective_function_test(config, fidelity={"budget": max_b}) + objective_test = 1 - eval_test["function_value"] + cost_eval = eval_test["cost"] + cost_step = cost_eval / (max_b - min_b + 1) + + eval_val = b.objective_function( + config, fidelity={"budget": max_b}, run_index=run_index + ) + objective_val = 1 - eval_val["function_value"] + + other_metadata = {} + + consumed_time = 0 # the cost here corresponds to time + + if optuna_trial: + for budget_i in range(min_b, max_b + 1): + eval = b.objective_function( + config, fidelity={"budget": budget_i}, run_index=run_index + ) + + consumed_time += cost_step + if DEEPHYPER_BENCHMARK_SIMULATE_RUN_TIME: + time.sleep(cost_step * DEEPHYPER_BENCHMARK_PROP_REAL_RUN_TIME) + + objective_i = 1 - eval["function_value"] # maximizing in deephyper + + # Trial report is not support for MOO in Optuna + if DEEPHYPER_BENCHMARK_MOO: + continue + + optuna_trial.report(objective_i, step=budget_i) + if optuna_trial.should_prune(): + break + + objective = objective_i + + else: + for budget_i in range(min_b, max_b + 1): + eval = b.objective_function( + config, fidelity={"budget": budget_i}, run_index=run_index + ) + + consumed_time += cost_step + if DEEPHYPER_BENCHMARK_SIMULATE_RUN_TIME: + time.sleep(cost_step * DEEPHYPER_BENCHMARK_PROP_REAL_RUN_TIME) + + objective_i = 1 - eval["function_value"] # maximizing in deephyper + job.record(budget_i, objective_i) + if job.stopped(): + break + + objective = job.objective + + if hasattr(job, "stopper") and hasattr(job.stopper, "infos_stopped"): + other_metadata["infos_stopped"] = job.stopper.infos_stopped + + metadata = { + "budget": budget_i, + "stopped": budget_i < max_b, + "objective_test": objective_test, + "objective_val": objective_val, + } + metadata.update(other_metadata) + + return { + "objective": (objective, -consumed_time) + if DEEPHYPER_BENCHMARK_MOO + else objective, + "metadata": metadata, + } + + +if __name__ == "__main__": + print(problem) + default_config = problem.default_configuration + print(f"{default_config=}") + result = run(RunningJob(parameters=default_config)) + print(f"{result=}") diff --git a/src/deephyper_benchmark/lib/hpobench/tabular/install.sh b/src/deephyper_benchmark/lib/hpobench/tabular/install.sh new file mode 100755 index 0000000..1eb5d47 --- /dev/null +++ b/src/deephyper_benchmark/lib/hpobench/tabular/install.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +set -x + +mkdir build/ && cd build + +# Clone HPOBench repository +git clone https://github.com/automl/HPOBench.git +cd HPOBench/ + +# Freeze commit used for the benchmark +git checkout d8b45b1eca9a61c63fe79cdfbe509f77d3f5c779 + +# Relax the constraint on the Python version required +sed -i '' 's/>=3.6, <=3.10/>=3.6, <3.12/g' setup.py + +# Download data +mkdir data && cd data/ + +# For tabular benchmarks +wget http://ml4aad.org/wp-content/uploads/2019/01/fcnet_tabular_benchmarks.tar.gz +tar xf fcnet_tabular_benchmarks.tar.gz + +# Install other dependencies +cd .. +pip install "." +pip install git+https://github.com/google-research/nasbench.git@master + +git clone https://github.com/automl/nas_benchmarks.git +cd nas_benchmarks + +# Freeze commit used for the benchmark +git checkout 1b09906ba3f522f15766b75643423acccd9db3a5 + +# Comment out the last line of the file +sed -i '' '5 s/./#&/' tabular_benchmarks/__init__.py + +python setup.py install + diff --git a/src/deephyper_benchmark/lib/hpobench/tabular/metrics.py b/src/deephyper_benchmark/lib/hpobench/tabular/metrics.py new file mode 100644 index 0000000..d135e38 --- /dev/null +++ b/src/deephyper_benchmark/lib/hpobench/tabular/metrics.py @@ -0,0 +1,78 @@ +import json +import os + +import numpy as np + +from .hpo import b as BENCHMARK + + +class PerformanceEvaluator: + """A class defining performance evaluators for the DTLZ problems.""" + + def __init__(self): + """Read the current problem defn from environment vars.""" + + # Retrieve the Best Configuration + configs, te, ve = [], [], [] + for k in BENCHMARK.benchmark.data.keys(): + configs.append(json.loads(k)) + te.append(np.min(BENCHMARK.benchmark.data[k]["final_test_error"])) + ve.append(np.min(np.mean(BENCHMARK.benchmark.data[k]["valid_mse"], axis=0))) + + idx_opt_ve = np.argmin(ve) + idx_opt_te = np.argmin(te) + + # Configuration, Validation Error, Test Error based on the best validation error + self.x_min_on_valid = dict(configs[idx_opt_ve]) + self.y_min_valid_on_valid = ve[idx_opt_ve] + self.y_min_test_on_valid = te[idx_opt_ve] + + # Configuration, Validation Error, Test Error based on the best final test error + # only available for this benchmark + self.x_min_on_test = dict(configs[idx_opt_te]) + self.y_min_valid_on_test = ve[idx_opt_te] + self.y_min_test_on_test = te[idx_opt_te] + + def simple_regret_valid(self, y_valid: np.ndarray) -> np.ndarray: + """Compute the regret of the objective (validation RMSE) of a list of ordered solutions. + + Args: + y_valid (np.ndarray): An array of solutions. + + Returns: + np.ndarray: An array of regret values. + """ + return y_valid - self.y_min_valid_on_valid + + def cumul_regret_valid(self, y_valid: np.ndarray) -> np.ndarray: + """Compute the cumulative regret of the objective (validation RMSE) aon n array of ordered solutions. + + Args: + y_valid (np.ndarray): An array of solutions. + + Returns: + np.ndarray: An array of cumulative regret values. + """ + return np.cumsum(self.simple_regret_valid(y_valid)) + + def simple_regret_test(self, y_test: np.ndarray) -> np.ndarray: + """Compute the regret of the test RMSE of a list of ordered solutions. + + Args: + y_test (np.ndarray): An array of solutions. + + Returns: + np.ndarray: An array of regret values. + """ + return y_test - self.y_min_test_on_test + + def cumul_regret_test(self, y_test: np.ndarray) -> np.ndarray: + """Compute the cumulative regret of the test RMSE aon n array of ordered solutions. + + Args: + y_test (np.ndarray): An array of solutions. + + Returns: + np.ndarray: An array of cumulative regret values. + """ + return np.cumsum(self.simple_regret_test(y_test)) diff --git a/src/deephyper_benchmark/lib/jahsbench/README.md b/src/deephyper_benchmark/lib/jahsbench/README.md new file mode 100644 index 0000000..26ac239 --- /dev/null +++ b/src/deephyper_benchmark/lib/jahsbench/README.md @@ -0,0 +1,176 @@ +# JAHS Benchmark Suite + +> **Warning** +> Work in progress, this benchmark is not yet ready. + +This module contains a DeepHyper wrapper for + [JAHS-Bench-201](https://github.com/automl/jahs_bench_201). + +JAHSBench implements a random forest surrogate model, trained on real-world +performance data for neural networks trained on three standard benchmark +problems: + - ``fashion_mnist`` (**default**) + - ``cifar10`` + - ``colorectal_histology`` + +Using these models as surrogates for the true performance, we can use this +benchmark problem to study the performance of AutoML techniques on joint +architecture-hyperparameter search tasks at minimal expense. + +The models allow us to tune 2 continuous training hyperparameters + - ``LearningRate`` and + - ``WeightDecay``, + +2 categorical training hyperparameters + - ``Activation`` and + - ``TrivialAugment``, + +and 5 categorical architecture parameters + - ``Op{i}`` for ``i=0, ..., 4``. + +For DeepHyper's implementation, we have added 9th integer-valued parameter, +which is the number of epochs trained + - ``nepochs``. + +When run with the option ``wait=True``, ``JAHSBench`` will wait for an +amount of time proportional to the ``runtime`` field returned by +JAHS-Bench-201's surrogates. By default, this is 1% of the true runtime. + +The benchmark can be run to tune a single objective (``valid-acc``) or +three objectives (``valid-acc``, ``latency``, and ``size_MB``). +*Note that in the original JAHS-Bench-201 benchmark, there are only 2 +objectives and ``size_MB`` is not included as an objective.* + +For further information, see: + +``` + @inproceedings{NEURIPS2022_fd78f2f6, + author = {Bansal, Archit and Stoll, Danny and Janowski, Maciej and Zela, Arber and Hutter, Frank}, + booktitle = {Advances in Neural Information Processing Systems}, + editor = {S. Koyejo and S. Mohamed and A. Agarwal and D. Belgrave and K. Cho and A. Oh}, + pages = {38788--38802}, + publisher = {Curran Associates, Inc.}, + title = {JAHS-Bench-201: A Foundation For Research On Joint Architecture And Hyperparameter Search}, + url = {https://proceedings.neurips.cc/paper_files/paper/2022/file/fd78f2f65881c1c7ce47e26b040cf48f-Paper-Datasets_and_Benchmarks.pdf}, + volume = {35}, + year = {2022} + } +``` + +## Installation + +To install this benchmark the following command can be run: +``` +python -c "import deephyper_benchmark as dhb; dhb.install('JAHSBench');" +``` + +## Configuration + +Prior to initialize the problem, set the following environment variables +to configure the JAHS-Bench problem: +- ``DEEPHYPER_BENCHMARK_MOO`` to `0` for single objective runs or `1` for + multiobjective runs. Defaults to `1`. +- ``DEEPHYPER_BENCHMARK_JAHS_PROB`` to one of the following: + `fashion_mnist` (default), `cifar10`, or `colorectal_histology`. + +## Metadata + +In addition to DeepHyper's standard metadata (timestamps), the following metadata +is produced by JAHS-Bench-201, and recorded by DeepHyper: +- ``m:size_MB``, +- ``m:runtime``, +- ``m:latency``, +- ``m:FLOPS``, +- ``m:valid-acc``, +- ``m:train-acc``, and +- ``m:test-acc``. + +The ``valid-acc`` is used to compute the objective in the single objective +case. +Additionally, the negative values of the ``latency`` and ``size_MB`` are +used in the multiobjective case. +All metadata is recorded regardless of the case. + +## Usage + +To use the benchmark follow this example set of instructions: + +```python + +import deephyper_benchmark as dhb + + +# Load JAHS-bench-201 +dhb.load("JAHSBench") + +from deephyper_benchmark.lib.jahsbench import hpo + +# Example of running one evaluation of JAHSBench +from deephyper.evaluator import RunningJob +config = hpo.problem.jahs_obj.__sample__() # get a default config to test +res = hpo.run(RunningJob(parameters=config)) + +``` + +Note that JAHS-Bench-201 uses XGBoost, which may not be compatible with older +versions of MacOS. +Additionally, the surrogate data has been pickled with an older version +of scikit-learn and newer versions will fail to correctly load the surrogate +models. + +For more information, see the following GitHub issues: + - https://github.com/automl/jahs_bench_201/issues/6 + - https://github.com/automl/jahs_bench_201/issues/18 + +## Evaluating Results + +To evaluate the results, the AutoML team recommends using the validation +error for single-objective runs or the hypervolume metric over both +validation error and evaluation latency for multiobjective-runs. +See their +[Evaluation Protocol](https://automl.github.io/jahs_bench_201/evaluation_protocol) +for more details. + +**For multiobjective problems:** +In their original benchmark, no recommended reference point is given, +as discussed in +[this GitHub issue](https://github.com/automl/jahs_bench_201/issues/19). +Since we have already modified the original problem, we have taken the +liberty of assigning a reference point. +This reference point was chosen to include all true solutions for the +``latency`` and ``size_MB`` objectives. +These values were chosen by observing the true Pareto front for the raw data. +However, we have purposefully created a lower bound on the "interesting range" +for the ``valid-acc`` objective, which is higher than it was before. +In particular, no accuracies less than 95% will be considered when +computing hypervolume scores for the Fashion MNIST dataset, +no accuracies less than 90% are considered for the CIFAR-10 dataset, +and no accuracies less than 93% are considered for the colorectal histology +dataset. +When solving any of these problems via DeepHyper, the ``moo_lower_bounds`` +argument should be set accordingly. +To evaluate hypervolume with these reference points, use our metrics as +shown below + +```python + +from deephyper_benchmark.lib.jahsbench import metrics +evaluator = metrics.PerformanceEvaluator() +hv = evaluator.hypervolume(res) + +``` + +## Additional Problem Details + +Each problem in JAHS-Bench-201 is given by a XGBoost **surrogate** for a +joint neural network architecture/hyperparameter search problem. +Surrogates are given for networks trained to solve image classification tasks +for three different datasets. +These datasets are listed below and have the following properties: + +- ``fashion_mnist`` is based on the Fashion MNIST dataset (https://github.com/zalandoresearch/fashion-mnist). + This problem has lower bound of 95% on the "interesting range" of validation accuracies, and the Kendall's tau for the XGBoost model is over 92% for all 3 objectives (92.2% for the ``valid-acc`` objective). +- ``cifar10`` is based on the Cifar-10 dataset (https://www.cs.toronto.edu/~kriz/cifar.html) + This problem has lower bound of 90% on the "interesting range" of validation accuracies, and the Kendall's tau for the XGBoost model is over 89% for all 3 objectives (89% for the ``valid-acc`` objective). +- ``colorecal-histology`` is based on the colorectal histology dataset (https://zenodo.org/record/53169#.XGZemKwzbmG). + This problem has lower bound of 93% on the "interesting range" of validation accuracies, and the Kendall's tau for the XGBoost model is considerably lower than with the other datasets (just 68.2% for the ``valid-acc`` objective), so it may not be an accurate representation of the true problem. diff --git a/src/deephyper_benchmark/lib/jahsbench/__init__.py b/src/deephyper_benchmark/lib/jahsbench/__init__.py new file mode 100644 index 0000000..f102a9c --- /dev/null +++ b/src/deephyper_benchmark/lib/jahsbench/__init__.py @@ -0,0 +1 @@ +__version__ = "0.0.1" diff --git a/src/deephyper_benchmark/lib/jahsbench/benchmark.py b/src/deephyper_benchmark/lib/jahsbench/benchmark.py new file mode 100644 index 0000000..2b99dc3 --- /dev/null +++ b/src/deephyper_benchmark/lib/jahsbench/benchmark.py @@ -0,0 +1,22 @@ +import os + +from deephyper_benchmark import * + +DIR = os.path.dirname(os.path.abspath(__file__)) + + +class JAHS201Benchmark(Benchmark): + version = "0.0.1" + + requires = { + "py-pip-requirements": { + "step": "install", + "type": "pip", + "args": "install -r " + os.path.join(DIR, "requirements.txt"), + }, + "bash-install": { + "step": "install", + "type": "cmd", + "cmd": "cd . && bash " + os.path.join(DIR, "./install.sh"), + }, + } diff --git a/src/deephyper_benchmark/lib/jahsbench/hpo.py b/src/deephyper_benchmark/lib/jahsbench/hpo.py new file mode 100644 index 0000000..4e8738a --- /dev/null +++ b/src/deephyper_benchmark/lib/jahsbench/hpo.py @@ -0,0 +1,57 @@ +import os +import numpy as np +import time + +from deephyper.evaluator import profile, RunningJob +from deephyper.hpo import HpProblem +from . import model + + +# Read in whether to do single- or multi-objectives +multiobj = int(os.environ.get("DEEPHYPER_BENCHMARK_MOO", 1)) +prob_name = os.environ.get("DEEPHYPER_BENCHMARK_JAHS_PROB", "fashion_mnist") + +# Create problem +problem = HpProblem() +jahs_obj = model.jahs_bench(dataset=prob_name) +# 2 continuous hyperparameters +problem.add_hyperparameter((1e-3, 1.0, "log-uniform"), "LearningRate") +problem.add_hyperparameter((1e-5, 1e-3, "log-uniform"), "WeightDecay") +# 2 categorical hyperparameters +problem.add_hyperparameter(["ReLU", "Hardswish", "Mish"], "Activation") +problem.add_hyperparameter(["on", "off"], "TrivialAugment") +# 6 categorical architecture design variables +for i in range(1, 7): + problem.add_hyperparameter([0, 1, 2, 3, 4], f"Op{i}") +# 1 integer hyperparameter number of training epochs (1 to 200) +problem.add_hyperparameter((1, 200), "nepochs") + + +@profile +def run(job: RunningJob, sleep=False, sleep_scale=0.01) -> dict: + config = job.parameters + result = jahs_obj(config) + + if sleep: + t_sleep = result["runtime"] * sleep_scale + time.sleep(t_sleep) + + dh_data = {} + dh_data["metadata"] = result + if multiobj: + dh_data["objective"] = [ + result["valid-acc"], + -result["latency"], + -result["size_MB"], + ] + else: + dh_data["objective"] = result["valid-acc"] + return dh_data + + +if __name__ == "__main__": + print(problem) + default_config = problem.default_configuration + print(f"{default_config=}") + result = run(RunningJob(parameters=default_config)) + print(f"{result=}") diff --git a/src/deephyper_benchmark/lib/jahsbench/install.sh b/src/deephyper_benchmark/lib/jahsbench/install.sh new file mode 100755 index 0000000..76a97b8 --- /dev/null +++ b/src/deephyper_benchmark/lib/jahsbench/install.sh @@ -0,0 +1 @@ +python -m jahs_bench.download --target surrogates diff --git a/src/deephyper_benchmark/lib/jahsbench/metrics.py b/src/deephyper_benchmark/lib/jahsbench/metrics.py new file mode 100644 index 0000000..f6040f7 --- /dev/null +++ b/src/deephyper_benchmark/lib/jahsbench/metrics.py @@ -0,0 +1,119 @@ +import numpy as np +from deephyper.skopt.moo import pareto_front, hypervolume + + +class PerformanceEvaluator: + """ A class defining performance evaluators for JAHS Bench 201 problems. + + Contains the following public methods: + + * `__init__()` constructs a new instance by reading the problem + definition from environment variables, + * `hypervolume(pts)` calculates the total hypervolume dominated by + the current point set, w.r.t. the reference point and filtering out + solutions that do not dominate the reference point, + * `refPt()` calculates a reference point for the current problem, and + * `numPts(pts)` calculates the number of solution points that dominate + the reference point. + + """ + + def __init__(self): + """ Read the current JAHS-Bench-201 problem definition and initialize. """ + + import os + + multiobj = int(os.environ.get("DEEPHYPER_BENCHMARK_MOO", 1)) + prob_name = os.environ.get("DEEPHYPER_BENCHMARK_JAHS_PROB", "fashion_mnist") + self.p_name = prob_name + if multiobj: + self.nobjs = 3 + else: + self.nobjs = 1 + + def hypervolume(self, pts): + """ Calculate the hypervolume dominated, w.r.t. the reference point. + + Args: + pts (numpy.ndarray): A 2d array of objective values. + Each row is an objective value in the solution set. + + Returns: + float: The total hypervolume dominated by the current solution + w.r.t. the reference point, after filtering out points worse than + the reference point. + + """ + + if self.nobjs < 2: + raise ValueError("Cannot calculate hypervolume for 1 objective") + if pts.size > 0 and pts[0, 0] > 0: + filtered_pts = -pts.copy() + else: + filtered_pts = pts.copy() + rp = self.refPt() + for i in range(pts.shape[0]): + if np.any(filtered_pts[i, :] > rp): + filtered_pts[i, :] = rp + return hypervolume(filtered_pts, rp) + + def refPt(self): + """ Calculate the reference point for the given problem definition. """ + + if self.p_name in ["fashion_mnist"]: + rp = np.ones(self.nobjs) + rp[0] = -95 + if self.nobjs > 1: + rp[1] = 1.75 + rp[2] = 0.6 + return rp + elif self.p_name in ["cifar10"]: + rp = np.ones(self.nobjs) + rp[0] = -90 + if self.nobjs > 1: + rp[1] = 4.0 + rp[2] = 0.9 + return rp + elif self.p_name in ["colorectal_histology"]: + rp = np.ones(self.nobjs) + rp[0] = -93 + if self.nobjs > 1: + rp[1] = 4.0 + rp[2] = 0.4 + return rp + else: + raise ValueError(f"{self.p_name} is not a valid problem") + + def numPts(self, pts): + """ Calculate the number of solutions that dominate the reference point. + + Args: + pts (numpy.ndarra): A 2d array of objective values. + Each row is an objective value in the solution set. + + Returns: + int: The number of fi in pts such that all(fi < self.refPt). + + """ + + if np.any(pts < 0): + pareto_pts = pareto_front(-pts) + else: + pareto_pts = pareto_front(pts) + return sum([all(fi <= self.refPt()) for fi in pareto_pts]) + + +if __name__ == "__main__": + """ Driver code to test performance metrics. """ + + result = np.array([[90, -1, -2], + [94, -0.1, -0.2], + [96, -0.75, -0.1], + [99.0, -0.5, -200.0]]) + + evaluator = PerformanceEvaluator() + + assert abs(evaluator.hypervolume(result) - 0.5) < 1.0e-8 + assert evaluator.numPts(result) == 1 + assert np.all(np.abs(evaluator.refPt() - np.array([-95.0, 1.75, 0.6])) + < 1.0e-8) diff --git a/src/deephyper_benchmark/lib/jahsbench/model.py b/src/deephyper_benchmark/lib/jahsbench/model.py new file mode 100644 index 0000000..6f0eef4 --- /dev/null +++ b/src/deephyper_benchmark/lib/jahsbench/model.py @@ -0,0 +1,80 @@ +""" This module contains objective function implementations of the JAHS 201 +benchmark suite, implemented as DeepHyper compatible models. + +""" + +import numpy as np + +class jahs_bench: + """ A callable class implementing the JAHS benchmark problems. """ + + def __init__(self, dataset="fashion_mnist"): + """ Import and configure the jahs-bench module. + + Args: + dataset (str): One of "cifar10", "colorectal_histology", + or "fashion_mnist" (default) + + """ + + from jahs_bench.api import Benchmark + import os + + ### JAHS bench settings ### + MODEL_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), + "jahs_bench_data") + # Define the benchmark + self.benchmark = Benchmark( + task=dataset, + save_dir=MODEL_PATH, + kind="surrogate", + download=False + ) + + def __sample__(self): + """ Randomly sample a JAHS-Bench-201 configuration. + + Returns: + dict: A configuration dictionary. + + """ + + config = self.benchmark.sample_config() + config['nepochs'] = np.random.randint(1, 200) + return config + + def __call__(self, x): + """ DeepHyper compatible objective function calling jahs-bench. + + Args: + x (dict): Configuration dictionary with same keys as jahs-bench. + + Returns: + tuple of floats: In order: accuracy (to be maximized), latency + (to be minimized). + + """ + + # Default config + config = { + 'Optimizer': 'SGD', + 'N': 5, + 'W': 16, + 'Resolution': 1.0, + } + # Update config using x + for key in x.keys(): + config[key] = x[key] + # Special rule for setting "TrivialAugment" + if x['TrivialAugment'] == "on": + config['TrivialAugment'] = True + else: + config['TrivialAugment'] = False + # Check for nepochs + nepochs = 200 + if 'nepochs' in x.keys(): + nepochs = x['nepochs'] + # Evaluate and return + fx = np.zeros(2) + result = self.benchmark(config, nepochs=nepochs) + return result[nepochs] diff --git a/src/deephyper_benchmark/lib/jahsbench/requirements.txt b/src/deephyper_benchmark/lib/jahsbench/requirements.txt new file mode 100644 index 0000000..702f41c --- /dev/null +++ b/src/deephyper_benchmark/lib/jahsbench/requirements.txt @@ -0,0 +1,2 @@ +jahs-bench +xgboost diff --git a/src/deephyper_benchmark/lib/lcbench/Makefile b/src/deephyper_benchmark/lib/lcbench/Makefile new file mode 100644 index 0000000..cb13f98 --- /dev/null +++ b/src/deephyper_benchmark/lib/lcbench/Makefile @@ -0,0 +1,14 @@ + +COMMIT=8de69dc0e7f7b4baee7129d599e74cc3e09b6d06 + +build: + mkdir -p build + wget https://github.com/automl/LCBench/archive/$(COMMIT).zip -O build/LCBench.zip + unzip build/LCBench.zip -d build/ + mv build/LCBench-$(COMMIT) build/LCBench + mkdir -p build/LCBench/data + wget https://figshare.com/ndownloader/files/21188607 -O build/LCBench/data/data_2k.zip + unzip build/LCBench/data/data_2k.zip -d build/LCBench/data/ + +clean: + rm -rf build/ \ No newline at end of file diff --git a/src/deephyper_benchmark/lib/lcbench/README.md b/src/deephyper_benchmark/lib/lcbench/README.md new file mode 100644 index 0000000..14ff7ae --- /dev/null +++ b/src/deephyper_benchmark/lib/lcbench/README.md @@ -0,0 +1,19 @@ +# LCBench + +✅ **Benchmark ready to be used.** + +This benchmark is only compatible with random search and only interesting to benchmark multi-fidelity algorithms that do not consider hyperparameter optimization. Candidate learners are represented by an id. + +## Installation + +To install the `LCBench` benchmark, run: +```console +python -c "import deephyper_benchmark as dhb; dhb.install('LCBench');" +``` + +Run the hyperparameter search +``` +import deephyper_benchmark as dhb +dhb.load("LCBench") +from deephyper_benchmark.lib.lcbench import hpo +``` \ No newline at end of file diff --git a/src/deephyper_benchmark/lib/lcbench/__init__.py b/src/deephyper_benchmark/lib/lcbench/__init__.py new file mode 100644 index 0000000..68a63b1 --- /dev/null +++ b/src/deephyper_benchmark/lib/lcbench/__init__.py @@ -0,0 +1,2 @@ + +datasets = ['APSFailure', 'Amazon_employee_access', 'Australian', 'Fashion-MNIST', 'KDDCup09_appetency', 'MiniBooNE', 'adult', 'airlines', 'albert', 'bank-marketing', 'blood-transfusion-service-center', 'car', 'christine', 'cnae-9', 'connect-4', 'covertype', 'credit-g', 'dionis', 'fabert', 'helena', 'higgs', 'jannis', 'jasmine', 'jungle_chess_2pcs_raw_endgame_complete', 'kc1', 'kr-vs-kp', 'mfeat-factors', 'nomao', 'numerai28.6', 'phoneme', 'segment', 'shuttle', 'sylvine', 'vehicle', 'volkert'] \ No newline at end of file diff --git a/src/deephyper_benchmark/lib/lcbench/benchmark.py b/src/deephyper_benchmark/lib/lcbench/benchmark.py new file mode 100644 index 0000000..18ddea6 --- /dev/null +++ b/src/deephyper_benchmark/lib/lcbench/benchmark.py @@ -0,0 +1,18 @@ +import os + +from deephyper_benchmark import * + +DIR = os.path.dirname(os.path.abspath(__file__)) + + +class LCBench(Benchmark): + version = "0.0.1" + + requires = { + "makefile": {"step": "install", "type": "cmd", "cmd": "make build"}, + "lcdbench-api": { + "step": "load", + "type": "pythonpath", + "path": f"{DIR}/../build/LCBench/", + }, + } diff --git a/src/deephyper_benchmark/lib/lcbench/hpo.py b/src/deephyper_benchmark/lib/lcbench/hpo.py new file mode 100644 index 0000000..9902dc0 --- /dev/null +++ b/src/deephyper_benchmark/lib/lcbench/hpo.py @@ -0,0 +1,78 @@ +import os + +DIR = os.path.dirname(os.path.abspath(__file__)) + +from deephyper.evaluator import profile, RunningJob +from deephyper.hpo import HpProblem + +dataset = os.environ.get("DEEPHYPER_BENCHMARK_LCBENCH_DATASET", "APSFailure") + +data_path = os.path.join(DIR, "../build/LCBench/data/data_2k.json") + +from api import Benchmark as LCBenchBenchmark +bench = LCBenchBenchmark(data_path, cache=True, cache_dir=os.path.join(DIR, "../build/LCBench/data/")) + +num_elements = 2000 + +problem = HpProblem() +problem.add_hyperparameter((0, num_elements - 1), "index") + + +@profile +def run(job: RunningJob, optuna_trial=None) -> dict: + + + # otherwise failure + idx = job.parameters["index"] + y_valid = bench.query(dataset, "Train/val_cross_entropy", idx)[1:51] + y_test = bench.query(dataset, "Train/test_cross_entropy", idx)[1:51] + + min_b, max_b = 1, 50 + + objective_val = -y_valid[-1] + objective_test = -y_test[-1] + + other_metadata = {} + + if optuna_trial: + + for budget_i in range(min_b, max_b + 1): + objective_i = -y_valid[budget_i-1] # maximizing in deephyper + optuna_trial.report(objective_i, step=budget_i) + if optuna_trial.should_prune(): + break + + objective = objective_i + + else: + + for budget_i in range(min_b, max_b + 1): + objective_i = -y_valid[budget_i-1] # maximizing in deephyper + job.record(budget_i, objective_i) + if job.stopped(): + break + + objective = job.objective + + if hasattr(job, "stopper") and hasattr(job.stopper, "infos_stopped"): + other_metadata["infos_stopped"] = job.stopper.infos_stopped + + metadata = { + "budget": budget_i, + "stopped": budget_i < max_b, + "objective_test": objective_test, + "objective_val": objective_val, + } + metadata.update(other_metadata) + return { + "objective": objective, + "metadata": metadata, + } + + +if __name__ == "__main__": + print(problem) + default_config = problem.default_configuration + print(f"{default_config=}") + result = run(RunningJob(parameters=default_config)) + print(f"{result=}") diff --git a/src/deephyper_benchmark/lib/lcdb/README.md b/src/deephyper_benchmark/lib/lcdb/README.md new file mode 100644 index 0000000..a9d798d --- /dev/null +++ b/src/deephyper_benchmark/lib/lcdb/README.md @@ -0,0 +1,4 @@ +# Learning Curve Databased Benchmark + +> **Warning** +> Work in progress, this benchmark is not yet ready. diff --git a/src/deephyper_benchmark/lib/lcdb/algorithm_selection/__init__.py b/src/deephyper_benchmark/lib/lcdb/algorithm_selection/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/deephyper_benchmark/lib/lcdb/hyperparameter_optimization/__init__.py b/src/deephyper_benchmark/lib/lcdb/hyperparameter_optimization/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/deephyper_benchmark/lib/lcdb/hyperparameter_optimization/benchmark.py b/src/deephyper_benchmark/lib/lcdb/hyperparameter_optimization/benchmark.py new file mode 100644 index 0000000..377c4c8 --- /dev/null +++ b/src/deephyper_benchmark/lib/lcdb/hyperparameter_optimization/benchmark.py @@ -0,0 +1,12 @@ +import os + +from deephyper_benchmark import * + +DIR = os.path.dirname(os.path.abspath(__file__)) + + +class LCDBHyperparameterOptimization(Benchmark): + """LCDB 2.0 + """ + + version = "0.0.1" diff --git a/src/deephyper_benchmark/lib/lcdb/hyperparameter_optimization/hpo.py b/src/deephyper_benchmark/lib/lcdb/hyperparameter_optimization/hpo.py new file mode 100644 index 0000000..45100ae --- /dev/null +++ b/src/deephyper_benchmark/lib/lcdb/hyperparameter_optimization/hpo.py @@ -0,0 +1,216 @@ +import gzip +import os +import time + +import numpy as np + +DIR = os.path.dirname(os.path.abspath(__file__)) + +from deephyper.evaluator import RunningJob, profile +from deephyper.hpo import HpProblem +from lcdb.analysis import read_csv_results +from lcdb.analysis.json import ( + QueryAnchorValues, + QueryMetricValuesFromAnchors, + QueryEpochValues, + QueryMetricValuesFromEpochs, +) +from lcdb.analysis.score import balanced_accuracy_from_confusion_matrix +from lcdb.utils import import_attr_from_module + +# DEEPHYPER_BENCHMARK_SIMULATE_RUN_TIME = bool( +# int(os.environ.get("DEEPHYPER_BENCHMARK_SIMULATE_RUN_TIME", 0)) +# ) +# DEEPHYPER_BENCHMARK_PROP_REAL_RUN_TIME = float( +# os.environ.get("DEEPHYPER_BENCHMARK_PROP_REAL_RUN_TIME", 1.0) +# ) + +# DEEPHYPER_BENCHMARK_FIDELITY = os.environ.get("DEEPHYPER_BENCHMARK_FIDELITY", "anchor") +# DEEPHYPER_BENCHMARK_WORKFLOW = "lcdb.workflow.sklearn.KNNWorkflow" +# DEEPHYPER_BENCHMARK_CSV = os.environ.get( +# "DEEPHYPER_BENCHMARK_CSV", +# "/Users/romainegele/Documents/Research/LCDB/lcdb/publications/2023-neurips/experiments/alcf/polaris/knn/output/lcdb.workflow.sklearn.KNNWorkflow/3/42-42-42/results.csv.gz", +# ) +DEEPHYPER_BENCHMARK_MAX_FIDELITY = int( + os.environ.get("DEEPHYPER_BENCHMARK_MAX_FIDELITY", 100) +) +DEEPHYPER_BENCHMARK_FIDELITY = os.environ.get("DEEPHYPER_BENCHMARK_FIDELITY", "epoch") +DEEPHYPER_BENCHMARK_CSV = os.environ.get( + "DEEPHYPER_BENCHMARK_CSV", + "/Users/romainegele/Documents/Research/LCDB/lcdb/publications/2023-neurips/experiments/alcf/polaris/densenn/output/lcdb.workflow.keras.DenseNNWorkflow/3/42-42-42/results.csv.gz", +) + + +# WorkflowClass = import_attr_from_module(DEEPHYPER_BENCHMARK_WORKFLOW) +# config_space = WorkflowClass.config_space() +# problem = HpProblem(config_space=config_space) +problem = HpProblem() + +with gzip.GzipFile(DEEPHYPER_BENCHMARK_CSV, "rb") as f: + r_df, r_df_failed = read_csv_results(f) +DEEPHYPER_BENCHMARK_DATA = r_df + +if DEEPHYPER_BENCHMARK_FIDELITY == "anchor": + query_anchor_values = QueryAnchorValues() + query_confusion_matrix_values_on_val = QueryMetricValuesFromAnchors( + "confusion_matrix", split_name="val" + ) + query_confusion_matrix_values_on_test = QueryMetricValuesFromAnchors( + "confusion_matrix", split_name="test" + ) +elif DEEPHYPER_BENCHMARK_FIDELITY == "epoch": + # Load the learning "epoch"-curve at last epoch + query_anchor_values_ = QueryEpochValues() + query_confusion_matrix_values_on_val_ = QueryMetricValuesFromEpochs( + "confusion_matrix", split_name="val" + ) + query_confusion_matrix_values_on_test_ = QueryMetricValuesFromEpochs( + "confusion_matrix", split_name="test" + ) + query_anchor_values = lambda x: query_anchor_values_(x)[-1] + query_confusion_matrix_values_on_val = ( + lambda x: query_confusion_matrix_values_on_val_(x)[-1] + ) + query_confusion_matrix_values_on_test = ( + lambda x: query_confusion_matrix_values_on_test_(x)[-1] + ) + + source = DEEPHYPER_BENCHMARK_DATA["m:json"] + values = source.apply(lambda x: query_confusion_matrix_values_on_val(x)).to_list() + mask = [len(x) > 0 for x in values] + DEEPHYPER_BENCHMARK_DATA = DEEPHYPER_BENCHMARK_DATA[mask] + + source = DEEPHYPER_BENCHMARK_DATA["m:json"] + values = source.apply(lambda x: query_confusion_matrix_values_on_test(x)).to_list() + mask = [len(x) > 0 for x in values] + DEEPHYPER_BENCHMARK_DATA = DEEPHYPER_BENCHMARK_DATA[mask] +else: + raise ValueError(f"Unknown fidelity: {DEEPHYPER_BENCHMARK_FIDELITY}") + +problem.add_hyperparameter((0, len(DEEPHYPER_BENCHMARK_DATA) - 1), "eval_id") + + +# TODO: To test LCPFN with accuracy metric +def query_balanced_accuracy_values_on_val(x): + cm = query_confusion_matrix_values_on_val(x) + return list(map(balanced_accuracy_from_confusion_matrix, cm)) + + +def query_coefficient_of_determiniation_values_on_val(x): + cm = query_confusion_matrix_values_on_val(x) + num_classes = len(cm[0]) + balanced_error_rate_baseline = 1 - 1 / num_classes + return list( + map( + lambda x: 1 + - (1 - balanced_accuracy_from_confusion_matrix(x)) + / balanced_error_rate_baseline, + cm, + ) + ) + + +def query_coefficient_of_determiniation_values_on_test(x): + cm = query_confusion_matrix_values_on_test(x) + num_classes = len(cm[0]) + balanced_error_rate_baseline = 1 - 1 / num_classes + return list( + map( + lambda x: 1 + - (1 - balanced_accuracy_from_confusion_matrix(x)) + / balanced_error_rate_baseline, + cm, + ) + ) + + +@profile +def run(job: RunningJob, optuna_trial=None) -> dict: + # otherwise failure + eval_id = job.parameters["eval_id"] + + source = DEEPHYPER_BENCHMARK_DATA.iloc[eval_id]["m:json"] + anchor_values = query_anchor_values(source) + + # TODO: to test with LCPFN + objective_values = query_balanced_accuracy_values_on_val(source) + + objective_val_values = query_coefficient_of_determiniation_values_on_val(source) + objective_test_values = query_coefficient_of_determiniation_values_on_test(source) + length = min(len(objective_val_values), len(objective_test_values)) + anchor_values = anchor_values[:length] + objective_values = objective_values[:length] + objective_val_values = objective_val_values[:length] + objective_test_values = objective_test_values[:length] + + objective_values = np.array(objective_values) + objective_val_values = np.array(objective_val_values) + objective_test_values = np.array(objective_test_values) + anchor_values = np.array(anchor_values) + + mask = anchor_values <= DEEPHYPER_BENCHMARK_MAX_FIDELITY + anchor_values = anchor_values[mask].tolist() + objective_values = objective_values[mask].tolist() + objective_val_values = objective_val_values[mask].tolist() + objective_test_values = objective_test_values[mask].tolist() + + if anchor_values[-1] <= DEEPHYPER_BENCHMARK_MAX_FIDELITY: + anchor_values.append(DEEPHYPER_BENCHMARK_MAX_FIDELITY) + objective_values.append(objective_values[-1]) + objective_val_values.append(objective_val_values[-1]) + objective_test_values.append(objective_test_values[-1]) + + min_b, max_b = anchor_values[0], anchor_values[-1] + + other_metadata = {} + + consumed_time = 0 # the cost here corresponds to time + + if optuna_trial: + for i, budget_i in enumerate(anchor_values): + objective_i = objective_val_values[i] + + optuna_trial.report(objective_i, step=budget_i) + if optuna_trial.should_prune(): + break + + objective = objective_i + + else: + for i, budget_i in enumerate(anchor_values): + # objective_i = objective_val_values[i] + # TODO: to test with LCPFN + objective_i = objective_values[i] + + job.record(budget_i, objective_i) + if job.stopped(): + break + + objective = job.objective + + if hasattr(job, "stopper") and hasattr(job.stopper, "infos_stopped"): + other_metadata["infos_stopped"] = job.stopper.infos_stopped + + metadata = { + "budget": budget_i, + "stopped": budget_i < max_b, + "objective_test": objective_test_values[-1], + "objective_val": objective_val_values[-1], + } + metadata.update(other_metadata) + + return { + "objective": objective, + "metadata": metadata, + } + + +if __name__ == "__main__": + print(problem) + # default_config = problem.default_configuration + for eval_id in range(10): + default_config = dict(eval_id=eval_id) + print(f"{default_config=}") + result = run(RunningJob(parameters=default_config)) + print(f"{result=}") + print() diff --git a/src/deephyper_benchmark/lib/lcmbench/README.md b/src/deephyper_benchmark/lib/lcmbench/README.md new file mode 100644 index 0000000..f65fb4a --- /dev/null +++ b/src/deephyper_benchmark/lib/lcmbench/README.md @@ -0,0 +1,4 @@ +# LCMBench: Learning Curve Models Benchmark + +> **Warning** +> Work in progress, this benchmark is not yet ready. diff --git a/src/deephyper_benchmark/lib/lcmbench/lcdb/README.md b/src/deephyper_benchmark/lib/lcmbench/lcdb/README.md new file mode 100644 index 0000000..046e4b5 --- /dev/null +++ b/src/deephyper_benchmark/lib/lcmbench/lcdb/README.md @@ -0,0 +1,3 @@ +# LCDB: Learning Curve DataBase + +Benchmark based on the LCDB project: https://github.com/fmohr/lcdb \ No newline at end of file diff --git a/src/deephyper_benchmark/lib/lcmbench/lcdb/__init__.py b/src/deephyper_benchmark/lib/lcmbench/lcdb/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/deephyper_benchmark/lib/lcmbench/lcdb/benchmark.py b/src/deephyper_benchmark/lib/lcmbench/lcdb/benchmark.py new file mode 100644 index 0000000..8067a67 --- /dev/null +++ b/src/deephyper_benchmark/lib/lcmbench/lcdb/benchmark.py @@ -0,0 +1,11 @@ +from deephyper_benchmark import * + + +class LCDBBenchmark(Benchmark): + + version = "0.0.1" + + requires = { + "py-lcdb": {"type": "pip", "name": "lcdb"}, + "py-func-timeout": {"type": "pip", "name": "func-timeout"}, + } diff --git a/src/deephyper_benchmark/lib/lcmbench/lcdb/hpo.py b/src/deephyper_benchmark/lib/lcmbench/lcdb/hpo.py new file mode 100644 index 0000000..280ce6e --- /dev/null +++ b/src/deephyper_benchmark/lib/lcmbench/lcdb/hpo.py @@ -0,0 +1,128 @@ +import numpy as np + +import lcdb + +from deephyper.evaluator import profile, RunningJob +from deephyper.hpo import HpProblem + + +df_meta = lcdb.get_meta_features() + +all_openmlid = df_meta["openmlid"].tolist() +all_name = df_meta["Name"].tolist() +all_algorithms = [ + "SVC_linear", + "SVC_poly", + "SVC_rbf", + "SVC_sigmoid", + "sklearn.tree.DecisionTreeClassifier", + "sklearn.tree.ExtraTreeClassifier", + "sklearn.linear_model.LogisticRegression", + "sklearn.linear_model.PassiveAggressiveClassifier", + "sklearn.linear_model.Perceptron", + "sklearn.linear_model.RidgeClassifier", + "sklearn.linear_model.SGDClassifier", + "sklearn.neural_network.MLPClassifier", + # "sklearn.discriminant_analysis.LinearDiscriminantAnalysis", + # "sklearn.discriminant_analysis.QuadraticDiscriminantAnalysis", + "sklearn.naive_bayes.BernoulliNB", + # "sklearn.naive_bayes.MultinomialNB", # often missing for large datasets + "sklearn.neighbors.KNeighborsClassifier", + "sklearn.ensemble.ExtraTreesClassifier", + "sklearn.ensemble.RandomForestClassifier", + "sklearn.ensemble.GradientBoostingClassifier", +] + +problem = HpProblem() +problem.add_hyperparameter(all_algorithms, "model") + + +@profile +def run(job: RunningJob, optuna_trial=None, task_id=3) -> dict: + + model = job.parameters["model"] + + try: + curve = lcdb.get_curve(task_id, model, metric="accuracy") + except Exception: + if optuna_trial: + objective = 0 # accuracy 0 + else: + objective = [[1], [0]] # accuracy 0 at step 1 + return { + "objective": objective, + "metadata": { + "budget": budget_i, + "stopped": budget_i < anchors[-1], + "duration": cum_time, + }, + } + + anchors, _, scores_valid, _ = curve + _, times = lcdb.get_train_times(task_id, model) + + # scores_valid = [list(v) for v in scores_valid] + # times = [list(t) for t in times] + + anchors = np.array(anchors).tolist() + # print(model, type(scores_valid), type(scores_valid[0]), type(scores_valid[0][0])) + # scores_valid = np.array(np.array(scores_valid).tolist()) + # times = np.array(np.array(times).tolist()) + + # print("->", np.ndim(scores_valid)) + # print("->", scores_valid) + # print("->", np.shape(scores_valid)) + + # if np.ndim(scores_valid) == 2: + # scores_valid = np.mean(scores_valid, axis=1) + scores_valid = np.array([v[0] for v in scores_valid]) + times = np.array([t[0] for t in times]) + + scores_valid = scores_valid.tolist() + times = times.tolist() + + cum_time = 0 + + if optuna_trial: + + for i, budget_i in enumerate(anchors): + objective_i = scores_valid[i] + cum_time += times[i] + optuna_trial.report(objective_i, step=budget_i) + if optuna_trial.should_prune(): + break + + return { + "objective": objective_i, + "metadata": { + "budget": budget_i, + "stopped": budget_i < anchors[-1], + "duration": cum_time, + }, + } + + else: + + for i, budget_i in enumerate(anchors): + objective_i = scores_valid[i] + cum_time += times[i] + job.record(budget_i, objective_i) + if job.stopped(): + break + + return { + "objective": job.observations, + "metadata": { + "budget": budget_i, + "stopped": budget_i < anchors[-1], + "duration": cum_time, + }, + } + + +if __name__ == "__main__": + print(problem) + default_config = problem.default_configuration + print(f"{default_config=}") + result = run(RunningJob(parameters=default_config)) + print(f"{result=}") diff --git a/src/deephyper_benchmark/lib/lcmbench/loglin2/README.md b/src/deephyper_benchmark/lib/lcmbench/loglin2/README.md new file mode 100644 index 0000000..e9bd949 --- /dev/null +++ b/src/deephyper_benchmark/lib/lcmbench/loglin2/README.md @@ -0,0 +1,5 @@ +# `LogLin2` + +Benchmark for Multi-Fidelity Black-Box Optimization. + +$$f_\text{loglin2}(z;\rho) = b(z)^{\rho_1} \cdot \exp{\rho_0}$$ \ No newline at end of file diff --git a/src/deephyper_benchmark/lib/lcmbench/loglin2/__init__.py b/src/deephyper_benchmark/lib/lcmbench/loglin2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/deephyper_benchmark/lib/lcmbench/loglin2/benchmark.py b/src/deephyper_benchmark/lib/lcmbench/loglin2/benchmark.py new file mode 100644 index 0000000..ca8cd5b --- /dev/null +++ b/src/deephyper_benchmark/lib/lcmbench/loglin2/benchmark.py @@ -0,0 +1,12 @@ +import os + +from deephyper_benchmark import * + +DIR = os.path.dirname(os.path.abspath(__file__)) + + +class LogLin2Benchmark(Benchmark): + + version = "0.0.1" + + requires = {} \ No newline at end of file diff --git a/src/deephyper_benchmark/lib/lcmbench/loglin2/hpo.py b/src/deephyper_benchmark/lib/lcmbench/loglin2/hpo.py new file mode 100644 index 0000000..2382caa --- /dev/null +++ b/src/deephyper_benchmark/lib/lcmbench/loglin2/hpo.py @@ -0,0 +1,71 @@ +import os + +import numpy as np + +DIR = os.path.dirname(os.path.abspath(__file__)) + +from deephyper.evaluator import profile, RunningJob +from deephyper.hpo import HpProblem + + +problem = HpProblem() +problem.add_hyperparameter((0.0, 1.0), "rho_0") +problem.add_hyperparameter((0.0, 1.0/25.0), "rho_1") + + +def f_loglin2(b, rho): + return np.power(b, rho[1]) * np.exp(rho[0]) + + +@profile +def run(job: RunningJob, optuna_trial=None) -> dict: + + # otherwise failure + min_b, max_b = 1, 100 + + rho = [job.parameters["rho_0"], job.parameters["rho_1"]] + f = lambda b: f_loglin2(b, rho) + + objective_test = f(max_b) + + if optuna_trial: + + for budget_i in range(min_b, max_b + 1): + objective_i = f(budget_i) + optuna_trial.report(objective_i, step=budget_i) + if optuna_trial.should_prune(): + break + + return { + "objective": objective_i, + "metadata": { + "budget": budget_i, + "stopped": budget_i < max_b, + "objective_test": objective_test, + }, + } + + else: + + for budget_i in range(min_b, max_b + 1): + objective_i = f(budget_i) + job.record(budget_i, objective_i) + if job.stopped(): + break + + return { + "objective": job.observations, + "metadata": { + "budget": budget_i, + "stopped": budget_i < max_b, + "objective_test": objective_test, + }, + } + + +if __name__ == "__main__": + print(problem) + default_config = problem.default_configuration + print(f"{default_config=}") + result = run(RunningJob(parameters=default_config)) + print(f"{result=}") diff --git a/src/deephyper_benchmark/lib/lcmbench/pow3/README.md b/src/deephyper_benchmark/lib/lcmbench/pow3/README.md new file mode 100644 index 0000000..01c296a --- /dev/null +++ b/src/deephyper_benchmark/lib/lcmbench/pow3/README.md @@ -0,0 +1,5 @@ +# `Pow3`: Inverse Power Law with 3 Parameters + +Inverse-Power Law benchmark for Multi-Fidelity Black-Box Optimization. + +$$f_\text{pow3}(b;\rho) = \rho_0 + \rho_1 \cdot b^{-\rho_2}$$ \ No newline at end of file diff --git a/src/deephyper_benchmark/lib/lcmbench/pow3/__init__.py b/src/deephyper_benchmark/lib/lcmbench/pow3/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/deephyper_benchmark/lib/lcmbench/pow3/benchmark.py b/src/deephyper_benchmark/lib/lcmbench/pow3/benchmark.py new file mode 100644 index 0000000..e740b42 --- /dev/null +++ b/src/deephyper_benchmark/lib/lcmbench/pow3/benchmark.py @@ -0,0 +1,12 @@ +import os + +from deephyper_benchmark import * + +DIR = os.path.dirname(os.path.abspath(__file__)) + + +class Pow3Benchmark(Benchmark): + + version = "0.0.1" + + requires = {} \ No newline at end of file diff --git a/src/deephyper_benchmark/lib/lcmbench/pow3/hpo.py b/src/deephyper_benchmark/lib/lcmbench/pow3/hpo.py new file mode 100644 index 0000000..2f994a3 --- /dev/null +++ b/src/deephyper_benchmark/lib/lcmbench/pow3/hpo.py @@ -0,0 +1,81 @@ +import os + +DIR = os.path.dirname(os.path.abspath(__file__)) + +from deephyper.evaluator import profile, RunningJob +from deephyper.hpo import HpProblem + + +problem = HpProblem() +problem.add_hyperparameter((1e-6, 10, "log-uniform"), "alpha") +problem.add_hyperparameter((1.0, 10.0), "beta") +problem.add_hyperparameter((0.1, 0.5), "gamma") + +# optimum is at +# C(s=100, alpha=1e-6, beta=1.0, gamma=0.5) -> 0.100001 + + +def ipl_curve(s, alpha, beta, gamma): + """Inverse Power-Law Model""" + return alpha + beta * s**-gamma + + +@profile +def run(job: RunningJob, optuna_trial=None) -> dict: + + print(f"{job.id=}") + + # otherwise failure + min_b, max_b = 1, 1000 + + alpha = job.parameters["alpha"] + beta = job.parameters["beta"] + gamma = job.parameters["gamma"] + + objective_test = -ipl_curve(max_b, alpha, beta, gamma) + + if optuna_trial: + + for budget_i in range(min_b, max_b + 1): + objective_i = -ipl_curve( + budget_i, alpha, beta, gamma + ) # maximisation in deephyper + optuna_trial.report(objective_i, step=budget_i) + if optuna_trial.should_prune(): + break + + return { + "objective": objective_i, + "metadata": { + "budget": budget_i, + "stopped": budget_i < max_b, + "objective_test": objective_test, + }, + } + + else: + + for budget_i in range(min_b, max_b + 1): + objective_i = -ipl_curve( + budget_i, alpha, beta, gamma + ) # maximisation in deephyper + job.record(budget_i, objective_i) + if job.stopped(): + break + + return { + "objective": job.observations, + "metadata": { + "budget": budget_i, + "stopped": budget_i < max_b, + "objective_test": objective_test, + }, + } + + +if __name__ == "__main__": + print(problem) + default_config = problem.default_configuration + print(f"{default_config=}") + result = run(RunningJob(parameters=default_config)) + print(f"{result=}") diff --git a/src/deephyper_benchmark/lib/pinnbench/Burgers/__init__.py b/src/deephyper_benchmark/lib/pinnbench/Burgers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/deephyper_benchmark/lib/pinnbench/Burgers/benchmark.py b/src/deephyper_benchmark/lib/pinnbench/Burgers/benchmark.py new file mode 100644 index 0000000..cfc92cf --- /dev/null +++ b/src/deephyper_benchmark/lib/pinnbench/Burgers/benchmark.py @@ -0,0 +1,17 @@ +import os + +from deephyper_benchmark import * + +DIR = os.path.dirname(os.path.abspath(__file__)) + + +class PINNDiffusionReactionBenchmark(Benchmark): + version = "0.0.1" + + requires = { + "py-pip-requirements": { + "step": "install", + "type": "pip", + "args": "install -r " + os.path.join(DIR, "requirements.txt"), + }, + } diff --git a/src/deephyper_benchmark/lib/pinnbench/Burgers/hpo.py b/src/deephyper_benchmark/lib/pinnbench/Burgers/hpo.py new file mode 100644 index 0000000..43ab995 --- /dev/null +++ b/src/deephyper_benchmark/lib/pinnbench/Burgers/hpo.py @@ -0,0 +1,111 @@ +import numpy as np + +from deephyper.hpo import HpProblem +from deephyper.search.hps import CBO +from .model import get_data, PINN, BurgerSupervisor, plotter +from deephyper.stopper import LCModelStopper +from deephyper.evaluator import profile, RunningJob + + +@profile +def run(job: RunningJob) -> dict: + # load data + nu = 0.01 / np.pi + train, val, test = get_data(NT=200, NX=128, X_U=1, X_L=-1, T_max=1, Nu=nu) + + config = job.parameters + min_b = 1 + max_b = 1000 + + num_layers = config["num_layers"] + hidden_dim = config["hidden_dim"] + output_dim = 1 + epochs = 100 + lr = config["lr"] + alpha = config["alpha"] + activation = config["activation"] + net = PINN( + num_layers=num_layers, + hidden_dim=hidden_dim, + output_dim=output_dim, + act_fn=activation, + ) + sup = BurgerSupervisor(nu, net, epochs, lr, alpha) + + for budget_i in range(min_b, max_b + 1): + train_loss, eval = sup.step(train, val) + objective_i = -eval # maximizing in deephyper + job.record(budget_i, objective_i) + if job.stopped(): + break + objective = job.objective + + # calculate test scores + pred_test = sup.net(test["x"][:, 0], test["x"][:, 1]) + test_loss = np.mean((test["y"] - pred_test) ** 2) + + metadata = { + "num_parameters": sup.net.count_params(), + "train_loss": train_loss, + "val_loss": eval, + "test_loss": test_loss, + "budget": budget_i, + "stopped": budget_i < max_b, + "infos_stopped": job.stopper.infos_stopped + if hasattr(job, "stopper") and hasattr(job.stopper, "infos_stopped") + else None, + } + return {"objective": objective, "metadata": metadata} + + +# define the search space +problem = HpProblem() +problem.add_hyperparameter((5, 20), "num_layers", default_value=5) +problem.add_hyperparameter((1e-5, 1e-2), "lr", default_value=0.01) +problem.add_hyperparameter((5, 50), "hidden_dim", default_value=5) +problem.add_hyperparameter((0.0, 5.0), "alpha", default_value=0.5) +problem.add_hyperparameter( + ["relu", "leaky_relu", "tanh", "elu", "gelu", "sigmoid"], + "activation", + default_value="tanh", +) + + +def evaluate(config): + """ + Evaluate an hyperparameter configuration + on training/validation and testing data. + """ + nu = 0.01 / np.pi + train, val, test = get_data(NT=200, NX=100, X_U=1.0, X_L=-1.0, T_max=1, Nu=nu) + num_layers = config["num_layers"] + hidden_dim = config["hidden_dim"] + output_dim = 1 + epochs = config["epochs"] + lr = config["lr"] + alpha = config["alpha"] + activation = config["activation"] + net = PINN( + num_layers=num_layers, + hidden_dim=hidden_dim, + output_dim=output_dim, + act_fn=activation, + ) + sup = BurgerSupervisor(nu=nu, net=net, epochs=epochs, lr=lr, alpha=alpha) + val_f_loss = sup.train(train, test) + + +if __name__ == "__main__": + import time + + start_time = time.time() + stopper = LCModelStopper(max_steps=1000, lc_model="mmf4") + scheduler = { + "type": "periodic-exp-decay", + "periode": 25, + "rate": 0.1, + } + + default_config = problem.default_configuration + result = run(RunningJob(parameters=default_config)) + print(f"{result=}") diff --git a/src/deephyper_benchmark/lib/pinnbench/Burgers/model.py b/src/deephyper_benchmark/lib/pinnbench/Burgers/model.py new file mode 100644 index 0000000..fb2a476 --- /dev/null +++ b/src/deephyper_benchmark/lib/pinnbench/Burgers/model.py @@ -0,0 +1,411 @@ +import numpy as np +import tensorflow as tf +import phi.flow as pl +from scipy.stats import qmc + + +def BurgersSolver( + NT: int, NX: int, X_U: float, X_L: float, T_max: float, Nu: float, IN_cond +): + """ + Burgers equation solver with Phiflow, + partially adopted from + https://physicsbaseddeeplearning.org/overview-burgers-forw.html + + args: + NT: number of points in the t-direction. + NX: number of points in the x-direction. + X_U: the upper bound of x range. + X_L: the lower bound of x range. + T_max: the maximum time lenght. + Nu: viscosity. + IN_cond: the initial condition function. + """ + DX = (X_U - X_L) / NX + DT = T_max / NT + x_ = np.linspace(X_L, X_U, NX) + t_ = np.linspace(0, T_max, NT) + xx, tt = np.meshgrid(x_, t_) + input_domain = np.stack((xx.flatten(), tt.flatten()), axis=1) + + x_range = np.linspace(X_L + DX / 2.0, X_U - DX / 2.0, NX) + INITIAL = IN_cond(x_range) + INITIAL = pl.math.tensor(INITIAL, pl.spatial("x")) # convert to phiflow tensor + velocity = pl.CenteredGrid( + INITIAL, pl.extrapolation.PERIODIC, x=NX, bounds=pl.Box(x=(X_L, X_U)) + ) + vt = pl.advect.semi_lagrangian(velocity, velocity, DT) + + velocities = [velocity] + age = 0.0 + for i in range(NT - 1): + v1 = pl.diffuse.explicit(velocities[-1], Nu, DT) + v2 = pl.advect.semi_lagrangian(v1, v1, DT) + age += DT + velocities.append(v2) + vels = np.array([v.values.numpy("x,vector") for v in velocities]) + u = np.squeeze(vels) + return x_, t_, u + + +def sample_latin_hypercube(self): + bc_engine = qmc.LatinHypercube(d=1) + t_d = bc_engine.random(n=50) + t_i = np.zeros((50, 1)) + t_d = np.append(t_i, t_d, axis=0) + return + + +def split_data(x_, t_, u, show_points=False): + """ + Split data into train, val, and test. + train: input coordinates on the boundaries and its solution (boundary conditions) + and the input coordinates of collocation points + val: the collocation points (in training) with solution + test: the entire domain and its solution. + """ + rd = np.random.RandomState(0) # set local random seed + lhc = qmc.LatinHypercube(d=1, seed=0) + + NUM_IC_PT = 50 + NUM_BC_PT = 50 + NUM_COL_PT = 10000 + + # x_ic_idx = np.array(rd.uniform(size=NUM_IC_PT) * len(x_), dtype=int) + x_ic_idx = np.array(lhc.random(n=NUM_IC_PT) * len(x_), dtype=int).flatten() + # print("sampled IC indices are:", x_ic_idx) + x_ic = x_[x_ic_idx] + input_ic = np.stack((x_ic, np.zeros(len(x_ic))), axis=1) + u_ic = u[0, x_ic_idx].reshape(-1, 1) + + # t_bc_idx = np.array(rd.uniform(size=NUM_BC_PT//2) * len(t_), dtype=int) + t_bc_idx = np.array(lhc.random(n=NUM_BC_PT) * len(t_), dtype=int).flatten() + t_bc = t_[t_bc_idx] + + input_bc_upper = np.stack( + [np.repeat(x_[-1], len(t_bc) // 2), t_bc[: NUM_BC_PT // 2]], axis=1 + ) + input_bc_lower = np.stack( + [np.repeat(x_[0], len(t_bc) // 2), t_bc[NUM_BC_PT // 2 :]], axis=1 + ) + input_bc = np.vstack([input_bc_lower, input_bc_upper]) + + u_bc_upper = u[t_bc_idx, -1] + u_bc_lower = u[t_bc_idx, 0] + u_bc = np.hstack([u_bc_lower, u_bc_upper]).reshape(-1, 1) + + t_col_idx = np.array(rd.uniform(size=NUM_COL_PT) * len(t_), dtype=int) + x_col_idx = np.array(rd.uniform(size=NUM_COL_PT) * len(x_), dtype=int) + input_col = np.stack((x_[x_col_idx], t_[t_col_idx]), axis=1) + u_col = u[t_col_idx, x_col_idx].reshape(-1, 1) + + x_u_train = np.vstack([input_ic, input_bc]) + y_u_train = np.vstack([u_ic, u_bc]) + idx = np.arange(0, NUM_IC_PT + NUM_BC_PT) + rd.shuffle(idx) + x_u_train = x_u_train[idx] + y_u_train = y_u_train[idx] + + x_f_train = input_col + + # prepare val data + rd2 = np.random.RandomState(42) + v_t_col_idx = np.array(rd2.uniform(size=NUM_COL_PT) * len(t_), dtype=int) + v_x_col_idx = np.array(rd2.uniform(size=NUM_COL_PT) * len(x_), dtype=int) + x_val = np.stack((x_[v_x_col_idx], t_[v_t_col_idx]), axis=1) + y_val = u[v_t_col_idx, v_x_col_idx].reshape(-1, 1) + + # prepare testing data + xx, tt = np.meshgrid(x_, t_) + input_domain = np.stack([xx.flatten(), tt.flatten()], axis=1) + y_test = u.flatten().reshape(-1, 1) + + train_data = {"x_u": x_u_train, "y_u": y_u_train, "x_f": x_f_train} + val_data = {"x": x_f_train, "y": u_col} + test_data = {"x": input_domain, "y": y_test} + + if show_points == True: + import matplotlib.pyplot as plt + + # plt.scatter(input_ic[:, 1], input_ic[:,0]) + # plt.scatter(input_bc[:, 1], input_bc[:,0]) + # plt.scatter(x_f_train[:,1], x_f_train[:,0]) + plt.scatter(x_val[:, 1], x_val[:, 0]) + plt.show() + + return train_data, val_data, test_data + + +def get_data(NT, NX, X_U, X_L, T_max, Nu): + def init_cond(x): + return -np.sin(np.pi * x) + + x_, t_, u = BurgersSolver( + NT=NT, NX=NX, X_U=X_U, X_L=X_L, T_max=T_max, Nu=Nu, IN_cond=init_cond + ) + train, val, test = split_data(x_, t_, u) + return train, val, test + + +class PINN(tf.keras.Model): + def __init__(self, num_layers, hidden_dim, output_dim, act_fn): + super().__init__() + # tf.random.set_seed(0) + tf.keras.utils.set_random_seed(0) + if act_fn == "relu": + self.act_fn = tf.nn.relu + elif act_fn == "leaky_relu": + self.act_fn = tf.nn.leaky_relu + elif act_fn == "elu": + self.act_fn = tf.nn.elu + elif act_fn == "gelu": + self.act_fn = tf.nn.gelu + elif act_fn == "tanh": + self.act_fn = tf.nn.tanh + elif act_fn == "sigmoid": + self.act_fn = tf.nn.sigmoid + else: + raise ValueError("%s is not in the activation function list" % act_fn) + + self.lys = [ + tf.keras.layers.Dense( + units=hidden_dim, + activation=self.act_fn, + kernel_initializer="glorot_uniform", + bias_initializer="zeros", + ) + ] + for n in range(num_layers): + self.lys.append( + tf.keras.layers.Dense( + units=hidden_dim, + activation=self.act_fn, + kernel_initializer="glorot_uniform", + bias_initializer="zeros", + ) + ) + self.lys.append( + tf.keras.layers.Dense( + units=output_dim, + activation=None, + kernel_initializer="glorot_uniform", + bias_initializer="zeros", + ) + ) + + def call(self, x, t): + x = tf.cast(x, tf.float32) + t = tf.cast(t, tf.float32) + + x_concat = tf.stack((x, t), axis=1) + for layer in self.lys: + x_concat = layer(x_concat) + return x_concat + + +class BurgerSupervisor: + def __init__(self, nu, net, epochs, lr, alpha): + self.nu = nu + self.net = net # the net predicting u from x, t pairs. + self.epochs = epochs + + self.alpha = alpha + self.optimizer = tf.keras.optimizers.Adam(learning_rate=lr) + + self.loss = tf.losses.MeanSquaredError() + + @tf.function + def f_net(self, x, t): + x = tf.cast(x, tf.float32) + t = tf.cast(t, tf.float32) + u0 = tf.squeeze(self.net(x, t)) + u_t = tf.gradients(u0, t)[0] + u_x = tf.gradients(u0, x)[0] + u_xx = tf.gradients(u_x, x)[0] + F = u_t + u0 * u_x - (0.01 / np.pi) * u_xx + return F + + def step(self, train, val): + x_u_train = tf.cast(train["x_u"], tf.float32) + x_f_train = tf.cast(train["x_f"], tf.float32) + y_train = tf.cast(train["y_u"], tf.float32) + + x_val = tf.cast(val["x"], tf.float32) + y_val = tf.cast(val["y"], tf.float32) + x_u = x_u_train[:, 0] + t_u = x_u_train[:, 1] + + x_f = x_f_train[:, 0] + t_f = x_f_train[:, 1] + with tf.GradientTape() as t: + u_out = self.net(x_u, t_u) + f_out = self.f_net(x_f, t_f) + loss_u = self.loss(y_train, u_out) + loss_f = self.loss(0, f_out) + tot_loss = loss_u + self.alpha * loss_f + + grads = t.gradient(tot_loss, self.net.trainable_weights) + self.optimizer.apply_gradients(zip(grads, self.net.trainable_weights)) + train_loss = loss_u.numpy() + loss_f.numpy() + val_pred = self.net(x_val[:, 0], x_val[:, 1]) + val_u_loss = self.loss(y_val, val_pred).numpy() + return train_loss, val_u_loss + + def train(self, train, val): + x_u_train = tf.cast(train["x_u"], tf.float32) + x_f_train = tf.cast(train["x_f"], tf.float32) + y_train = tf.cast(train["y_u"], tf.float32) + + x_val = tf.cast(val["x"], tf.float32) + y_val = tf.cast(val["y"], tf.float32) + x_u = x_u_train[:, 0] + t_u = x_u_train[:, 1] + + x_f = x_f_train[:, 0] + t_f = x_f_train[:, 1] + for i in range(self.epochs): + train_ls = [] + with tf.GradientTape() as t: + u_out = self.net(x_u, t_u) + f_out = self.f_net(x_f, t_f) + loss_u = self.loss(y_train, u_out) + loss_f = self.loss(0, f_out) + tot_loss = loss_u + self.alpha * loss_f + train_ls.append(tot_loss) + grads = t.gradient(tot_loss, self.net.trainable_weights) + self.optimizer.apply_gradients(zip(grads, self.net.trainable_weights)) + obj = loss_u.numpy() + loss_f.numpy() + # print('epoch {}, obj is {}'.format(i, obj)) + # validation loss + val_r2 = self.r2(y_val, self.net(x_val[:, 0], x_val[:, 1])) + val_f_loss = self.loss(0, self.f_net(x_val[:, 0], x_val[:, 1])).numpy() + val_pred = self.net(x_val[:, 0], x_val[:, 1]) + val_u_loss = self.loss(y_val, val_pred).numpy() + # val_f_loss = self.loss(0, self.f_net(x_val[:, 0], x_val[:, 1])).numpy() + val_loss = val_u_loss + print( + "Epoch %d, training loss: (%.4f, %.4f), obj loss %.4f" + % (i + 1, loss_u.numpy(), loss_f.numpy(), val_loss) + ) + return val_loss # return the objective. + + def r2(self, true, pred): + true = tf.cast(true, tf.float32) + rss = tf.reduce_sum((true - pred) ** 2) + tss = tf.reduce_sum((true - tf.reduce_mean(true)) ** 2) + return 1 - rss / tss + + def test(self, x_test, y_test): + out = self.net(x_test) + r2 = self.r2(y_test, out) + f_loss = self.f_loss(x_test) + return r2, f_loss + + +def plotter(x_, u_, name, NUMX, NUMT, **kwargs): + """ + x_: shape=[num_points, 2]. First dim being x and second t. + u_: shape=[num_points,] + NUMX: number of points the in x-direction. + NUMT: number of points the in t-direction. + """ + import matplotlib.animation as animation + + u_ = u_.reshape(NUMT, NUMX).T + xv = x_[:, 0].reshape(NUMT, NUMX) + tv = x_[:, 1].reshape(NUMT, NUMX) + + import matplotlib.pyplot as plt + + fig, ax = plt.subplots(2, 2, figsize=(16, 9)) + ax[0, 0].imshow( + u_, + interpolation="nearest", + cmap="rainbow", + extent=[tv.min(), tv.max(), xv.min(), xv.max()], + origin="lower", + aspect="auto", + ) + ax[0, 0].set_xlabel("t") + ax[0, 0].set_ylabel("x") + + (line,) = ax[1, 0].plot(xv[0], u_[:, 0]) + text = ax[1, 0].text(0.8, 0.9, " ", transform=ax[1, 0].transAxes) + ax[1, 0].set_xlabel("x") + ax[1, 0].set_ylabel("u") + + u_true = kwargs["u_true"].reshape(NUMT, NUMX).T + ax[0, 1].set_title("True", fontsize=24) + ax[0, 0].set_title("Predicted", fontsize=24) + ax[0, 1].imshow( + u_true, + interpolation="nearest", + cmap="rainbow", + extent=[tv.min(), tv.max(), xv.min(), xv.max()], + origin="lower", + aspect="auto", + ) + ax[0, 1].set_xlabel("t") + ax[0, 1].set_ylabel("x") + + (line_true,) = ax[1, 1].plot(xv[0], u_true[:, 0]) + text_true = ax[1, 1].text(0.8, 0.9, " ", transform=ax[1, 1].transAxes) + ax[1, 0].set_xlabel("x") + ax[1, 0].set_ylabel("u") + + T = np.linspace(x_[:, 1].min(), x_[:, 1].max(), NUMT) + + def animate(t): + line.set_ydata(u_[:, t]) + line_true.set_ydata(u_true[:, t]) + text.set_text("t= %.3f" % T[t]) + text_true.set_text("t= %.3f" % T[t]) + return line, text, line_true, text_true + + ani = animation.FuncAnimation(fig, animate, frames=u_.shape[1], interval=50) + plt.tight_layout() + writergif = animation.PillowWriter(fps=60) + ani.save("./Burgers_" + name + ".gif", writer=writergif) + + +if __name__ == "__main__": + import time + import pickle + + start_time = time.time() + config = { + "num_layers": 17, + "hidden_dim": 9, + "epochs": 1000, + "lr": 0.00580381, + "alpha": 0.050742893, + "activation": "tanh", + } + + nu = 0.01 / np.pi + train, val, test = get_data(NT=200, NX=128, X_U=1, X_L=-1, T_max=1, Nu=nu) + with open("./data/data.pkl", "rb") as f: + data = pickle.load(f) + + train = data["train"] + val = data["val"] + + num_layers = config["num_layers"] + hidden_dim = config["hidden_dim"] + output_dim = 1 + epochs = config["epochs"] + lr = config["lr"] + alpha = config["alpha"] + activation = config["activation"] + net = PINN( + num_layers=num_layers, + hidden_dim=hidden_dim, + output_dim=output_dim, + act_fn=activation, + ) + sup = BurgerSupervisor(nu, net, epochs, lr, alpha) + val_f_loss = sup.train(train, val) + print("--- %s seconds ---" % (time.time() - start_time)) + print("objective is ", val_f_loss) + # pred_test = net(test['x'][:,0], test['x'][:,1]).numpy() + # plotter(test['x'], pred_test, 'true_pred_hps-adbo', 128, 200, u_true=test['y']) diff --git a/src/deephyper_benchmark/lib/pinnbench/Burgers/requirements.txt b/src/deephyper_benchmark/lib/pinnbench/Burgers/requirements.txt new file mode 100644 index 0000000..ff2206e --- /dev/null +++ b/src/deephyper_benchmark/lib/pinnbench/Burgers/requirements.txt @@ -0,0 +1,5 @@ +deephyper>=0.5.0 +matplotlib==3.4.2 +numpy==1.23.4 +phiflow==2.2.7 +tensorflow==2.10.0 \ No newline at end of file diff --git a/src/deephyper_benchmark/lib/pinnbench/DiffusionReaction/__init__.py b/src/deephyper_benchmark/lib/pinnbench/DiffusionReaction/__init__.py new file mode 100644 index 0000000..dc5340c --- /dev/null +++ b/src/deephyper_benchmark/lib/pinnbench/DiffusionReaction/__init__.py @@ -0,0 +1 @@ +from . import hpo diff --git a/src/deephyper_benchmark/lib/pinnbench/DiffusionReaction/benchmark.py b/src/deephyper_benchmark/lib/pinnbench/DiffusionReaction/benchmark.py new file mode 100644 index 0000000..bacef6c --- /dev/null +++ b/src/deephyper_benchmark/lib/pinnbench/DiffusionReaction/benchmark.py @@ -0,0 +1,28 @@ +import os + +from deephyper_benchmark import * + +DIR = os.path.dirname(os.path.abspath(__file__)) + + +class PINNDiffusionReactionBenchmark(Benchmark): + version = "0.0.1" + + requires = { + "py-pip-requirements": { + "step": "install", + "type": "pip", + "args": "install -r " + os.path.join(DIR, "requirements.txt"), + }, + "bash-install": { + "step": "install", + "type": "cmd", + "cmd": "cd .. && bash " + os.path.join(DIR, "./install.sh"), + }, + "deepxde-backend": { + "step": "load", + "type": "env", + "key": "DDE_BACKEND", + "value": "pytorch", + }, + } diff --git a/src/deephyper_benchmark/lib/pinnbench/DiffusionReaction/hpo.py b/src/deephyper_benchmark/lib/pinnbench/DiffusionReaction/hpo.py new file mode 100644 index 0000000..7df1633 --- /dev/null +++ b/src/deephyper_benchmark/lib/pinnbench/DiffusionReaction/hpo.py @@ -0,0 +1,206 @@ +import os + +import numpy as np +import torch +from deephyper.evaluator import RunningJob, profile +from deephyper.hpo import HpProblem +from deephyper.stopper.integration.deepxde import DeepXDEStopperCallback +from deepxde.callbacks import EarlyStopping +from fvcore.nn import FlopCountAnalysis +from pdebench.models.pinn.train import run_training + +from deephyper_benchmark.integration.torch import count_params +from deephyper_benchmark.utils.json_utils import array_to_json + +from .model import FNN, ACTIVATIONS + +DIR = os.path.dirname(os.path.abspath(__file__)) +DEEPHYPER_BENCHMARK_MOO = bool(int(os.environ.get("DEEPHYPER_BENCHMARK_MOO", 0))) + +# define the search space +problem = HpProblem() +problem.add_hyperparameter((5, 20), "num_layers", default_value=10) +problem.add_hyperparameter((5, 100), "num_neurons", default_value=10) +problem.add_hyperparameter((100, 1000), "epochs", default_value=100) +problem.add_hyperparameter( + list(ACTIVATIONS.keys()), + "activation", + default_value="relu", +) +problem.add_hyperparameter([True, False], "skip_co", default_value=False) +problem.add_hyperparameter((0.0, 0.5), "dropout", default_value=0) + +# Regularization hyperparameters +problem.add_hyperparameter([True, False], "batch_norm", default_value=False) +problem.add_hyperparameter(["None", "l2"], "regularization", default_value="None") +problem.add_hyperparameter( + (1e-5, 1.0, "log-uniform"), "weight_decay", default_value=0.01 +) +problem.add_hyperparameter( + ["Glorot normal", "Glorot uniform", "He normal", "He uniform"], + "kernel_initializer", + default_value="Glorot normal", +) + +# Optimization hyperparameters +problem.add_hyperparameter(["None", "step"], "decay", default_value="None") +problem.add_hyperparameter((1, 100), "decay_step_size", default_value=5) +problem.add_hyperparameter((1e-5, 1.0, "log-uniform"), "decay_gamma", default_value=0.1) + +problem.add_hyperparameter( + ["adam", "sgd", "rmsprop", "adamw"], "optimizer", default_value="adam" +) +problem.add_hyperparameter( + (1e-5, 1e-1, "log-uniform"), "learning_rate", default_value=0.01 +) + +# Loss weights is tuned only if MOO is activated. +if DEEPHYPER_BENCHMARK_MOO: + problem.add_hyperparameter((0.1, 0.9), "loss_weights", default_value=0.5) + + +@profile +def run(job: RunningJob) -> dict: + config = job.parameters.copy() + dataset = "2D_diff-react_NA_NA" + + for k, v in config.items(): + if v == "None": + config[k] = None + + if "loss_weights" not in config: + lw = 0.5 + else: + lw = config["loss_weights"] + config["loss_weights"] = np.array([lw, lw, 1 - lw, 1 - lw, 1 - lw, 1 - lw]) + + # https://github.com/lululxvi/deepxde/blob/master/deepxde/optimizers/pytorch/optimizers.py + if config["decay"] == "step": + config["decay"] = ("step", config["decay_step_size"], config["decay_gamma"]) + + # To avoid error: https://github.com/lululxvi/deepxde/blob/master/deepxde/optimizers/pytorch/optimizers.py#L45C4-L45C4 + if config["optimizer"] == "adamw": + config["regularization"] = "l2" + + stopper_callback = DeepXDEStopperCallback(job) + + error_type = None + try: + ( + val_loss, + test_loss, + losshistory, + model, + duration_batch_inference, + ) = run_training( + net_class=FNN, + scenario="diff-react", + epochs=config["epochs"], + learning_rate=config["learning_rate"], + model_update=500, + root_path=os.path.join( + DIR, "../build/PDEBench-DH/pdebench/data/" + dataset + ), + flnm=dataset + ".h5", + config=config, + seed="0000", + callbacks=stopper_callback, + ) + except torch.cuda.OutOfMemoryError: + error_type = "F_OOM" + + # Managing failures to learn memory constraints + if error_type is not None: + metadata = { + "num_parameters": None, + "num_parameters_train": None, + "val_loss": None, + "test_rmse": None, + "budget": None, + "stopped": None, + "lc_train_loss": None, + "lc_val_loss": None, + "flops": None, + "duration_batch_inference": None, + } + + if DEEPHYPER_BENCHMARK_MOO: + objective = [error_type for _ in range(3)] + else: + objective = error_type + + return {"objective": objective, "metadata": metadata} + + param_count = count_params(model) + flops = FlopCountAnalysis(model, inputs=(torch.randn(1, 3))).total() + + train_ls = np.array(losshistory.loss_train).sum(axis=1) + val_ls = np.array(losshistory.loss_test).sum(axis=1) + steps = np.array(losshistory.steps) + lc_train_X = np.stack([steps, train_ls], axis=1) + lc_val_X = np.stack([steps, val_ls], axis=1) + lc_train_X_json = array_to_json(lc_train_X) + lc_val_X_json = array_to_json(lc_val_X) + + if DEEPHYPER_BENCHMARK_MOO: + print("Optimizing multiple objectives...") + + if np.isnan(val_loss[:2]).any() or np.isinf(val_loss[:2]).any(): + objective_0 = "F" + else: + objective_0 = -(val_loss[:2] / config["loss_weights"][:2]).sum() + + if np.isnan(val_loss[2:]).any() or np.isinf(val_loss[2:]).any(): + objective_1 = "F" + else: + objective_1 = -(val_loss[2:] / config["loss_weights"][2:]).sum() + + objective = [ + objective_0, + objective_1, + -flops, + ] + else: + objective = ( + "F" + if np.isnan(val_loss).any() or np.isinf(val_loss).any() + else -(val_loss / config["loss_weights"]).sum() + ) + metadata = { + "num_parameters": param_count["num_parameters"], + "num_parameters_train": param_count["num_parameters_train"], + "val_loss": array_to_json(val_loss), # array of 4 elements + "test_rmse": float(test_loss[0]), + "budget": stopper_callback.budget, + "stopped": stopper_callback.stopped, + "lc_train_loss": lc_train_X_json, + "lc_val_loss": lc_val_X_json, + "flops": flops, + "duration_batch_inference": duration_batch_inference, # add the inference time in seconds + } + + return {"objective": objective, "metadata": metadata} + + +def evaluate(config): + """ + Evaluate an hyperparameter configuration + on training/validation and testing data. + """ + callbacks = EarlyStopping(patience=100_000) + DIR = os.path.dirname(os.path.abspath(__file__)) + dataset = os.environ.get("DEEPHYPER_BENCHMARK_DATASET") + + val_loss, test_loss, losshistory, model = run_training( + net_class=FNN, + scenario="diff-react", + epochs=config["epochs"], + learning_rate=config["lr"], + model_update=1, + root_path=os.path.join(DIR, "../build/PDEBench-DH/pdebench/data/" + dataset), + flnm="2D_diff-react_NA_NA.h5", + config=config, + seed="0000", + callbacks=callbacks, + ) + return val_loss, test_loss, losshistory diff --git a/src/deephyper_benchmark/lib/pinnbench/DiffusionReaction/model.py b/src/deephyper_benchmark/lib/pinnbench/DiffusionReaction/model.py new file mode 100644 index 0000000..ff9e3d0 --- /dev/null +++ b/src/deephyper_benchmark/lib/pinnbench/DiffusionReaction/model.py @@ -0,0 +1,154 @@ +import torch +import torch.nn as nn + +from deepxde.nn import NN +from deepxde.nn import initializers +from deepxde import config + +INITIALIZERS = initializers + +class Sin(nn.Module): + def __init__(self): + super(Sin, self).__init__() + + def forward(self, x): + return torch.sin(x) + +ACTIVATIONS = { + # "id": nn.Identity, + "elu": nn.ELU, + "relu": nn.ReLU, + "selu": nn.SELU, + "sigmoid": nn.Sigmoid, + "silu": nn.SiLU, # same as swish + "sin": Sin, + "tanh": nn.Tanh, + "hardswish": nn.Hardswish, + "leakyrelu": nn.LeakyReLU, + "mish": nn.Mish, + "softplus": nn.Softplus, +} + + +class FNN(NN): + """Fully-connected neural network.""" + + def __init__( + self, + input_dim: int, + output_dim: int, + num_layers: int = 5, + num_neurons: int = 30, + activation: str = "elu", + kernel_initializer: str = "Glorot normal", + batch_norm: bool = False, + skip_co: bool = False, + dropout_rate: float = 0.0, + regularization: str = None, + weight_decay: float = 0.01, + **kwargs, + ): + super(FNN, self).__init__() + + if regularization is None: + self.regularizer = None + else: + self.regularizer = [regularization, weight_decay] + + layer_sizes = [input_dim] + [num_neurons for _ in range(num_layers)] + + initializer = INITIALIZERS.get(kernel_initializer) + initializer_zero = INITIALIZERS.get("zeros") + + self.linears = nn.Sequential() + for i in range(1, len(layer_sizes)): + if skip_co: + self.linears.append( + SkipConnection( + in_dim=layer_sizes[i - 1], + out_dim=layer_sizes[i], + batch_norm=batch_norm, + kernel_initializer=kernel_initializer, + activation=activation, + ) + ) + + else: + linear_module = nn.Linear( + layer_sizes[i - 1], layer_sizes[i], dtype=config.real(torch) + ) + initializer(linear_module.weight) + initializer_zero(linear_module.bias) + self.linears.append(linear_module) + if batch_norm: + self.linears.append( + nn.BatchNorm1d(layer_sizes[i], dtype=config.real(torch)) + ) + + if activation != "id": + self.linears.append(ACTIVATIONS.get(activation)()) + + self.linears.append(nn.Dropout(p=dropout_rate)) + + self.linears.append( + nn.Linear(layer_sizes[-1], output_dim, dtype=config.real(torch)) + ) + + def forward(self, inputs): + x = inputs + if self._input_transform is not None: + x = self._input_transform(x) + x = self.linears(x) + if self._output_transform is not None: + x = self._output_transform(inputs, x) + return x + + +class SkipConnection(nn.Module): + def __init__( + self, + in_dim, + out_dim, + kernel_initializer="Glorot normal", + batch_norm=False, + activation="elu", + ) -> None: + super(SkipConnection, self).__init__() + + self.in_dim = in_dim + self.out_dim = out_dim + + initializer = INITIALIZERS.get(kernel_initializer) + initializer_zero = INITIALIZERS.get("zeros") + + if self.in_dim != self.out_dim: + self.map = nn.Linear(in_dim, out_dim) + initializer(self.map.weight) + initializer_zero(self.map.bias) + + self.block = nn.Sequential() + + linear_module = nn.Linear(in_dim, out_dim, dtype=config.real(torch)) + initializer(linear_module.weight) + initializer_zero(linear_module.bias) + self.block.append(linear_module) + + if batch_norm: + self.block.append(nn.BatchNorm1d(out_dim, dtype=config.real(torch))) + + if activation != "id": + self.block.append(ACTIVATIONS.get(activation)()) + + if activation != "id": + self.act = ACTIVATIONS.get(activation)() + else: + self.act = None + + def forward(self, x): + residual = x + if self.in_dim != self.out_dim: + residual = self.map(residual) + out = self.block(x) + residual + if self.act is not None: + out = self.act(out) + return out diff --git a/src/deephyper_benchmark/lib/pinnbench/DiffusionReaction/requirements.txt b/src/deephyper_benchmark/lib/pinnbench/DiffusionReaction/requirements.txt new file mode 100644 index 0000000..3ad0199 --- /dev/null +++ b/src/deephyper_benchmark/lib/pinnbench/DiffusionReaction/requirements.txt @@ -0,0 +1,6 @@ +DeepXDE>=1.8.2 +numpy>=1.24.2 +omegaconf>=2.3.0 +packaging>=23.0 +torch>=2.0.0 +fvcore>=0.1.5 \ No newline at end of file diff --git a/src/deephyper_benchmark/lib/pinnbench/README.md b/src/deephyper_benchmark/lib/pinnbench/README.md new file mode 100644 index 0000000..520a485 --- /dev/null +++ b/src/deephyper_benchmark/lib/pinnbench/README.md @@ -0,0 +1,144 @@ +# Physics-informed Neural Networks Benchmark + +> **Warning** +> Work in progress, this benchmark is not yet ready. + +Physics-Informed Neural Networks (PINNs) are a class of machine learning models that combine the strengths of neural networks and physics-based modeling. PINNs are used to solve partial differential equations (PDEs) and other physical problems by learning a solution directly from data. + +The basic idea behind PINNs is to use a neural network to approximate the solution to a PDE, while also enforcing the underlying physical laws that govern the problem. This is achieved by incorporating the PDE as a constraint in the neural network training process. More details can be found in the [original work](https://arxiv.org/abs/1711.10561). + +This set of benchmarks seeks to incorporate AutoML workflow into the development of PINNs with DeepHyper. The PINN benchmark problems support Hyperparameter Optimization (HPO), Neural Architecture Search (NAS), and Multi-fidelity evaluations. + +The currently available problems are + + + +- [Diffusion-reaction Equation](#diffusion-reaction-equation) (Dataset Size: 13 GB) + + + + + + + + + + +## Diffusion-reaction Equation + +This benchmark is based on **modified** [`PDEBench`](https://github.com/pdebench/PDEBench) and [`DeepXDE`](https://github.com/lululxvi/deepxde). + +### Installation + +To install the **modified** `PDEBench` and this benchmark, run: +``` +python -c "import deephyper_benchmark as dhb; dhb.install('PINNBench/DiffusionReaction');" +``` + +Run the hyperparameter search +``` +import os +os.environ['DEEPHYPER_BENCHMARK_MOO'] = '1' # enable multi-objective optimization + +import deephyper_benchmark as dhb +diff_react = dhb.load("PINNBench/DiffusionReaction") + +from deephyper.evaluator import RunningJob +config = diff_react.hpo.problem.default_configuration # get a default config to test +res = diff_react.hpo.run(RunningJob(parameters=config)) +``` + + +### Configuration + +It is necessary to configure `DeepXDE` to use `PyTorch` backend. The instructions can be found [here](https://deepxde.readthedocs.io/en/latest/user/installation.html#working-with-different-backends). + +### Metadata + +- [x] `num_parameters`: integer value of the number of parameters in the neural network. +- [x] `num_parameters_train`: integer value of the number of **trainable** parameters of the neural network. +- [x] `budget`: scalar value (float/int) of the budget consumed by the neural network. Therefore the budget should be defined for each benchmark (e.g., number of epochs in general). +- [x] `stopped`: boolean value indicating if the evaluation was stopped before consuming the maximum budget. +- [x] `train_loss`: scalar value of the training metrics (replace `X` by the metric name, 1 key per metric). +- [x] `valid_loss`: scalar value of the validation metrics (replace `X` by the metric name, 1 key per metric). +- [x] `test_rmse`: scalar value of the testing metrics (replace `X` by the metric name, 1 key per metric). +- [x] `flops`: number of flops of the model such as computed in `fvcore.nn.FlopCountAnalysis(...).total()` (See [documentation](https://detectron2.readthedocs.io/en/latest/modules/fvcore.html#module-fvcore.nn)). +- [ ] `latency`: TO BE CLARIFIED +- [x] `lc_train_loss`: recorded learning curves of the trained model, the `bi` variables are the budget value (e.g., epochs/batches), and the `yi` values are the recorded metric. `X` in `train_X` is replaced by the name of the metric such as `train_loss` or `train_accuracy`. The format is `[[b0, y0], [b1, y1], ...]`. +- [x] `lc_valid_loss`: Same as `lc_train_X` but for validation data. +- [x] `duration_batch_inference`: average inference time for a single data point. + +### Other details + +#### Supported datasets + +The currently available dataset is `2D_diff-react_NA_NA`. The rest datasets from PDEBench (see [list](https://github.com/iamyixuan/PDEBench-DH/tree/main/pdebench/data_download) )will be supported in the future. + +#### Supported hyperparameters + +- [x] `num_layers`: number of layers (or other building blocks) in the network. +- [x] `lr`: learning rate for the optimizer. +- [x] `num_neurons`: number of neurons per layer. +- [x] `epochs`: number of maximum epochs for training. +- [x] `activation`: activation functions. +- [x] `skip_co`: if using skip connection (residual block). +- [x] `dropout_rate`: dropout rate. +- [x] `optimizer`: choices of the optimizer. +- [x] `weight_decay`: magnitude of L2 regularization. +- [x] `initialization`: initialization strategy for network weights. +- [x] `loss_weights`: weights assigned to the PDE loss. + +#### Multi-objective Optimization (MOO) + +MOO is supported for the `DiffusionReaction` benchmark. To enable MOO, set environment variable `DEEPHYPER_BENCHMARK_MOO=1`. There are five minimized objectives in this case: + +- `objective_0`: Validation PDE loss. +- `objective_1`: Validation boundary and initial condition solution loss. +- `objective_2`: Batch inference duration (in seconds). +- `objective_3`: FLOPS from [FVCORE Package](https://github.com/facebookresearch/fvcore/blob/main/docs/flop_count.md). diff --git a/src/deephyper_benchmark/lib/pinnbench/install.sh b/src/deephyper_benchmark/lib/pinnbench/install.sh new file mode 100644 index 0000000..d28fea3 --- /dev/null +++ b/src/deephyper_benchmark/lib/pinnbench/install.sh @@ -0,0 +1,13 @@ +#!/bin/bash + + +mkdir build/ && cd build/ +# install modified PDEBench +git clone https://github.com/iamyixuan/PDEBench-DH.git +cd PDEBench-DH/ +python -m pip install -e . + +# generate data +mkdir ./pdebench/data/ +cd ./pdebench/data_gen/ +python gen_diff_react.py diff --git a/src/deephyper_benchmark/lib/yahpo/README.md b/src/deephyper_benchmark/lib/yahpo/README.md new file mode 100644 index 0000000..6fbb7c4 --- /dev/null +++ b/src/deephyper_benchmark/lib/yahpo/README.md @@ -0,0 +1,13 @@ +# YAHPO Gym - Surrogate Benchmark for Hyperparameter Optimization + +> **Warning** +> Work in progress, this benchmark is not yet ready. + +* [YAHPO Gym - Github](https://github.com/slds-lmu/yahpo_gym) + + +## Installation + +```console +python -c "import deephyper_benchmark as dhb; dhb.install('YAHPO/lcbench');" +``` diff --git a/src/deephyper_benchmark/lib/yahpo/lcbench/__init__.py b/src/deephyper_benchmark/lib/yahpo/lcbench/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/deephyper_benchmark/lib/yahpo/lcbench/benchmark.py b/src/deephyper_benchmark/lib/yahpo/lcbench/benchmark.py new file mode 100644 index 0000000..18ec8d7 --- /dev/null +++ b/src/deephyper_benchmark/lib/yahpo/lcbench/benchmark.py @@ -0,0 +1,27 @@ +import os + +from deephyper_benchmark import * + +DIR = os.path.dirname(os.path.abspath(__file__)) + + +class YAHPOLCBench(Benchmark): + version = "0.0.1" + + data_dir = os.path.join(DIR, "..", "build", "data") + requires = { + "download-data": { + "step": "install", + "type": "cmd", + "cmd": f"git clone https://github.com/slds-lmu/yahpo_data.git {data_dir}", + }, + "pip-yahpo-gym": {"step": "install", "type": "pip", "args": "install yahpo-gym"}, + } + + def install(self): + super().install() + + from yahpo_gym import local_config + + local_config.init_config() + local_config.set_data_path(self.data_dir) diff --git a/src/deephyper_benchmark/lib/yahpo/lcbench/hpo.py b/src/deephyper_benchmark/lib/yahpo/lcbench/hpo.py new file mode 100644 index 0000000..539ba7a --- /dev/null +++ b/src/deephyper_benchmark/lib/yahpo/lcbench/hpo.py @@ -0,0 +1,75 @@ +import os + +from ConfigSpace import ConfigurationSpace +from deephyper.evaluator import profile, RunningJob +from deephyper.hpo import HpProblem + +from yahpo_gym import benchmark_set +import yahpo_gym.benchmarks.lcbench + +DEEPHYPER_BENCHMARK_INSTANCE = os.environ.get("DEEPHYPER_BENCHMARK_INSTANCE", "3945") + + +bench = benchmark_set.BenchmarkSet("lcbench") +bench.set_instance(DEEPHYPER_BENCHMARK_INSTANCE) + +config_space = ConfigurationSpace() +config_space.add_hyperparameters( + [ + hp + for hp in bench.config_space.get_hyperparameters() + if not (hp.name in ["OpenML_task_id", "epoch"]) + ] +) +problem = HpProblem(config_space=config_space) + + +@profile() +def run(job: RunningJob) -> dict: + config = job.parameters.copy() + config["OpenML_task_id"] = DEEPHYPER_BENCHMARK_INSTANCE + + # Min/Max budget (here epochs) + min_b, max_b = 2, 51 + + # Run the benchmark (batch for better performance) + def update_config(config, budget): + config = config.copy() + config["epoch"] = budget + return config + + outputs = [ + bench.objective_function(update_config(config, budget=b)) + for b in range(min_b, max_b + 1) + ] + + for i, out_i in enumerate(outputs): + out_i = out_i[0] + budget_i = i + 1 + objective_i = -out_i["val_cross_entropy"] + + job.record(budget_i, objective_i) + if job.stopped(): + break + + return { + "objective": objective_i, + "metadata": { + "budget": budget_i, + "stopped": budget_i < len(outputs), + "test_balanced_accuracy": outputs[-1][0]["test_balanced_accuracy"], + "val_balanced_accuracy": outputs[-1][0]["val_balanced_accuracy"], + "test_cross_entropy": outputs[-1][0]["test_cross_entropy"], + "val_cross_entropy": outputs[-1][0]["val_cross_entropy"], + "time": outputs[-1][0]["time"], + }, + } + + +if __name__ == "__main__": + print(problem) + config = bench.config_space.sample_configuration(1).get_dictionary() + config.pop("OpenML_task_id") + print(config) + result = run(RunningJob(id=0, parameters=config)) + print(result) diff --git a/src/deephyper_benchmark/lib/yahpo/lcbench/metrics.py b/src/deephyper_benchmark/lib/yahpo/lcbench/metrics.py new file mode 100644 index 0000000..92d0359 --- /dev/null +++ b/src/deephyper_benchmark/lib/yahpo/lcbench/metrics.py @@ -0,0 +1,69 @@ +import json +import os + +import numpy as np + +from .hpo import bench as BENCHMARK + + +class PerformanceEvaluator: + """A class defining performance evaluators for the YAHPO/lcbench problems.""" + + def __init__(self): + """Read the current problem defn from environment vars.""" + + df = BENCHMARK.target_stats + + self.x_min = None + + self.y_metric = "cross_entropy" + + cond = (df["metric"] == "val_cross_entropy") & (df["statistic"] == "min") + self.y_min_valid = df[cond].iloc[0]["value"] + + cond = (df["metric"] == "test_cross_entropy") & (df["statistic"] == "min") + self.y_min_test = df[cond].iloc[0]["value"] + + def simple_regret_valid(self, y_valid: np.ndarray) -> np.ndarray: + """Compute the regret of the objective (validation RMSE) of a list of ordered solutions. + + Args: + y_valid (np.ndarray): An array of solutions. + + Returns: + np.ndarray: An array of regret values. + """ + return y_valid - self.y_min_valid + + def cumul_regret_valid(self, y_valid: np.ndarray) -> np.ndarray: + """Compute the cumulative regret of the objective (validation RMSE) aon n array of ordered solutions. + + Args: + y_valid (np.ndarray): An array of solutions. + + Returns: + np.ndarray: An array of cumulative regret values. + """ + return np.cumsum(self.simple_regret_valid(y_valid)) + + def simple_regret_test(self, y_test: np.ndarray) -> np.ndarray: + """Compute the regret of the test RMSE of a list of ordered solutions. + + Args: + y_test (np.ndarray): An array of solutions. + + Returns: + np.ndarray: An array of regret values. + """ + return y_test - self.y_min_test + + def cumul_regret_test(self, y_test: np.ndarray) -> np.ndarray: + """Compute the cumulative regret of the test RMSE aon n array of ordered solutions. + + Args: + y_test (np.ndarray): An array of solutions. + + Returns: + np.ndarray: An array of cumulative regret values. + """ + return np.cumsum(self.simple_regret_test(y_test)) diff --git a/tests/test_c_bbo.py b/tests/test_c_bbo.py deleted file mode 100644 index 543f8f8..0000000 --- a/tests/test_c_bbo.py +++ /dev/null @@ -1,12 +0,0 @@ -import deephyper_benchmark.lib.c_bbo.ackley as bench - - -def test_benchmark(): - - bench.hpo.problem - bench.hpo.run_function - bench.hpo.scorer - - -if __name__ == "__main__": - test_benchmark() diff --git a/tests/test_cbbo.py b/tests/test_cbbo.py new file mode 100644 index 0000000..7be8073 --- /dev/null +++ b/tests/test_cbbo.py @@ -0,0 +1,24 @@ +"""here.""" + +from deephyper.hpo import RandomSearch +import deephyper_benchmark.benchmarks.cbbo as bench + + +def test_cbbo_benchmarks(tmp_path="."): + """Test cbbo benchmarks.""" + + benchmarks = [ + bench.AckleyBenchmark(), + bench.BraninBenchmark(), + bench.EasomBenchmark(), + bench.GriewankBenchmark(), + bench.Hartmann6DBenchmark(), + ] + + max_evals = 25 + for b in benchmarks: + search = RandomSearch(b.problem, b.run_function, log_dir=tmp_path) + results = search.search(max_evals) + assert len(results) == max_evals + cumul_regret = b.scorer.cumul_regret(results.objective) + assert len(cumul_regret) == max_evals diff --git a/tests/test_dtlz.py b/tests/test_dtlz.py new file mode 100644 index 0000000..dece560 --- /dev/null +++ b/tests/test_dtlz.py @@ -0,0 +1,21 @@ +"""here.""" + +from deephyper.hpo import RandomSearch +import deephyper_benchmark.benchmarks.dtlz as bench + + +def test_dtlz_benchmarks(tmp_path="."): + """Test cbbo benchmarks.""" + + max_evals = 25 + nobj = 2 + + for prob_id in range(1, 8): + b = bench.DTLZBenchmark(prob_id=prob_id, nobj=nobj) + search = RandomSearch(b.problem, b.run_function, log_dir=tmp_path) + results = search.search(max_evals) + assert len(results) == max_evals + cols = [f"objective_{i}" for i in range(nobj)] + assert all(c in results.columns for c in cols) + hvi = b.scorer.hypervolume(results[cols].values) + assert len(hvi) == max_evals diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..8976905 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1067 @@ +version = 1 +revision = 1 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", + "python_full_version < '3.11'", +] + +[[package]] +name = "about-time" +version = "4.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3f/ccb16bdc53ebb81c1bf837c1ee4b5b0b69584fd2e4a802a2a79936691c0a/about-time-4.2.1.tar.gz", hash = "sha256:6a538862d33ce67d997429d14998310e1dbfda6cb7d9bbfbf799c4709847fece", size = 15380 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/cd/7ee00d6aa023b1d0551da0da5fee3bc23c3eeea632fbfc5126d1fec52b7e/about_time-4.2.1-py3-none-any.whl", hash = "sha256:8bbf4c75fe13cbd3d72f49a03b02c5c7dca32169b6d49117c257e7eb3eaee341", size = 13295 }, +] + +[[package]] +name = "absl-py" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/8f/fc001b92ecc467cc32ab38398bd0bfb45df46e7523bf33c2ad22a505f06e/absl-py-2.1.0.tar.gz", hash = "sha256:7820790efbb316739cde8b4e19357243fc3608a152024288513dd968d7d959ff", size = 118055 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/ad/e0d3c824784ff121c03cc031f944bc7e139a8f1870ffd2845cc2dd76f6c4/absl_py-2.1.0-py3-none-any.whl", hash = "sha256:526a04eadab8b4ee719ce68f204172ead1027549089702d99b9059f129ff1308", size = 133706 }, +] + +[[package]] +name = "alive-progress" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "about-time" }, + { name = "grapheme" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/66/c2c1e6674b3b7202ce529cf7d9971c93031e843b8e0c86a85f693e6185b8/alive-progress-3.2.0.tar.gz", hash = "sha256:ede29d046ff454fe56b941f686f89dd9389430c4a5b7658e445cb0b80e0e4deb", size = 113231 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/39/cade3a5a97fffa3ae84f298208237b3a9f7112d6b0ed57e8ff4b755e44b4/alive_progress-3.2.0-py3-none-any.whl", hash = "sha256:0677929f8d3202572e9d142f08170b34dbbe256cc6d2afbf75ef187c7da964a8", size = 77106 }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, +] + +[[package]] +name = "autograd" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/ed/67975d75c0fe71220c8df2370c6c1390805790a641359b502f39c042c0c1/autograd-1.7.0.tar.gz", hash = "sha256:de743fd368d6df523cd37305dcd171861a9752a144493677d2c9f5a56983ff2f", size = 2564855 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/90/d13cf396989052cadd8511c1878b0913bbce28eeef5feb95710a92e03076/autograd-1.7.0-py3-none-any.whl", hash = "sha256:49680300f842f3a8722b060ac0d3ed7aca071d1ad4d3d38c9fdadafdcc73c30b", size = 52522 }, +] + +[[package]] +name = "cloudpickle" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/39/069100b84d7418bc358d81669d5748efb14b9cceacd2f9c75f550424132f/cloudpickle-3.1.1.tar.gz", hash = "sha256:b216fa8ae4019d5482a8ac3c95d8f6346115d8835911fd4aefd1a445e4242c64", size = 22113 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/e8/64c37fadfc2816a7701fa8a6ed8d87327c7d54eacfbfb6edab14a2f2be75/cloudpickle-3.1.1-py3-none-any.whl", hash = "sha256:c8c5a44295039331ee9dad40ba100a9c7297b6f988e50e87ccdf3765a668350e", size = 20992 }, +] + +[[package]] +name = "cma" +version = "3.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/3c/85f3a10db637d2c8776a4154d593a7fd820be8d900fde5514df45ebac466/cma-3.2.2.tar.gz", hash = "sha256:47ac71ddd2b9a922daa601516064a585a1a3dbbae7802bca935db25e525547eb", size = 240341 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/d9/e607fd2257cc18d64ed4634d3fc437fda81cbd6d728f2d7aa5aad3a95447/cma-3.2.2-py2.py3-none-any.whl", hash = "sha256:0e3994b1f93c065b3eb2e68dbc589d9e694e191698d0aa58cb70cbdb803ade4e", size = 249112 }, +] + +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "configspace" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, + { name = "numpy" }, + { name = "pyparsing" }, + { name = "scipy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/bc/2a53b259ec14101a42690992ea8fc2456edcacc73ede62f803dd3c4b0bdc/configspace-1.2.1.tar.gz", hash = "sha256:2fc1c4477f7839d38b53d8813c2581967df57ccd7d8b4b506e879a8835cb964c", size = 130983 } + +[[package]] +name = "contourpy" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/c2/fc7193cc5383637ff390a712e88e4ded0452c9fbcf84abe3de5ea3df1866/contourpy-1.3.1.tar.gz", hash = "sha256:dfd97abd83335045a913e3bcc4a09c0ceadbe66580cf573fe961f4a825efa699", size = 13465753 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/80937fe3efe0edacf67c9a20b955139a1a622730042c1ea991956f2704ad/contourpy-1.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a045f341a77b77e1c5de31e74e966537bba9f3c4099b35bf4c2e3939dd54cdab", size = 268466 }, + { url = "https://files.pythonhosted.org/packages/82/1d/e3eaebb4aa2d7311528c048350ca8e99cdacfafd99da87bc0a5f8d81f2c2/contourpy-1.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:500360b77259914f7805af7462e41f9cb7ca92ad38e9f94d6c8641b089338124", size = 253314 }, + { url = "https://files.pythonhosted.org/packages/de/f3/d796b22d1a2b587acc8100ba8c07fb7b5e17fde265a7bb05ab967f4c935a/contourpy-1.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2f926efda994cdf3c8d3fdb40b9962f86edbc4457e739277b961eced3d0b4c1", size = 312003 }, + { url = "https://files.pythonhosted.org/packages/bf/f5/0e67902bc4394daee8daa39c81d4f00b50e063ee1a46cb3938cc65585d36/contourpy-1.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adce39d67c0edf383647a3a007de0a45fd1b08dedaa5318404f1a73059c2512b", size = 351896 }, + { url = "https://files.pythonhosted.org/packages/1f/d6/e766395723f6256d45d6e67c13bb638dd1fa9dc10ef912dc7dd3dcfc19de/contourpy-1.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abbb49fb7dac584e5abc6636b7b2a7227111c4f771005853e7d25176daaf8453", size = 320814 }, + { url = "https://files.pythonhosted.org/packages/a9/57/86c500d63b3e26e5b73a28b8291a67c5608d4aa87ebd17bd15bb33c178bc/contourpy-1.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0cffcbede75c059f535725c1680dfb17b6ba8753f0c74b14e6a9c68c29d7ea3", size = 324969 }, + { url = "https://files.pythonhosted.org/packages/b8/62/bb146d1289d6b3450bccc4642e7f4413b92ebffd9bf2e91b0404323704a7/contourpy-1.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab29962927945d89d9b293eabd0d59aea28d887d4f3be6c22deaefbb938a7277", size = 1265162 }, + { url = "https://files.pythonhosted.org/packages/18/04/9f7d132ce49a212c8e767042cc80ae390f728060d2eea47058f55b9eff1c/contourpy-1.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:974d8145f8ca354498005b5b981165b74a195abfae9a8129df3e56771961d595", size = 1324328 }, + { url = "https://files.pythonhosted.org/packages/46/23/196813901be3f97c83ababdab1382e13e0edc0bb4e7b49a7bff15fcf754e/contourpy-1.3.1-cp310-cp310-win32.whl", hash = "sha256:ac4578ac281983f63b400f7fe6c101bedc10651650eef012be1ccffcbacf3697", size = 173861 }, + { url = "https://files.pythonhosted.org/packages/e0/82/c372be3fc000a3b2005061ca623a0d1ecd2eaafb10d9e883a2fc8566e951/contourpy-1.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:174e758c66bbc1c8576992cec9599ce8b6672b741b5d336b5c74e35ac382b18e", size = 218566 }, + { url = "https://files.pythonhosted.org/packages/12/bb/11250d2906ee2e8b466b5f93e6b19d525f3e0254ac8b445b56e618527718/contourpy-1.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3e8b974d8db2c5610fb4e76307e265de0edb655ae8169e8b21f41807ccbeec4b", size = 269555 }, + { url = "https://files.pythonhosted.org/packages/67/71/1e6e95aee21a500415f5d2dbf037bf4567529b6a4e986594d7026ec5ae90/contourpy-1.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:20914c8c973f41456337652a6eeca26d2148aa96dd7ac323b74516988bea89fc", size = 254549 }, + { url = "https://files.pythonhosted.org/packages/31/2c/b88986e8d79ac45efe9d8801ae341525f38e087449b6c2f2e6050468a42c/contourpy-1.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19d40d37c1c3a4961b4619dd9d77b12124a453cc3d02bb31a07d58ef684d3d86", size = 313000 }, + { url = "https://files.pythonhosted.org/packages/c4/18/65280989b151fcf33a8352f992eff71e61b968bef7432fbfde3a364f0730/contourpy-1.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:113231fe3825ebf6f15eaa8bc1f5b0ddc19d42b733345eae0934cb291beb88b6", size = 352925 }, + { url = "https://files.pythonhosted.org/packages/f5/c7/5fd0146c93220dbfe1a2e0f98969293b86ca9bc041d6c90c0e065f4619ad/contourpy-1.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4dbbc03a40f916a8420e420d63e96a1258d3d1b58cbdfd8d1f07b49fcbd38e85", size = 323693 }, + { url = "https://files.pythonhosted.org/packages/85/fc/7fa5d17daf77306840a4e84668a48ddff09e6bc09ba4e37e85ffc8e4faa3/contourpy-1.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a04ecd68acbd77fa2d39723ceca4c3197cb2969633836ced1bea14e219d077c", size = 326184 }, + { url = "https://files.pythonhosted.org/packages/ef/e7/104065c8270c7397c9571620d3ab880558957216f2b5ebb7e040f85eeb22/contourpy-1.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c414fc1ed8ee1dbd5da626cf3710c6013d3d27456651d156711fa24f24bd1291", size = 1268031 }, + { url = "https://files.pythonhosted.org/packages/e2/4a/c788d0bdbf32c8113c2354493ed291f924d4793c4a2e85b69e737a21a658/contourpy-1.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:31c1b55c1f34f80557d3830d3dd93ba722ce7e33a0b472cba0ec3b6535684d8f", size = 1325995 }, + { url = "https://files.pythonhosted.org/packages/a6/e6/a2f351a90d955f8b0564caf1ebe4b1451a3f01f83e5e3a414055a5b8bccb/contourpy-1.3.1-cp311-cp311-win32.whl", hash = "sha256:f611e628ef06670df83fce17805c344710ca5cde01edfdc72751311da8585375", size = 174396 }, + { url = "https://files.pythonhosted.org/packages/a8/7e/cd93cab453720a5d6cb75588cc17dcdc08fc3484b9de98b885924ff61900/contourpy-1.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:b2bdca22a27e35f16794cf585832e542123296b4687f9fd96822db6bae17bfc9", size = 219787 }, + { url = "https://files.pythonhosted.org/packages/37/6b/175f60227d3e7f5f1549fcb374592be311293132207e451c3d7c654c25fb/contourpy-1.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ffa84be8e0bd33410b17189f7164c3589c229ce5db85798076a3fa136d0e509", size = 271494 }, + { url = "https://files.pythonhosted.org/packages/6b/6a/7833cfae2c1e63d1d8875a50fd23371394f540ce809d7383550681a1fa64/contourpy-1.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805617228ba7e2cbbfb6c503858e626ab528ac2a32a04a2fe88ffaf6b02c32bc", size = 255444 }, + { url = "https://files.pythonhosted.org/packages/7f/b3/7859efce66eaca5c14ba7619791b084ed02d868d76b928ff56890d2d059d/contourpy-1.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade08d343436a94e633db932e7e8407fe7de8083967962b46bdfc1b0ced39454", size = 307628 }, + { url = "https://files.pythonhosted.org/packages/48/b2/011415f5e3f0a50b1e285a0bf78eb5d92a4df000553570f0851b6e309076/contourpy-1.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47734d7073fb4590b4a40122b35917cd77be5722d80683b249dac1de266aac80", size = 347271 }, + { url = "https://files.pythonhosted.org/packages/84/7d/ef19b1db0f45b151ac78c65127235239a8cf21a59d1ce8507ce03e89a30b/contourpy-1.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ba94a401342fc0f8b948e57d977557fbf4d515f03c67682dd5c6191cb2d16ec", size = 318906 }, + { url = "https://files.pythonhosted.org/packages/ba/99/6794142b90b853a9155316c8f470d2e4821fe6f086b03e372aca848227dd/contourpy-1.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efa874e87e4a647fd2e4f514d5e91c7d493697127beb95e77d2f7561f6905bd9", size = 323622 }, + { url = "https://files.pythonhosted.org/packages/3c/0f/37d2c84a900cd8eb54e105f4fa9aebd275e14e266736778bb5dccbf3bbbb/contourpy-1.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1bf98051f1045b15c87868dbaea84f92408337d4f81d0e449ee41920ea121d3b", size = 1266699 }, + { url = "https://files.pythonhosted.org/packages/3a/8a/deb5e11dc7d9cc8f0f9c8b29d4f062203f3af230ba83c30a6b161a6effc9/contourpy-1.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:61332c87493b00091423e747ea78200659dc09bdf7fd69edd5e98cef5d3e9a8d", size = 1326395 }, + { url = "https://files.pythonhosted.org/packages/1a/35/7e267ae7c13aaf12322ccc493531f1e7f2eb8fba2927b9d7a05ff615df7a/contourpy-1.3.1-cp312-cp312-win32.whl", hash = "sha256:e914a8cb05ce5c809dd0fe350cfbb4e881bde5e2a38dc04e3afe1b3e58bd158e", size = 175354 }, + { url = "https://files.pythonhosted.org/packages/a1/35/c2de8823211d07e8a79ab018ef03960716c5dff6f4d5bff5af87fd682992/contourpy-1.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:08d9d449a61cf53033612cb368f3a1b26cd7835d9b8cd326647efe43bca7568d", size = 220971 }, + { url = "https://files.pythonhosted.org/packages/9a/e7/de62050dce687c5e96f946a93546910bc67e483fe05324439e329ff36105/contourpy-1.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a761d9ccfc5e2ecd1bf05534eda382aa14c3e4f9205ba5b1684ecfe400716ef2", size = 271548 }, + { url = "https://files.pythonhosted.org/packages/78/4d/c2a09ae014ae984c6bdd29c11e74d3121b25eaa117eca0bb76340efd7e1c/contourpy-1.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:523a8ee12edfa36f6d2a49407f705a6ef4c5098de4f498619787e272de93f2d5", size = 255576 }, + { url = "https://files.pythonhosted.org/packages/ab/8a/915380ee96a5638bda80cd061ccb8e666bfdccea38d5741cb69e6dbd61fc/contourpy-1.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece6df05e2c41bd46776fbc712e0996f7c94e0d0543af1656956d150c4ca7c81", size = 306635 }, + { url = "https://files.pythonhosted.org/packages/29/5c/c83ce09375428298acd4e6582aeb68b1e0d1447f877fa993d9bf6cd3b0a0/contourpy-1.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:573abb30e0e05bf31ed067d2f82500ecfdaec15627a59d63ea2d95714790f5c2", size = 345925 }, + { url = "https://files.pythonhosted.org/packages/29/63/5b52f4a15e80c66c8078a641a3bfacd6e07106835682454647aca1afc852/contourpy-1.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fa36448e6a3a1a9a2ba23c02012c43ed88905ec80163f2ffe2421c7192a5d7", size = 318000 }, + { url = "https://files.pythonhosted.org/packages/9a/e2/30ca086c692691129849198659bf0556d72a757fe2769eb9620a27169296/contourpy-1.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ea9924d28fc5586bf0b42d15f590b10c224117e74409dd7a0be3b62b74a501c", size = 322689 }, + { url = "https://files.pythonhosted.org/packages/6b/77/f37812ef700f1f185d348394debf33f22d531e714cf6a35d13d68a7003c7/contourpy-1.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b75aa69cb4d6f137b36f7eb2ace9280cfb60c55dc5f61c731fdf6f037f958a3", size = 1268413 }, + { url = "https://files.pythonhosted.org/packages/3f/6d/ce84e79cdd128542ebeb268f84abb4b093af78e7f8ec504676673d2675bc/contourpy-1.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:041b640d4ec01922083645a94bb3b2e777e6b626788f4095cf21abbe266413c1", size = 1326530 }, + { url = "https://files.pythonhosted.org/packages/72/22/8282f4eae20c73c89bee7a82a19c4e27af9b57bb602ecaa00713d5bdb54d/contourpy-1.3.1-cp313-cp313-win32.whl", hash = "sha256:36987a15e8ace5f58d4d5da9dca82d498c2bbb28dff6e5d04fbfcc35a9cb3a82", size = 175315 }, + { url = "https://files.pythonhosted.org/packages/e3/d5/28bca491f65312b438fbf076589dcde7f6f966b196d900777f5811b9c4e2/contourpy-1.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:a7895f46d47671fa7ceec40f31fae721da51ad34bdca0bee83e38870b1f47ffd", size = 220987 }, + { url = "https://files.pythonhosted.org/packages/2f/24/a4b285d6adaaf9746e4700932f579f1a7b6f9681109f694cfa233ae75c4e/contourpy-1.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9ddeb796389dadcd884c7eb07bd14ef12408aaae358f0e2ae24114d797eede30", size = 285001 }, + { url = "https://files.pythonhosted.org/packages/48/1d/fb49a401b5ca4f06ccf467cd6c4f1fd65767e63c21322b29b04ec40b40b9/contourpy-1.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:19c1555a6801c2f084c7ddc1c6e11f02eb6a6016ca1318dd5452ba3f613a1751", size = 268553 }, + { url = "https://files.pythonhosted.org/packages/79/1e/4aef9470d13fd029087388fae750dccb49a50c012a6c8d1d634295caa644/contourpy-1.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:841ad858cff65c2c04bf93875e384ccb82b654574a6d7f30453a04f04af71342", size = 310386 }, + { url = "https://files.pythonhosted.org/packages/b0/34/910dc706ed70153b60392b5305c708c9810d425bde12499c9184a1100888/contourpy-1.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4318af1c925fb9a4fb190559ef3eec206845f63e80fb603d47f2d6d67683901c", size = 349806 }, + { url = "https://files.pythonhosted.org/packages/31/3c/faee6a40d66d7f2a87f7102236bf4780c57990dd7f98e5ff29881b1b1344/contourpy-1.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:14c102b0eab282427b662cb590f2e9340a9d91a1c297f48729431f2dcd16e14f", size = 321108 }, + { url = "https://files.pythonhosted.org/packages/17/69/390dc9b20dd4bb20585651d7316cc3054b7d4a7b4f8b710b2b698e08968d/contourpy-1.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05e806338bfeaa006acbdeba0ad681a10be63b26e1b17317bfac3c5d98f36cda", size = 327291 }, + { url = "https://files.pythonhosted.org/packages/ef/74/7030b67c4e941fe1e5424a3d988080e83568030ce0355f7c9fc556455b01/contourpy-1.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4d76d5993a34ef3df5181ba3c92fabb93f1eaa5729504fb03423fcd9f3177242", size = 1263752 }, + { url = "https://files.pythonhosted.org/packages/f0/ed/92d86f183a8615f13f6b9cbfc5d4298a509d6ce433432e21da838b4b63f4/contourpy-1.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:89785bb2a1980c1bd87f0cb1517a71cde374776a5f150936b82580ae6ead44a1", size = 1318403 }, + { url = "https://files.pythonhosted.org/packages/b3/0e/c8e4950c77dcfc897c71d61e56690a0a9df39543d2164040301b5df8e67b/contourpy-1.3.1-cp313-cp313t-win32.whl", hash = "sha256:8eb96e79b9f3dcadbad2a3891672f81cdcab7f95b27f28f1c67d75f045b6b4f1", size = 185117 }, + { url = "https://files.pythonhosted.org/packages/c1/31/1ae946f11dfbd229222e6d6ad8e7bd1891d3d48bde5fbf7a0beb9491f8e3/contourpy-1.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:287ccc248c9e0d0566934e7d606201abd74761b5703d804ff3df8935f523d546", size = 236668 }, + { url = "https://files.pythonhosted.org/packages/3e/4f/e56862e64b52b55b5ddcff4090085521fc228ceb09a88390a2b103dccd1b/contourpy-1.3.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b457d6430833cee8e4b8e9b6f07aa1c161e5e0d52e118dc102c8f9bd7dd060d6", size = 265605 }, + { url = "https://files.pythonhosted.org/packages/b0/2e/52bfeeaa4541889f23d8eadc6386b442ee2470bd3cff9baa67deb2dd5c57/contourpy-1.3.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb76c1a154b83991a3cbbf0dfeb26ec2833ad56f95540b442c73950af2013750", size = 315040 }, + { url = "https://files.pythonhosted.org/packages/52/94/86bfae441707205634d80392e873295652fc313dfd93c233c52c4dc07874/contourpy-1.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:44a29502ca9c7b5ba389e620d44f2fbe792b1fb5734e8b931ad307071ec58c53", size = 218221 }, +] + +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321 }, +] + +[[package]] +name = "deephyper" +version = "0.9.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpickle" }, + { name = "configspace" }, + { name = "dm-tree" }, + { name = "jinja2" }, + { name = "loky" }, + { name = "matplotlib" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "parse" }, + { name = "psutil" }, + { name = "pymoo" }, + { name = "pyyaml" }, + { name = "scikit-learn" }, + { name = "scipy" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/c2/58f5a7d12907504df1664b7e39cd68bd67ab9c1b91a9f0a4dfee0bd461fe/deephyper-0.9.3.tar.gz", hash = "sha256:e28f9f6ca596edee7fa73f7377ca17ddab99ac4b3c7f15db27cb6e786f770f23", size = 3240932 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/16/ce94a9083fc950f6fe13532f9c79d7a9163c1d322d214857216f73d4df0a/deephyper-0.9.3-py3-none-any.whl", hash = "sha256:dfcea106b291b6e71c96e8658177f290378d986ff2b7a85f92b0253222620043", size = 263078 }, +] + +[[package]] +name = "deephyper-benchmark" +version = "0.0.2" +source = { editable = "." } +dependencies = [ + { name = "deephyper" }, +] + +[package.metadata] +requires-dist = [{ name = "deephyper", specifier = ">=0.9.3" }] + +[[package]] +name = "deprecated" +version = "1.2.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998 }, +] + +[[package]] +name = "dill" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/43/86fe3f9e130c4137b0f1b50784dd70a5087b911fe07fa81e53e0c4c47fea/dill-0.3.9.tar.gz", hash = "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c", size = 187000 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/d1/e73b6ad76f0b1fb7f23c35c6d95dbc506a9c8804f43dda8cb5b0fa6331fd/dill-0.3.9-py3-none-any.whl", hash = "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a", size = 119418 }, +] + +[[package]] +name = "dm-tree" +version = "0.1.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "absl-py" }, + { name = "attrs" }, + { name = "numpy" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/83/ce29720ccf934c6cfa9b9c95ebbe96558386e66886626066632b5e44afed/dm_tree-0.1.9.tar.gz", hash = "sha256:a4c7db3d3935a5a2d5e4b383fc26c6b0cd6f78c6d4605d3e7b518800ecd5342b", size = 35623 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/6a/64924e102f559c1380263a28a751f20a1bdd18e85ea599e216feead84adf/dm_tree-0.1.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54d5616015412311df154908069fcf2c2d8786f6088a2ae3554d186cdf2b1e15", size = 146935 }, + { url = "https://files.pythonhosted.org/packages/7c/79/ba0f7274164eb6bd06a36c2f8cb21b0debc32fd9ba8e73a7c9e50c90041b/dm_tree-0.1.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831699d2c60a1b38776a193b7143ae0acad0a687d87654e6d3342584166816bc", size = 152892 }, + { url = "https://files.pythonhosted.org/packages/bf/20/8b96a34a15c5c4d1d6af44795963fa44381716975aabac83beab4fe80974/dm_tree-0.1.9-cp310-cp310-win_amd64.whl", hash = "sha256:1ae3cbff592bb3f2e197f5a8030de4a94e292e6cdd85adeea0b971d07a1b85f2", size = 101469 }, + { url = "https://files.pythonhosted.org/packages/3e/07/57459f32cf5683c25b596ab58f42a3305f91876c2f03d2fa6e9d0df75fcb/dm_tree-0.1.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e660d1779ddcbd1348410d08f67db4870d413a3ec4ba8b4b045bd5ce4bd8f35c", size = 146926 }, + { url = "https://files.pythonhosted.org/packages/e8/46/939fbf81177c7cb3b1e5ddebd696237b3be9520769cce882f064de497103/dm_tree-0.1.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:294dc1cecf87552a45cdd5ddb215e7f5295a5a47c46f1f0a0463c3dd02a527d7", size = 152851 }, + { url = "https://files.pythonhosted.org/packages/35/3e/a46933e0157b0ac87619a754ce1a796b2afc6386fca7c11f95c010f40745/dm_tree-0.1.9-cp311-cp311-win_amd64.whl", hash = "sha256:12f4cc6cd52a39aa38ff31577b6d79b6136a9a89273a876bf62335c9f65c27bf", size = 101522 }, + { url = "https://files.pythonhosted.org/packages/81/10/120cd40556407879c1069941bd8b0d1a75754128c1a5bf0e27dbcf2a49fc/dm_tree-0.1.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80c43417814b1181d3367b335460bfdd30b79ee187a64220e11f6ddd093a4b15", size = 147204 }, + { url = "https://files.pythonhosted.org/packages/86/52/27607a275c12858b979b8e943d2bd3bd0f9028503bb7079d5830a8b3cac0/dm_tree-0.1.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2334cfe9d2ed4293f9f1c7aefba0657deaab9ea74b5fadd966f6d01d9b6b42d9", size = 153013 }, + { url = "https://files.pythonhosted.org/packages/ea/97/4f78412f73a9350bc8f934441bae5b68b102c8f4240a7f06b4114b51d6de/dm_tree-0.1.9-cp312-cp312-win_amd64.whl", hash = "sha256:9020a5ce256fcc83aa4bc190cc96dd66e87685db0a6e501b0c06aa492c2e38fc", size = 102022 }, + { url = "https://files.pythonhosted.org/packages/37/6a/512abdf7f20acc6cd6fce77f7663014d129aa313b5953aa2603d58fdb0c9/dm_tree-0.1.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d05622d074353cf434049206e53c12147903a048c4bd7d77f2800d427413ad78", size = 147210 }, + { url = "https://files.pythonhosted.org/packages/e5/0a/f4d72ffb64ab3edc1fa66261f81ee3b4142ab14cd8aa1dfc7bbeca5ee4ba/dm_tree-0.1.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f68b0efad76703dd4648586c75618a48cdd671b68c3266fe980e323c15423607", size = 153043 }, + { url = "https://files.pythonhosted.org/packages/0d/ee/529ce999770b4d621a64af86c60cfee52f0cdd7294752105179ebf1c07c6/dm_tree-0.1.9-cp313-cp313-win_amd64.whl", hash = "sha256:e97c34fcb44941c36b7ee81dcdbceba0fbe728bddcc77e5837ab2eb665bcbff8", size = 102043 }, + { url = "https://files.pythonhosted.org/packages/84/1d/3cdbeeb3f6937a47a26cee502bffeccc2e55b97dfcce8a1d1135ea1b5b47/dm_tree-0.1.9-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6893fcdc5cf1a4f459cfc383526d35d42e7c671ae565d7e429a2f2cb2cb93e89", size = 147282 }, + { url = "https://files.pythonhosted.org/packages/c5/37/15603079854394f16e3833a7b50696c1f3cbf30a2243a119f64f18a16f36/dm_tree-0.1.9-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1f5d1e96b3a7de22b25b13a5eb30f41f8cf9c02dd4479a24920de99e780903c", size = 153052 }, +] + +[[package]] +name = "fonttools" +version = "4.56.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/8c/9ffa2a555af0e5e5d0e2ed7fdd8c9bef474ed676995bb4c57c9cd0014248/fonttools-4.56.0.tar.gz", hash = "sha256:a114d1567e1a1586b7e9e7fc2ff686ca542a82769a296cef131e4c4af51e58f4", size = 3462892 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/6ac30c2cc6a29454260f13c9c6422fc509b7982c13cd4597041260d8f482/fonttools-4.56.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:331954d002dbf5e704c7f3756028e21db07097c19722569983ba4d74df014000", size = 2752190 }, + { url = "https://files.pythonhosted.org/packages/92/3a/ac382a8396d1b420ee45eeb0f65b614a9ca7abbb23a1b17524054f0f2200/fonttools-4.56.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8d1613abd5af2f93c05867b3a3759a56e8bf97eb79b1da76b2bc10892f96ff16", size = 2280624 }, + { url = "https://files.pythonhosted.org/packages/8a/ae/00b58bfe20e9ff7fbc3dda38f5d127913942b5e252288ea9583099a31bf5/fonttools-4.56.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:705837eae384fe21cee5e5746fd4f4b2f06f87544fa60f60740007e0aa600311", size = 4562074 }, + { url = "https://files.pythonhosted.org/packages/46/d0/0004ca8f6a200252e5bd6982ed99b5fe58c4c59efaf5f516621c4cd8f703/fonttools-4.56.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc871904a53a9d4d908673c6faa15689874af1c7c5ac403a8e12d967ebd0c0dc", size = 4604747 }, + { url = "https://files.pythonhosted.org/packages/45/ea/c8862bd3e09d143ef8ed8268ec8a7d477828f960954889e65288ac050b08/fonttools-4.56.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:38b947de71748bab150259ee05a775e8a0635891568e9fdb3cdd7d0e0004e62f", size = 4559025 }, + { url = "https://files.pythonhosted.org/packages/8f/75/bb88a9552ec1de31a414066257bfd9f40f4ada00074f7a3799ea39b5741f/fonttools-4.56.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:86b2a1013ef7a64d2e94606632683f07712045ed86d937c11ef4dde97319c086", size = 4728482 }, + { url = "https://files.pythonhosted.org/packages/2a/5f/80a2b640df1e1bb7d459d62c8b3f37fe83fd413897e549106d4ebe6371f5/fonttools-4.56.0-cp310-cp310-win32.whl", hash = "sha256:133bedb9a5c6376ad43e6518b7e2cd2f866a05b1998f14842631d5feb36b5786", size = 2155557 }, + { url = "https://files.pythonhosted.org/packages/8f/85/0904f9dbe51ac70d878d3242a8583b9453a09105c3ed19c6301247fd0d3a/fonttools-4.56.0-cp310-cp310-win_amd64.whl", hash = "sha256:17f39313b649037f6c800209984a11fc256a6137cbe5487091c6c7187cae4685", size = 2200017 }, + { url = "https://files.pythonhosted.org/packages/35/56/a2f3e777d48fcae7ecd29de4d96352d84e5ea9871e5f3fc88241521572cf/fonttools-4.56.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ef04bc7827adb7532be3d14462390dd71287644516af3f1e67f1e6ff9c6d6df", size = 2753325 }, + { url = "https://files.pythonhosted.org/packages/71/85/d483e9c4e5ed586b183bf037a353e8d766366b54fd15519b30e6178a6a6e/fonttools-4.56.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ffda9b8cd9cb8b301cae2602ec62375b59e2e2108a117746f12215145e3f786c", size = 2281554 }, + { url = "https://files.pythonhosted.org/packages/09/67/060473b832b2fade03c127019794df6dc02d9bc66fa4210b8e0d8a99d1e5/fonttools-4.56.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e993e8db36306cc3f1734edc8ea67906c55f98683d6fd34c3fc5593fdbba4c", size = 4869260 }, + { url = "https://files.pythonhosted.org/packages/28/e9/47c02d5a7027e8ed841ab6a10ca00c93dadd5f16742f1af1fa3f9978adf4/fonttools-4.56.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:003548eadd674175510773f73fb2060bb46adb77c94854af3e0cc5bc70260049", size = 4898508 }, + { url = "https://files.pythonhosted.org/packages/bf/8a/221d456d1afb8ca043cfd078f59f187ee5d0a580f4b49351b9ce95121f57/fonttools-4.56.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd9825822e7bb243f285013e653f6741954d8147427aaa0324a862cdbf4cbf62", size = 4877700 }, + { url = "https://files.pythonhosted.org/packages/a4/8c/e503863adf7a6aeff7b960e2f66fa44dd0c29a7a8b79765b2821950d7b05/fonttools-4.56.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b23d30a2c0b992fb1c4f8ac9bfde44b5586d23457759b6cf9a787f1a35179ee0", size = 5045817 }, + { url = "https://files.pythonhosted.org/packages/2b/50/79ba3b7e42f4eaa70b82b9e79155f0f6797858dc8a97862428b6852c6aee/fonttools-4.56.0-cp311-cp311-win32.whl", hash = "sha256:47b5e4680002ae1756d3ae3b6114e20aaee6cc5c69d1e5911f5ffffd3ee46c6b", size = 2154426 }, + { url = "https://files.pythonhosted.org/packages/3b/90/4926e653041c4116ecd43e50e3c79f5daae6dcafc58ceb64bc4f71dd4924/fonttools-4.56.0-cp311-cp311-win_amd64.whl", hash = "sha256:14a3e3e6b211660db54ca1ef7006401e4a694e53ffd4553ab9bc87ead01d0f05", size = 2200937 }, + { url = "https://files.pythonhosted.org/packages/39/32/71cfd6877999576a11824a7fe7bc0bb57c5c72b1f4536fa56a3e39552643/fonttools-4.56.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6f195c14c01bd057bc9b4f70756b510e009c83c5ea67b25ced3e2c38e6ee6e9", size = 2747757 }, + { url = "https://files.pythonhosted.org/packages/15/52/d9f716b072c5061a0b915dd4c387f74bef44c68c069e2195c753905bd9b7/fonttools-4.56.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fa760e5fe8b50cbc2d71884a1eff2ed2b95a005f02dda2fa431560db0ddd927f", size = 2279007 }, + { url = "https://files.pythonhosted.org/packages/d1/97/f1b3a8afa9a0d814a092a25cd42f59ccb98a0bb7a295e6e02fc9ba744214/fonttools-4.56.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d54a45d30251f1d729e69e5b675f9a08b7da413391a1227781e2a297fa37f6d2", size = 4783991 }, + { url = "https://files.pythonhosted.org/packages/95/70/2a781bedc1c45a0c61d29c56425609b22ed7f971da5d7e5df2679488741b/fonttools-4.56.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:661a8995d11e6e4914a44ca7d52d1286e2d9b154f685a4d1f69add8418961563", size = 4855109 }, + { url = "https://files.pythonhosted.org/packages/0c/02/a2597858e61a5e3fb6a14d5f6be9e6eb4eaf090da56ad70cedcbdd201685/fonttools-4.56.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9d94449ad0a5f2a8bf5d2f8d71d65088aee48adbe45f3c5f8e00e3ad861ed81a", size = 4762496 }, + { url = "https://files.pythonhosted.org/packages/f2/00/aaf00100d6078fdc73f7352b44589804af9dc12b182a2540b16002152ba4/fonttools-4.56.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f59746f7953f69cc3290ce2f971ab01056e55ddd0fb8b792c31a8acd7fee2d28", size = 4990094 }, + { url = "https://files.pythonhosted.org/packages/bf/dc/3ff1db522460db60cf3adaf1b64e0c72b43406717d139786d3fa1eb20709/fonttools-4.56.0-cp312-cp312-win32.whl", hash = "sha256:bce60f9a977c9d3d51de475af3f3581d9b36952e1f8fc19a1f2254f1dda7ce9c", size = 2142888 }, + { url = "https://files.pythonhosted.org/packages/6f/e3/5a181a85777f7809076e51f7422e0dc77eb04676c40ec8bf6a49d390d1ff/fonttools-4.56.0-cp312-cp312-win_amd64.whl", hash = "sha256:300c310bb725b2bdb4f5fc7e148e190bd69f01925c7ab437b9c0ca3e1c7cd9ba", size = 2189734 }, + { url = "https://files.pythonhosted.org/packages/a5/55/f06b48d48e0b4ec3a3489efafe9bd4d81b6e0802ac51026e3ee4634e89ba/fonttools-4.56.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f20e2c0dfab82983a90f3d00703ac0960412036153e5023eed2b4641d7d5e692", size = 2735127 }, + { url = "https://files.pythonhosted.org/packages/59/db/d2c7c9b6dd5cbd46f183e650a47403ffb88fca17484eb7c4b1cd88f9e513/fonttools-4.56.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f36a0868f47b7566237640c026c65a86d09a3d9ca5df1cd039e30a1da73098a0", size = 2272519 }, + { url = "https://files.pythonhosted.org/packages/4d/a2/da62d779c34a0e0c06415f02eab7fa3466de5d46df459c0275a255cefc65/fonttools-4.56.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62b4c6802fa28e14dba010e75190e0e6228513573f1eeae57b11aa1a39b7e5b1", size = 4762423 }, + { url = "https://files.pythonhosted.org/packages/be/6a/fd4018e0448c8a5e12138906411282c5eab51a598493f080a9f0960e658f/fonttools-4.56.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a05d1f07eb0a7d755fbe01fee1fd255c3a4d3730130cf1bfefb682d18fd2fcea", size = 4834442 }, + { url = "https://files.pythonhosted.org/packages/6d/63/fa1dec8efb35bc11ef9c39b2d74754b45d48a3ccb2cf78c0109c0af639e8/fonttools-4.56.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0073b62c3438cf0058488c002ea90489e8801d3a7af5ce5f7c05c105bee815c3", size = 4742800 }, + { url = "https://files.pythonhosted.org/packages/dd/f4/963247ae8c73ccc4cf2929e7162f595c81dbe17997d1d0ea77da24a217c9/fonttools-4.56.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cad98c94833465bcf28f51c248aaf07ca022efc6a3eba750ad9c1e0256d278", size = 4963746 }, + { url = "https://files.pythonhosted.org/packages/ea/e0/46f9600c39c644b54e4420f941f75fa200d9288c9ae171e5d80918b8cbb9/fonttools-4.56.0-cp313-cp313-win32.whl", hash = "sha256:d0cb73ccf7f6d7ca8d0bc7ea8ac0a5b84969a41c56ac3ac3422a24df2680546f", size = 2140927 }, + { url = "https://files.pythonhosted.org/packages/27/6d/3edda54f98a550a0473f032d8050315fbc8f1b76a0d9f3879b72ebb2cdd6/fonttools-4.56.0-cp313-cp313-win_amd64.whl", hash = "sha256:62cc1253827d1e500fde9dbe981219fea4eb000fd63402283472d38e7d8aa1c6", size = 2186709 }, + { url = "https://files.pythonhosted.org/packages/bf/ff/44934a031ce5a39125415eb405b9efb76fe7f9586b75291d66ae5cbfc4e6/fonttools-4.56.0-py3-none-any.whl", hash = "sha256:1088182f68c303b50ca4dc0c82d42083d176cba37af1937e1a976a31149d4d14", size = 1089800 }, +] + +[[package]] +name = "grapheme" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/e7/bbaab0d2a33e07c8278910c1d0d8d4f3781293dfbc70b5c38197159046bf/grapheme-0.6.0.tar.gz", hash = "sha256:44c2b9f21bbe77cfb05835fec230bd435954275267fea1858013b102f8603cca", size = 207306 } + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, +] + +[[package]] +name = "joblib" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/64/33/60135848598c076ce4b231e1b1895170f45fbcaeaa2c9d5e38b04db70c35/joblib-1.4.2.tar.gz", hash = "sha256:2382c5816b2636fbd20a09e0f4e9dad4736765fdfb7dca582943b9c1366b3f0e", size = 2116621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/29/df4b9b42f2be0b623cbd5e2140cafcaa2bef0759a00b7b70104dcfe2fb51/joblib-1.4.2-py3-none-any.whl", hash = "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6", size = 301817 }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/5f/4d8e9e852d98ecd26cdf8eaf7ed8bc33174033bba5e07001b289f07308fd/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db", size = 124623 }, + { url = "https://files.pythonhosted.org/packages/1d/70/7f5af2a18a76fe92ea14675f8bd88ce53ee79e37900fa5f1a1d8e0b42998/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72941acb7b67138f35b879bbe85be0f6c6a70cab78fe3ef6db9c024d9223e5b", size = 66720 }, + { url = "https://files.pythonhosted.org/packages/c6/13/e15f804a142353aefd089fadc8f1d985561a15358c97aca27b0979cb0785/kiwisolver-1.4.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce2cf1e5688edcb727fdf7cd1bbd0b6416758996826a8be1d958f91880d0809d", size = 65413 }, + { url = "https://files.pythonhosted.org/packages/ce/6d/67d36c4d2054e83fb875c6b59d0809d5c530de8148846b1370475eeeece9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c8bf637892dc6e6aad2bc6d4d69d08764166e5e3f69d469e55427b6ac001b19d", size = 1650826 }, + { url = "https://files.pythonhosted.org/packages/de/c6/7b9bb8044e150d4d1558423a1568e4f227193662a02231064e3824f37e0a/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:034d2c891f76bd3edbdb3ea11140d8510dca675443da7304205a2eaa45d8334c", size = 1628231 }, + { url = "https://files.pythonhosted.org/packages/b6/38/ad10d437563063eaaedbe2c3540a71101fc7fb07a7e71f855e93ea4de605/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47b28d1dfe0793d5e96bce90835e17edf9a499b53969b03c6c47ea5985844c3", size = 1408938 }, + { url = "https://files.pythonhosted.org/packages/52/ce/c0106b3bd7f9e665c5f5bc1e07cc95b5dabd4e08e3dad42dbe2faad467e7/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb158fe28ca0c29f2260cca8c43005329ad58452c36f0edf298204de32a9a3ed", size = 1422799 }, + { url = "https://files.pythonhosted.org/packages/d0/87/efb704b1d75dc9758087ba374c0f23d3254505edaedd09cf9d247f7878b9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5536185fce131780ebd809f8e623bf4030ce1b161353166c49a3c74c287897f", size = 1354362 }, + { url = "https://files.pythonhosted.org/packages/eb/b3/fd760dc214ec9a8f208b99e42e8f0130ff4b384eca8b29dd0efc62052176/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:369b75d40abedc1da2c1f4de13f3482cb99e3237b38726710f4a793432b1c5ff", size = 2222695 }, + { url = "https://files.pythonhosted.org/packages/a2/09/a27fb36cca3fc01700687cc45dae7a6a5f8eeb5f657b9f710f788748e10d/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:641f2ddf9358c80faa22e22eb4c9f54bd3f0e442e038728f500e3b978d00aa7d", size = 2370802 }, + { url = "https://files.pythonhosted.org/packages/3d/c3/ba0a0346db35fe4dc1f2f2cf8b99362fbb922d7562e5f911f7ce7a7b60fa/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d561d2d8883e0819445cfe58d7ddd673e4015c3c57261d7bdcd3710d0d14005c", size = 2334646 }, + { url = "https://files.pythonhosted.org/packages/41/52/942cf69e562f5ed253ac67d5c92a693745f0bed3c81f49fc0cbebe4d6b00/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1732e065704b47c9afca7ffa272f845300a4eb959276bf6970dc07265e73b605", size = 2467260 }, + { url = "https://files.pythonhosted.org/packages/32/26/2d9668f30d8a494b0411d4d7d4ea1345ba12deb6a75274d58dd6ea01e951/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bcb1ebc3547619c3b58a39e2448af089ea2ef44b37988caf432447374941574e", size = 2288633 }, + { url = "https://files.pythonhosted.org/packages/98/99/0dd05071654aa44fe5d5e350729961e7bb535372935a45ac89a8924316e6/kiwisolver-1.4.8-cp310-cp310-win_amd64.whl", hash = "sha256:89c107041f7b27844179ea9c85d6da275aa55ecf28413e87624d033cf1f6b751", size = 71885 }, + { url = "https://files.pythonhosted.org/packages/6c/fc/822e532262a97442989335394d441cd1d0448c2e46d26d3e04efca84df22/kiwisolver-1.4.8-cp310-cp310-win_arm64.whl", hash = "sha256:b5773efa2be9eb9fcf5415ea3ab70fc785d598729fd6057bea38d539ead28271", size = 65175 }, + { url = "https://files.pythonhosted.org/packages/da/ed/c913ee28936c371418cb167b128066ffb20bbf37771eecc2c97edf8a6e4c/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84", size = 124635 }, + { url = "https://files.pythonhosted.org/packages/4c/45/4a7f896f7467aaf5f56ef093d1f329346f3b594e77c6a3c327b2d415f521/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561", size = 66717 }, + { url = "https://files.pythonhosted.org/packages/5f/b4/c12b3ac0852a3a68f94598d4c8d569f55361beef6159dce4e7b624160da2/kiwisolver-1.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7", size = 65413 }, + { url = "https://files.pythonhosted.org/packages/a9/98/1df4089b1ed23d83d410adfdc5947245c753bddfbe06541c4aae330e9e70/kiwisolver-1.4.8-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03", size = 1343994 }, + { url = "https://files.pythonhosted.org/packages/8d/bf/b4b169b050c8421a7c53ea1ea74e4ef9c335ee9013216c558a047f162d20/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954", size = 1434804 }, + { url = "https://files.pythonhosted.org/packages/66/5a/e13bd341fbcf73325ea60fdc8af752addf75c5079867af2e04cc41f34434/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79", size = 1450690 }, + { url = "https://files.pythonhosted.org/packages/9b/4f/5955dcb376ba4a830384cc6fab7d7547bd6759fe75a09564910e9e3bb8ea/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6", size = 1376839 }, + { url = "https://files.pythonhosted.org/packages/3a/97/5edbed69a9d0caa2e4aa616ae7df8127e10f6586940aa683a496c2c280b9/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0", size = 1435109 }, + { url = "https://files.pythonhosted.org/packages/13/fc/e756382cb64e556af6c1809a1bbb22c141bbc2445049f2da06b420fe52bf/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab", size = 2245269 }, + { url = "https://files.pythonhosted.org/packages/76/15/e59e45829d7f41c776d138245cabae6515cb4eb44b418f6d4109c478b481/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc", size = 2393468 }, + { url = "https://files.pythonhosted.org/packages/e9/39/483558c2a913ab8384d6e4b66a932406f87c95a6080112433da5ed668559/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25", size = 2355394 }, + { url = "https://files.pythonhosted.org/packages/01/aa/efad1fbca6570a161d29224f14b082960c7e08268a133fe5dc0f6906820e/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc", size = 2490901 }, + { url = "https://files.pythonhosted.org/packages/c9/4f/15988966ba46bcd5ab9d0c8296914436720dd67fca689ae1a75b4ec1c72f/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67", size = 2312306 }, + { url = "https://files.pythonhosted.org/packages/2d/27/bdf1c769c83f74d98cbc34483a972f221440703054894a37d174fba8aa68/kiwisolver-1.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34", size = 71966 }, + { url = "https://files.pythonhosted.org/packages/4a/c9/9642ea855604aeb2968a8e145fc662edf61db7632ad2e4fb92424be6b6c0/kiwisolver-1.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2", size = 65311 }, + { url = "https://files.pythonhosted.org/packages/fc/aa/cea685c4ab647f349c3bc92d2daf7ae34c8e8cf405a6dcd3a497f58a2ac3/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502", size = 124152 }, + { url = "https://files.pythonhosted.org/packages/c5/0b/8db6d2e2452d60d5ebc4ce4b204feeb16176a851fd42462f66ade6808084/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31", size = 66555 }, + { url = "https://files.pythonhosted.org/packages/60/26/d6a0db6785dd35d3ba5bf2b2df0aedc5af089962c6eb2cbf67a15b81369e/kiwisolver-1.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb", size = 65067 }, + { url = "https://files.pythonhosted.org/packages/c9/ed/1d97f7e3561e09757a196231edccc1bcf59d55ddccefa2afc9c615abd8e0/kiwisolver-1.4.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f", size = 1378443 }, + { url = "https://files.pythonhosted.org/packages/29/61/39d30b99954e6b46f760e6289c12fede2ab96a254c443639052d1b573fbc/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc", size = 1472728 }, + { url = "https://files.pythonhosted.org/packages/0c/3e/804163b932f7603ef256e4a715e5843a9600802bb23a68b4e08c8c0ff61d/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a", size = 1478388 }, + { url = "https://files.pythonhosted.org/packages/8a/9e/60eaa75169a154700be74f875a4d9961b11ba048bef315fbe89cb6999056/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a", size = 1413849 }, + { url = "https://files.pythonhosted.org/packages/bc/b3/9458adb9472e61a998c8c4d95cfdfec91c73c53a375b30b1428310f923e4/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a", size = 1475533 }, + { url = "https://files.pythonhosted.org/packages/e4/7a/0a42d9571e35798de80aef4bb43a9b672aa7f8e58643d7bd1950398ffb0a/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3", size = 2268898 }, + { url = "https://files.pythonhosted.org/packages/d9/07/1255dc8d80271400126ed8db35a1795b1a2c098ac3a72645075d06fe5c5d/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b", size = 2425605 }, + { url = "https://files.pythonhosted.org/packages/84/df/5a3b4cf13780ef6f6942df67b138b03b7e79e9f1f08f57c49957d5867f6e/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4", size = 2375801 }, + { url = "https://files.pythonhosted.org/packages/8f/10/2348d068e8b0f635c8c86892788dac7a6b5c0cb12356620ab575775aad89/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d", size = 2520077 }, + { url = "https://files.pythonhosted.org/packages/32/d8/014b89fee5d4dce157d814303b0fce4d31385a2af4c41fed194b173b81ac/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8", size = 2338410 }, + { url = "https://files.pythonhosted.org/packages/bd/72/dfff0cc97f2a0776e1c9eb5bef1ddfd45f46246c6533b0191887a427bca5/kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50", size = 71853 }, + { url = "https://files.pythonhosted.org/packages/dc/85/220d13d914485c0948a00f0b9eb419efaf6da81b7d72e88ce2391f7aed8d/kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476", size = 65424 }, + { url = "https://files.pythonhosted.org/packages/79/b3/e62464a652f4f8cd9006e13d07abad844a47df1e6537f73ddfbf1bc997ec/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09", size = 124156 }, + { url = "https://files.pythonhosted.org/packages/8d/2d/f13d06998b546a2ad4f48607a146e045bbe48030774de29f90bdc573df15/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1", size = 66555 }, + { url = "https://files.pythonhosted.org/packages/59/e3/b8bd14b0a54998a9fd1e8da591c60998dc003618cb19a3f94cb233ec1511/kiwisolver-1.4.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c", size = 65071 }, + { url = "https://files.pythonhosted.org/packages/f0/1c/6c86f6d85ffe4d0ce04228d976f00674f1df5dc893bf2dd4f1928748f187/kiwisolver-1.4.8-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b", size = 1378053 }, + { url = "https://files.pythonhosted.org/packages/4e/b9/1c6e9f6dcb103ac5cf87cb695845f5fa71379021500153566d8a8a9fc291/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47", size = 1472278 }, + { url = "https://files.pythonhosted.org/packages/ee/81/aca1eb176de671f8bda479b11acdc42c132b61a2ac861c883907dde6debb/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16", size = 1478139 }, + { url = "https://files.pythonhosted.org/packages/49/f4/e081522473671c97b2687d380e9e4c26f748a86363ce5af48b4a28e48d06/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc", size = 1413517 }, + { url = "https://files.pythonhosted.org/packages/8f/e9/6a7d025d8da8c4931522922cd706105aa32b3291d1add8c5427cdcd66e63/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246", size = 1474952 }, + { url = "https://files.pythonhosted.org/packages/82/13/13fa685ae167bee5d94b415991c4fc7bb0a1b6ebea6e753a87044b209678/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794", size = 2269132 }, + { url = "https://files.pythonhosted.org/packages/ef/92/bb7c9395489b99a6cb41d502d3686bac692586db2045adc19e45ee64ed23/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b", size = 2425997 }, + { url = "https://files.pythonhosted.org/packages/ed/12/87f0e9271e2b63d35d0d8524954145837dd1a6c15b62a2d8c1ebe0f182b4/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3", size = 2376060 }, + { url = "https://files.pythonhosted.org/packages/02/6e/c8af39288edbce8bf0fa35dee427b082758a4b71e9c91ef18fa667782138/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957", size = 2520471 }, + { url = "https://files.pythonhosted.org/packages/13/78/df381bc7b26e535c91469f77f16adcd073beb3e2dd25042efd064af82323/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb", size = 2338793 }, + { url = "https://files.pythonhosted.org/packages/d0/dc/c1abe38c37c071d0fc71c9a474fd0b9ede05d42f5a458d584619cfd2371a/kiwisolver-1.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2", size = 71855 }, + { url = "https://files.pythonhosted.org/packages/a0/b6/21529d595b126ac298fdd90b705d87d4c5693de60023e0efcb4f387ed99e/kiwisolver-1.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30", size = 65430 }, + { url = "https://files.pythonhosted.org/packages/34/bd/b89380b7298e3af9b39f49334e3e2a4af0e04819789f04b43d560516c0c8/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c", size = 126294 }, + { url = "https://files.pythonhosted.org/packages/83/41/5857dc72e5e4148eaac5aa76e0703e594e4465f8ab7ec0fc60e3a9bb8fea/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc", size = 67736 }, + { url = "https://files.pythonhosted.org/packages/e1/d1/be059b8db56ac270489fb0b3297fd1e53d195ba76e9bbb30e5401fa6b759/kiwisolver-1.4.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712", size = 66194 }, + { url = "https://files.pythonhosted.org/packages/e1/83/4b73975f149819eb7dcf9299ed467eba068ecb16439a98990dcb12e63fdd/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e", size = 1465942 }, + { url = "https://files.pythonhosted.org/packages/c7/2c/30a5cdde5102958e602c07466bce058b9d7cb48734aa7a4327261ac8e002/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880", size = 1595341 }, + { url = "https://files.pythonhosted.org/packages/ff/9b/1e71db1c000385aa069704f5990574b8244cce854ecd83119c19e83c9586/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062", size = 1598455 }, + { url = "https://files.pythonhosted.org/packages/85/92/c8fec52ddf06231b31cbb779af77e99b8253cd96bd135250b9498144c78b/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7", size = 1522138 }, + { url = "https://files.pythonhosted.org/packages/0b/51/9eb7e2cd07a15d8bdd976f6190c0164f92ce1904e5c0c79198c4972926b7/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed", size = 1582857 }, + { url = "https://files.pythonhosted.org/packages/0f/95/c5a00387a5405e68ba32cc64af65ce881a39b98d73cc394b24143bebc5b8/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d", size = 2293129 }, + { url = "https://files.pythonhosted.org/packages/44/83/eeb7af7d706b8347548313fa3a3a15931f404533cc54fe01f39e830dd231/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165", size = 2421538 }, + { url = "https://files.pythonhosted.org/packages/05/f9/27e94c1b3eb29e6933b6986ffc5fa1177d2cd1f0c8efc5f02c91c9ac61de/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6", size = 2390661 }, + { url = "https://files.pythonhosted.org/packages/d9/d4/3c9735faa36ac591a4afcc2980d2691000506050b7a7e80bcfe44048daa7/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90", size = 2546710 }, + { url = "https://files.pythonhosted.org/packages/4c/fa/be89a49c640930180657482a74970cdcf6f7072c8d2471e1babe17a222dc/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85", size = 2349213 }, + { url = "https://files.pythonhosted.org/packages/1f/f9/ae81c47a43e33b93b0a9819cac6723257f5da2a5a60daf46aa5c7226ea85/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e7a019419b7b510f0f7c9dceff8c5eae2392037eae483a7f9162625233802b0a", size = 60403 }, + { url = "https://files.pythonhosted.org/packages/58/ca/f92b5cb6f4ce0c1ebfcfe3e2e42b96917e16f7090e45b21102941924f18f/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:286b18e86682fd2217a48fc6be6b0f20c1d0ed10958d8dc53453ad58d7be0bf8", size = 58657 }, + { url = "https://files.pythonhosted.org/packages/80/28/ae0240f732f0484d3a4dc885d055653c47144bdf59b670aae0ec3c65a7c8/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4191ee8dfd0be1c3666ccbac178c5a05d5f8d689bbe3fc92f3c4abec817f8fe0", size = 84948 }, + { url = "https://files.pythonhosted.org/packages/5d/eb/78d50346c51db22c7203c1611f9b513075f35c4e0e4877c5dde378d66043/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd2785b9391f2873ad46088ed7599a6a71e762e1ea33e87514b1a441ed1da1c", size = 81186 }, + { url = "https://files.pythonhosted.org/packages/43/f8/7259f18c77adca88d5f64f9a522792e178b2691f3748817a8750c2d216ef/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c07b29089b7ba090b6f1a669f1411f27221c3662b3a1b7010e67b59bb5a6f10b", size = 80279 }, + { url = "https://files.pythonhosted.org/packages/3a/1d/50ad811d1c5dae091e4cf046beba925bcae0a610e79ae4c538f996f63ed5/kiwisolver-1.4.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:65ea09a5a3faadd59c2ce96dc7bf0f364986a315949dc6374f04396b0d60e09b", size = 71762 }, +] + +[[package]] +name = "loky" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpickle" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/44/33d05972d2251ec2b87ed1e53edb9e5a9fa504b361d67af699e8372e702e/loky-3.5.0.tar.gz", hash = "sha256:0ac7648a2c02758c37dcf3587ee4832d245ec5a4b0fec5cc0c6a9cc8e5695b53", size = 100917 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/92/1dd2b193ae15c0b1fa7730b9bbc69ab47bbcd8a0ed7155c3b2da3c7ceb18/loky-3.5.0-py3-none-any.whl", hash = "sha256:9481712107902174df88e86b49793fcb2bd41053c1326b22480b8fd989bea587", size = 54943 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357 }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393 }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732 }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866 }, + { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964 }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977 }, + { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366 }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091 }, + { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065 }, + { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514 }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, +] + +[[package]] +name = "matplotlib" +version = "3.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/08/b89867ecea2e305f408fbb417139a8dd941ecf7b23a2e02157c36da546f0/matplotlib-3.10.1.tar.gz", hash = "sha256:e8d2d0e3881b129268585bf4765ad3ee73a4591d77b9a18c214ac7e3a79fb2ba", size = 36743335 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/b1/f70e27cf1cd76ce2a5e1aa5579d05afe3236052c6d9b9a96325bc823a17e/matplotlib-3.10.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ff2ae14910be903f4a24afdbb6d7d3a6c44da210fc7d42790b87aeac92238a16", size = 8163654 }, + { url = "https://files.pythonhosted.org/packages/26/af/5ec3d4636106718bb62503a03297125d4514f98fe818461bd9e6b9d116e4/matplotlib-3.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0721a3fd3d5756ed593220a8b86808a36c5031fce489adb5b31ee6dbb47dd5b2", size = 8037943 }, + { url = "https://files.pythonhosted.org/packages/a1/3d/07f9003a71b698b848c9925d05979ffa94a75cd25d1a587202f0bb58aa81/matplotlib-3.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0673b4b8f131890eb3a1ad058d6e065fb3c6e71f160089b65f8515373394698", size = 8449510 }, + { url = "https://files.pythonhosted.org/packages/12/87/9472d4513ff83b7cd864311821793ab72234fa201ab77310ec1b585d27e2/matplotlib-3.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e875b95ac59a7908978fe307ecdbdd9a26af7fa0f33f474a27fcf8c99f64a19", size = 8586585 }, + { url = "https://files.pythonhosted.org/packages/31/9e/fe74d237d2963adae8608faeb21f778cf246dbbf4746cef87cffbc82c4b6/matplotlib-3.10.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2589659ea30726284c6c91037216f64a506a9822f8e50592d48ac16a2f29e044", size = 9397911 }, + { url = "https://files.pythonhosted.org/packages/b6/1b/025d3e59e8a4281ab463162ad7d072575354a1916aba81b6a11507dfc524/matplotlib-3.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a97ff127f295817bc34517255c9db6e71de8eddaab7f837b7d341dee9f2f587f", size = 8052998 }, + { url = "https://files.pythonhosted.org/packages/a5/14/a1b840075be247bb1834b22c1e1d558740b0f618fe3a823740181ca557a1/matplotlib-3.10.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:057206ff2d6ab82ff3e94ebd94463d084760ca682ed5f150817b859372ec4401", size = 8174669 }, + { url = "https://files.pythonhosted.org/packages/0a/e4/300b08e3e08f9c98b0d5635f42edabf2f7a1d634e64cb0318a71a44ff720/matplotlib-3.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a144867dd6bf8ba8cb5fc81a158b645037e11b3e5cf8a50bd5f9917cb863adfe", size = 8047996 }, + { url = "https://files.pythonhosted.org/packages/75/f9/8d99ff5a2498a5f1ccf919fb46fb945109623c6108216f10f96428f388bc/matplotlib-3.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56c5d9fcd9879aa8040f196a235e2dcbdf7dd03ab5b07c0696f80bc6cf04bedd", size = 8461612 }, + { url = "https://files.pythonhosted.org/packages/40/b8/53fa08a5eaf78d3a7213fd6da1feec4bae14a81d9805e567013811ff0e85/matplotlib-3.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f69dc9713e4ad2fb21a1c30e37bd445d496524257dfda40ff4a8efb3604ab5c", size = 8602258 }, + { url = "https://files.pythonhosted.org/packages/40/87/4397d2ce808467af86684a622dd112664553e81752ea8bf61bdd89d24a41/matplotlib-3.10.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4c59af3e8aca75d7744b68e8e78a669e91ccbcf1ac35d0102a7b1b46883f1dd7", size = 9408896 }, + { url = "https://files.pythonhosted.org/packages/d7/68/0d03098b3feb786cbd494df0aac15b571effda7f7cbdec267e8a8d398c16/matplotlib-3.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:11b65088c6f3dae784bc72e8d039a2580186285f87448babb9ddb2ad0082993a", size = 8061281 }, + { url = "https://files.pythonhosted.org/packages/7c/1d/5e0dc3b59c034e43de16f94deb68f4ad8a96b3ea00f4b37c160b7474928e/matplotlib-3.10.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:66e907a06e68cb6cfd652c193311d61a12b54f56809cafbed9736ce5ad92f107", size = 8175488 }, + { url = "https://files.pythonhosted.org/packages/7a/81/dae7e14042e74da658c3336ab9799128e09a1ee03964f2d89630b5d12106/matplotlib-3.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b4bb156abb8fa5e5b2b460196f7db7264fc6d62678c03457979e7d5254b7be", size = 8046264 }, + { url = "https://files.pythonhosted.org/packages/21/c4/22516775dcde10fc9c9571d155f90710761b028fc44f660508106c363c97/matplotlib-3.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1985ad3d97f51307a2cbfc801a930f120def19ba22864182dacef55277102ba6", size = 8452048 }, + { url = "https://files.pythonhosted.org/packages/63/23/c0615001f67ce7c96b3051d856baedc0c818a2ed84570b9bf9bde200f85d/matplotlib-3.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c96f2c2f825d1257e437a1482c5a2cf4fee15db4261bd6fc0750f81ba2b4ba3d", size = 8597111 }, + { url = "https://files.pythonhosted.org/packages/ca/c0/a07939a82aed77770514348f4568177d7dadab9787ebc618a616fe3d665e/matplotlib-3.10.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35e87384ee9e488d8dd5a2dd7baf471178d38b90618d8ea147aced4ab59c9bea", size = 9402771 }, + { url = "https://files.pythonhosted.org/packages/a6/b6/a9405484fb40746fdc6ae4502b16a9d6e53282ba5baaf9ebe2da579f68c4/matplotlib-3.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:cfd414bce89cc78a7e1d25202e979b3f1af799e416010a20ab2b5ebb3a02425c", size = 8063742 }, + { url = "https://files.pythonhosted.org/packages/60/73/6770ff5e5523d00f3bc584acb6031e29ee5c8adc2336b16cd1d003675fe0/matplotlib-3.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c42eee41e1b60fd83ee3292ed83a97a5f2a8239b10c26715d8a6172226988d7b", size = 8176112 }, + { url = "https://files.pythonhosted.org/packages/08/97/b0ca5da0ed54a3f6599c3ab568bdda65269bc27c21a2c97868c1625e4554/matplotlib-3.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4f0647b17b667ae745c13721602b540f7aadb2a32c5b96e924cd4fea5dcb90f1", size = 8046931 }, + { url = "https://files.pythonhosted.org/packages/df/9a/1acbdc3b165d4ce2dcd2b1a6d4ffb46a7220ceee960c922c3d50d8514067/matplotlib-3.10.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa3854b5f9473564ef40a41bc922be978fab217776e9ae1545c9b3a5cf2092a3", size = 8453422 }, + { url = "https://files.pythonhosted.org/packages/51/d0/2bc4368abf766203e548dc7ab57cf7e9c621f1a3c72b516cc7715347b179/matplotlib-3.10.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e496c01441be4c7d5f96d4e40f7fca06e20dcb40e44c8daa2e740e1757ad9e6", size = 8596819 }, + { url = "https://files.pythonhosted.org/packages/ab/1b/8b350f8a1746c37ab69dda7d7528d1fc696efb06db6ade9727b7887be16d/matplotlib-3.10.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5d45d3f5245be5b469843450617dcad9af75ca50568acf59997bed9311131a0b", size = 9402782 }, + { url = "https://files.pythonhosted.org/packages/89/06/f570373d24d93503988ba8d04f213a372fa1ce48381c5eb15da985728498/matplotlib-3.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:8e8e25b1209161d20dfe93037c8a7f7ca796ec9aa326e6e4588d8c4a5dd1e473", size = 8063812 }, + { url = "https://files.pythonhosted.org/packages/fc/e0/8c811a925b5a7ad75135f0e5af46408b78af88bbb02a1df775100ef9bfef/matplotlib-3.10.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:19b06241ad89c3ae9469e07d77efa87041eac65d78df4fcf9cac318028009b01", size = 8214021 }, + { url = "https://files.pythonhosted.org/packages/4a/34/319ec2139f68ba26da9d00fce2ff9f27679fb799a6c8e7358539801fd629/matplotlib-3.10.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01e63101ebb3014e6e9f80d9cf9ee361a8599ddca2c3e166c563628b39305dbb", size = 8090782 }, + { url = "https://files.pythonhosted.org/packages/77/ea/9812124ab9a99df5b2eec1110e9b2edc0b8f77039abf4c56e0a376e84a29/matplotlib-3.10.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f06bad951eea6422ac4e8bdebcf3a70c59ea0a03338c5d2b109f57b64eb3972", size = 8478901 }, + { url = "https://files.pythonhosted.org/packages/c9/db/b05bf463689134789b06dea85828f8ebe506fa1e37593f723b65b86c9582/matplotlib-3.10.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3dfb036f34873b46978f55e240cff7a239f6c4409eac62d8145bad3fc6ba5a3", size = 8613864 }, + { url = "https://files.pythonhosted.org/packages/c2/04/41ccec4409f3023a7576df3b5c025f1a8c8b81fbfe922ecfd837ac36e081/matplotlib-3.10.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dc6ab14a7ab3b4d813b88ba957fc05c79493a037f54e246162033591e770de6f", size = 9409487 }, + { url = "https://files.pythonhosted.org/packages/ac/c2/0d5aae823bdcc42cc99327ecdd4d28585e15ccd5218c453b7bcd827f3421/matplotlib-3.10.1-cp313-cp313t-win_amd64.whl", hash = "sha256:bc411ebd5889a78dabbc457b3fa153203e22248bfa6eedc6797be5df0164dbf9", size = 8134832 }, + { url = "https://files.pythonhosted.org/packages/c8/f6/10adb696d8cbeed2ab4c2e26ecf1c80dd3847bbf3891f4a0c362e0e08a5a/matplotlib-3.10.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:648406f1899f9a818cef8c0231b44dcfc4ff36f167101c3fd1c9151f24220fdc", size = 8158685 }, + { url = "https://files.pythonhosted.org/packages/3f/84/0603d917406072763e7f9bb37747d3d74d7ecd4b943a8c947cc3ae1cf7af/matplotlib-3.10.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:02582304e352f40520727984a5a18f37e8187861f954fea9be7ef06569cf85b4", size = 8035491 }, + { url = "https://files.pythonhosted.org/packages/fd/7d/6a8b31dd07ed856b3eae001c9129670ef75c4698fa1c2a6ac9f00a4a7054/matplotlib-3.10.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3809916157ba871bcdd33d3493acd7fe3037db5daa917ca6e77975a94cef779", size = 8590087 }, +] + +[[package]] +name = "more-itertools" +version = "10.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/3b/7fa1fe835e2e93fd6d7b52b2f95ae810cf5ba133e1845f726f5a992d62c2/more-itertools-10.6.0.tar.gz", hash = "sha256:2cd7fad1009c31cc9fb6a035108509e6547547a7a738374f10bd49a09eb3ee3b", size = 125009 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/62/0fe302c6d1be1c777cab0616e6302478251dfbf9055ad426f5d0def75c89/more_itertools-10.6.0-py3-none-any.whl", hash = "sha256:6eb054cb4b6db1473f6e15fcc676a08e4732548acd47c708f0e179c2c7c01e89", size = 63038 }, +] + +[[package]] +name = "numpy" +version = "2.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/78/31103410a57bc2c2b93a3597340a8119588571f6a4539067546cb9a0bfac/numpy-2.2.4.tar.gz", hash = "sha256:9ba03692a45d3eef66559efe1d1096c4b9b75c0986b5dff5530c378fb8331d4f", size = 20270701 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/89/a79e86e5c1433926ed7d60cb267fb64aa578b6101ab645800fd43b4801de/numpy-2.2.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8146f3550d627252269ac42ae660281d673eb6f8b32f113538e0cc2a9aed42b9", size = 21250661 }, + { url = "https://files.pythonhosted.org/packages/79/c2/f50921beb8afd60ed9589ad880332cfefdb805422210d327fb48f12b7a81/numpy-2.2.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e642d86b8f956098b564a45e6f6ce68a22c2c97a04f5acd3f221f57b8cb850ae", size = 14389926 }, + { url = "https://files.pythonhosted.org/packages/c7/b9/2c4e96130b0b0f97b0ef4a06d6dae3b39d058b21a5e2fa2decd7fd6b1c8f/numpy-2.2.4-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:a84eda42bd12edc36eb5b53bbcc9b406820d3353f1994b6cfe453a33ff101775", size = 5428329 }, + { url = "https://files.pythonhosted.org/packages/7f/a5/3d7094aa898f4fc5c84cdfb26beeae780352d43f5d8bdec966c4393d644c/numpy-2.2.4-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:4ba5054787e89c59c593a4169830ab362ac2bee8a969249dc56e5d7d20ff8df9", size = 6963559 }, + { url = "https://files.pythonhosted.org/packages/4c/22/fb1be710a14434c09080dd4a0acc08939f612ec02efcb04b9e210474782d/numpy-2.2.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7716e4a9b7af82c06a2543c53ca476fa0b57e4d760481273e09da04b74ee6ee2", size = 14368066 }, + { url = "https://files.pythonhosted.org/packages/c2/07/2e5cc71193e3ef3a219ffcf6ca4858e46ea2be09c026ddd480d596b32867/numpy-2.2.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adf8c1d66f432ce577d0197dceaac2ac00c0759f573f28516246351c58a85020", size = 16417040 }, + { url = "https://files.pythonhosted.org/packages/1a/97/3b1537776ad9a6d1a41813818343745e8dd928a2916d4c9edcd9a8af1dac/numpy-2.2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:218f061d2faa73621fa23d6359442b0fc658d5b9a70801373625d958259eaca3", size = 15879862 }, + { url = "https://files.pythonhosted.org/packages/b0/b7/4472f603dd45ef36ff3d8e84e84fe02d9467c78f92cc121633dce6da307b/numpy-2.2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:df2f57871a96bbc1b69733cd4c51dc33bea66146b8c63cacbfed73eec0883017", size = 18206032 }, + { url = "https://files.pythonhosted.org/packages/0d/bd/6a092963fb82e6c5aa0d0440635827bbb2910da229545473bbb58c537ed3/numpy-2.2.4-cp310-cp310-win32.whl", hash = "sha256:a0258ad1f44f138b791327961caedffbf9612bfa504ab9597157806faa95194a", size = 6608517 }, + { url = "https://files.pythonhosted.org/packages/01/e3/cb04627bc2a1638948bc13e818df26495aa18e20d5be1ed95ab2b10b6847/numpy-2.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:0d54974f9cf14acf49c60f0f7f4084b6579d24d439453d5fc5805d46a165b542", size = 12943498 }, + { url = "https://files.pythonhosted.org/packages/16/fb/09e778ee3a8ea0d4dc8329cca0a9c9e65fed847d08e37eba74cb7ed4b252/numpy-2.2.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e9e0a277bb2eb5d8a7407e14688b85fd8ad628ee4e0c7930415687b6564207a4", size = 21254989 }, + { url = "https://files.pythonhosted.org/packages/a2/0a/1212befdbecab5d80eca3cde47d304cad986ad4eec7d85a42e0b6d2cc2ef/numpy-2.2.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9eeea959168ea555e556b8188da5fa7831e21d91ce031e95ce23747b7609f8a4", size = 14425910 }, + { url = "https://files.pythonhosted.org/packages/2b/3e/e7247c1d4f15086bb106c8d43c925b0b2ea20270224f5186fa48d4fb5cbd/numpy-2.2.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bd3ad3b0a40e713fc68f99ecfd07124195333f1e689387c180813f0e94309d6f", size = 5426490 }, + { url = "https://files.pythonhosted.org/packages/5d/fa/aa7cd6be51419b894c5787a8a93c3302a1ed4f82d35beb0613ec15bdd0e2/numpy-2.2.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cf28633d64294969c019c6df4ff37f5698e8326db68cc2b66576a51fad634880", size = 6967754 }, + { url = "https://files.pythonhosted.org/packages/d5/ee/96457c943265de9fadeb3d2ffdbab003f7fba13d971084a9876affcda095/numpy-2.2.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fa8fa7697ad1646b5c93de1719965844e004fcad23c91228aca1cf0800044a1", size = 14373079 }, + { url = "https://files.pythonhosted.org/packages/c5/5c/ceefca458559f0ccc7a982319f37ed07b0d7b526964ae6cc61f8ad1b6119/numpy-2.2.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4162988a360a29af158aeb4a2f4f09ffed6a969c9776f8f3bdee9b06a8ab7e5", size = 16428819 }, + { url = "https://files.pythonhosted.org/packages/22/31/9b2ac8eee99e001eb6add9fa27514ef5e9faf176169057a12860af52704c/numpy-2.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:892c10d6a73e0f14935c31229e03325a7b3093fafd6ce0af704be7f894d95687", size = 15881470 }, + { url = "https://files.pythonhosted.org/packages/f0/dc/8569b5f25ff30484b555ad8a3f537e0225d091abec386c9420cf5f7a2976/numpy-2.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db1f1c22173ac1c58db249ae48aa7ead29f534b9a948bc56828337aa84a32ed6", size = 18218144 }, + { url = "https://files.pythonhosted.org/packages/5e/05/463c023a39bdeb9bb43a99e7dee2c664cb68d5bb87d14f92482b9f6011cc/numpy-2.2.4-cp311-cp311-win32.whl", hash = "sha256:ea2bb7e2ae9e37d96835b3576a4fa4b3a97592fbea8ef7c3587078b0068b8f09", size = 6606368 }, + { url = "https://files.pythonhosted.org/packages/8b/72/10c1d2d82101c468a28adc35de6c77b308f288cfd0b88e1070f15b98e00c/numpy-2.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:f7de08cbe5551911886d1ab60de58448c6df0f67d9feb7d1fb21e9875ef95e91", size = 12947526 }, + { url = "https://files.pythonhosted.org/packages/a2/30/182db21d4f2a95904cec1a6f779479ea1ac07c0647f064dea454ec650c42/numpy-2.2.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a7b9084668aa0f64e64bd00d27ba5146ef1c3a8835f3bd912e7a9e01326804c4", size = 20947156 }, + { url = "https://files.pythonhosted.org/packages/24/6d/9483566acfbda6c62c6bc74b6e981c777229d2af93c8eb2469b26ac1b7bc/numpy-2.2.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dbe512c511956b893d2dacd007d955a3f03d555ae05cfa3ff1c1ff6df8851854", size = 14133092 }, + { url = "https://files.pythonhosted.org/packages/27/f6/dba8a258acbf9d2bed2525cdcbb9493ef9bae5199d7a9cb92ee7e9b2aea6/numpy-2.2.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:bb649f8b207ab07caebba230d851b579a3c8711a851d29efe15008e31bb4de24", size = 5163515 }, + { url = "https://files.pythonhosted.org/packages/62/30/82116199d1c249446723c68f2c9da40d7f062551036f50b8c4caa42ae252/numpy-2.2.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:f34dc300df798742b3d06515aa2a0aee20941c13579d7a2f2e10af01ae4901ee", size = 6696558 }, + { url = "https://files.pythonhosted.org/packages/0e/b2/54122b3c6df5df3e87582b2e9430f1bdb63af4023c739ba300164c9ae503/numpy-2.2.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3f7ac96b16955634e223b579a3e5798df59007ca43e8d451a0e6a50f6bfdfba", size = 14084742 }, + { url = "https://files.pythonhosted.org/packages/02/e2/e2cbb8d634151aab9528ef7b8bab52ee4ab10e076509285602c2a3a686e0/numpy-2.2.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f92084defa704deadd4e0a5ab1dc52d8ac9e8a8ef617f3fbb853e79b0ea3592", size = 16134051 }, + { url = "https://files.pythonhosted.org/packages/8e/21/efd47800e4affc993e8be50c1b768de038363dd88865920439ef7b422c60/numpy-2.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4e84a6283b36632e2a5b56e121961f6542ab886bc9e12f8f9818b3c266bfbb", size = 15578972 }, + { url = "https://files.pythonhosted.org/packages/04/1e/f8bb88f6157045dd5d9b27ccf433d016981032690969aa5c19e332b138c0/numpy-2.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:11c43995255eb4127115956495f43e9343736edb7fcdb0d973defd9de14cd84f", size = 17898106 }, + { url = "https://files.pythonhosted.org/packages/2b/93/df59a5a3897c1f036ae8ff845e45f4081bb06943039ae28a3c1c7c780f22/numpy-2.2.4-cp312-cp312-win32.whl", hash = "sha256:65ef3468b53269eb5fdb3a5c09508c032b793da03251d5f8722b1194f1790c00", size = 6311190 }, + { url = "https://files.pythonhosted.org/packages/46/69/8c4f928741c2a8efa255fdc7e9097527c6dc4e4df147e3cadc5d9357ce85/numpy-2.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:2aad3c17ed2ff455b8eaafe06bcdae0062a1db77cb99f4b9cbb5f4ecb13c5146", size = 12644305 }, + { url = "https://files.pythonhosted.org/packages/2a/d0/bd5ad792e78017f5decfb2ecc947422a3669a34f775679a76317af671ffc/numpy-2.2.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cf4e5c6a278d620dee9ddeb487dc6a860f9b199eadeecc567f777daace1e9e7", size = 20933623 }, + { url = "https://files.pythonhosted.org/packages/c3/bc/2b3545766337b95409868f8e62053135bdc7fa2ce630aba983a2aa60b559/numpy-2.2.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1974afec0b479e50438fc3648974268f972e2d908ddb6d7fb634598cdb8260a0", size = 14148681 }, + { url = "https://files.pythonhosted.org/packages/6a/70/67b24d68a56551d43a6ec9fe8c5f91b526d4c1a46a6387b956bf2d64744e/numpy-2.2.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:79bd5f0a02aa16808fcbc79a9a376a147cc1045f7dfe44c6e7d53fa8b8a79392", size = 5148759 }, + { url = "https://files.pythonhosted.org/packages/1c/8b/e2fc8a75fcb7be12d90b31477c9356c0cbb44abce7ffb36be39a0017afad/numpy-2.2.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:3387dd7232804b341165cedcb90694565a6015433ee076c6754775e85d86f1fc", size = 6683092 }, + { url = "https://files.pythonhosted.org/packages/13/73/41b7b27f169ecf368b52533edb72e56a133f9e86256e809e169362553b49/numpy-2.2.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f527d8fdb0286fd2fd97a2a96c6be17ba4232da346931d967a0630050dfd298", size = 14081422 }, + { url = "https://files.pythonhosted.org/packages/4b/04/e208ff3ae3ddfbafc05910f89546382f15a3f10186b1f56bd99f159689c2/numpy-2.2.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bce43e386c16898b91e162e5baaad90c4b06f9dcbe36282490032cec98dc8ae7", size = 16132202 }, + { url = "https://files.pythonhosted.org/packages/fe/bc/2218160574d862d5e55f803d88ddcad88beff94791f9c5f86d67bd8fbf1c/numpy-2.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31504f970f563d99f71a3512d0c01a645b692b12a63630d6aafa0939e52361e6", size = 15573131 }, + { url = "https://files.pythonhosted.org/packages/a5/78/97c775bc4f05abc8a8426436b7cb1be806a02a2994b195945600855e3a25/numpy-2.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:81413336ef121a6ba746892fad881a83351ee3e1e4011f52e97fba79233611fd", size = 17894270 }, + { url = "https://files.pythonhosted.org/packages/b9/eb/38c06217a5f6de27dcb41524ca95a44e395e6a1decdc0c99fec0832ce6ae/numpy-2.2.4-cp313-cp313-win32.whl", hash = "sha256:f486038e44caa08dbd97275a9a35a283a8f1d2f0ee60ac260a1790e76660833c", size = 6308141 }, + { url = "https://files.pythonhosted.org/packages/52/17/d0dd10ab6d125c6d11ffb6dfa3423c3571befab8358d4f85cd4471964fcd/numpy-2.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:207a2b8441cc8b6a2a78c9ddc64d00d20c303d79fba08c577752f080c4007ee3", size = 12636885 }, + { url = "https://files.pythonhosted.org/packages/fa/e2/793288ede17a0fdc921172916efb40f3cbc2aa97e76c5c84aba6dc7e8747/numpy-2.2.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8120575cb4882318c791f839a4fd66161a6fa46f3f0a5e613071aae35b5dd8f8", size = 20961829 }, + { url = "https://files.pythonhosted.org/packages/3a/75/bb4573f6c462afd1ea5cbedcc362fe3e9bdbcc57aefd37c681be1155fbaa/numpy-2.2.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a761ba0fa886a7bb33c6c8f6f20213735cb19642c580a931c625ee377ee8bd39", size = 14161419 }, + { url = "https://files.pythonhosted.org/packages/03/68/07b4cd01090ca46c7a336958b413cdbe75002286295f2addea767b7f16c9/numpy-2.2.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:ac0280f1ba4a4bfff363a99a6aceed4f8e123f8a9b234c89140f5e894e452ecd", size = 5196414 }, + { url = "https://files.pythonhosted.org/packages/a5/fd/d4a29478d622fedff5c4b4b4cedfc37a00691079623c0575978d2446db9e/numpy-2.2.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:879cf3a9a2b53a4672a168c21375166171bc3932b7e21f622201811c43cdd3b0", size = 6709379 }, + { url = "https://files.pythonhosted.org/packages/41/78/96dddb75bb9be730b87c72f30ffdd62611aba234e4e460576a068c98eff6/numpy-2.2.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f05d4198c1bacc9124018109c5fba2f3201dbe7ab6e92ff100494f236209c960", size = 14051725 }, + { url = "https://files.pythonhosted.org/packages/00/06/5306b8199bffac2a29d9119c11f457f6c7d41115a335b78d3f86fad4dbe8/numpy-2.2.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f085ce2e813a50dfd0e01fbfc0c12bbe5d2063d99f8b29da30e544fb6483b8", size = 16101638 }, + { url = "https://files.pythonhosted.org/packages/fa/03/74c5b631ee1ded596945c12027649e6344614144369fd3ec1aaced782882/numpy-2.2.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:92bda934a791c01d6d9d8e038363c50918ef7c40601552a58ac84c9613a665bc", size = 15571717 }, + { url = "https://files.pythonhosted.org/packages/cb/dc/4fc7c0283abe0981e3b89f9b332a134e237dd476b0c018e1e21083310c31/numpy-2.2.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ee4d528022f4c5ff67332469e10efe06a267e32f4067dc76bb7e2cddf3cd25ff", size = 17879998 }, + { url = "https://files.pythonhosted.org/packages/e5/2b/878576190c5cfa29ed896b518cc516aecc7c98a919e20706c12480465f43/numpy-2.2.4-cp313-cp313t-win32.whl", hash = "sha256:05c076d531e9998e7e694c36e8b349969c56eadd2cdcd07242958489d79a7286", size = 6366896 }, + { url = "https://files.pythonhosted.org/packages/3e/05/eb7eec66b95cf697f08c754ef26c3549d03ebd682819f794cb039574a0a6/numpy-2.2.4-cp313-cp313t-win_amd64.whl", hash = "sha256:188dcbca89834cc2e14eb2f106c96d6d46f200fe0200310fc29089657379c58d", size = 12739119 }, + { url = "https://files.pythonhosted.org/packages/b2/5c/f09c33a511aff41a098e6ef3498465d95f6360621034a3d95f47edbc9119/numpy-2.2.4-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7051ee569db5fbac144335e0f3b9c2337e0c8d5c9fee015f259a5bd70772b7e8", size = 21081956 }, + { url = "https://files.pythonhosted.org/packages/ba/30/74c48b3b6494c4b820b7fa1781d441e94d87a08daa5b35d222f06ba41a6f/numpy-2.2.4-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:ab2939cd5bec30a7430cbdb2287b63151b77cf9624de0532d629c9a1c59b1d5c", size = 6827143 }, + { url = "https://files.pythonhosted.org/packages/54/f5/ab0d2f48b490535c7a80e05da4a98902b632369efc04f0e47bb31ca97d8f/numpy-2.2.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0f35b19894a9e08639fd60a1ec1978cb7f5f7f1eace62f38dd36be8aecdef4d", size = 16233350 }, + { url = "https://files.pythonhosted.org/packages/3b/3a/2f6d8c1f8e45d496bca6baaec93208035faeb40d5735c25afac092ec9a12/numpy-2.2.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b4adfbbc64014976d2f91084915ca4e626fbf2057fb81af209c1a6d776d23e3d", size = 12857565 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "pandas" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/70/c853aec59839bceed032d52010ff5f1b8d87dc3114b762e4ba2727661a3b/pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5", size = 12580827 }, + { url = "https://files.pythonhosted.org/packages/99/f2/c4527768739ffa4469b2b4fff05aa3768a478aed89a2f271a79a40eee984/pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348", size = 11303897 }, + { url = "https://files.pythonhosted.org/packages/ed/12/86c1747ea27989d7a4064f806ce2bae2c6d575b950be087837bdfcabacc9/pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed", size = 66480908 }, + { url = "https://files.pythonhosted.org/packages/44/50/7db2cd5e6373ae796f0ddad3675268c8d59fb6076e66f0c339d61cea886b/pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57", size = 13064210 }, + { url = "https://files.pythonhosted.org/packages/61/61/a89015a6d5536cb0d6c3ba02cebed51a95538cf83472975275e28ebf7d0c/pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42", size = 16754292 }, + { url = "https://files.pythonhosted.org/packages/ce/0d/4cc7b69ce37fac07645a94e1d4b0880b15999494372c1523508511b09e40/pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f", size = 14416379 }, + { url = "https://files.pythonhosted.org/packages/31/9e/6ebb433de864a6cd45716af52a4d7a8c3c9aaf3a98368e61db9e69e69a9c/pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645", size = 11598471 }, + { url = "https://files.pythonhosted.org/packages/a8/44/d9502bf0ed197ba9bf1103c9867d5904ddcaf869e52329787fc54ed70cc8/pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039", size = 12602222 }, + { url = "https://files.pythonhosted.org/packages/52/11/9eac327a38834f162b8250aab32a6781339c69afe7574368fffe46387edf/pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd", size = 11321274 }, + { url = "https://files.pythonhosted.org/packages/45/fb/c4beeb084718598ba19aa9f5abbc8aed8b42f90930da861fcb1acdb54c3a/pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698", size = 15579836 }, + { url = "https://files.pythonhosted.org/packages/cd/5f/4dba1d39bb9c38d574a9a22548c540177f78ea47b32f99c0ff2ec499fac5/pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc", size = 13058505 }, + { url = "https://files.pythonhosted.org/packages/b9/57/708135b90391995361636634df1f1130d03ba456e95bcf576fada459115a/pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3", size = 16744420 }, + { url = "https://files.pythonhosted.org/packages/86/4a/03ed6b7ee323cf30404265c284cee9c65c56a212e0a08d9ee06984ba2240/pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32", size = 14440457 }, + { url = "https://files.pythonhosted.org/packages/ed/8c/87ddf1fcb55d11f9f847e3c69bb1c6f8e46e2f40ab1a2d2abadb2401b007/pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5", size = 11617166 }, + { url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893 }, + { url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475 }, + { url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645 }, + { url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445 }, + { url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235 }, + { url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756 }, + { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248 }, + { url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643 }, + { url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573 }, + { url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085 }, + { url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809 }, + { url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316 }, + { url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055 }, + { url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175 }, + { url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650 }, + { url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177 }, + { url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526 }, + { url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013 }, + { url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620 }, + { url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436 }, +] + +[[package]] +name = "parse" +version = "1.20.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/78/d9b09ba24bb36ef8b83b71be547e118d46214735b6dfb39e4bfde0e9b9dd/parse-1.20.2.tar.gz", hash = "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce", size = 29391 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/31/ba45bf0b2aa7898d81cbbfac0e88c267befb59ad91a19e36e1bc5578ddb1/parse-1.20.2-py2.py3-none-any.whl", hash = "sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558", size = 20126 }, +] + +[[package]] +name = "pillow" +version = "11.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/af/c097e544e7bd278333db77933e535098c259609c4eb3b85381109602fb5b/pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20", size = 46742715 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/1c/2dcea34ac3d7bc96a1fd1bd0a6e06a57c67167fec2cff8d95d88229a8817/pillow-11.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:e1abe69aca89514737465752b4bcaf8016de61b3be1397a8fc260ba33321b3a8", size = 3229983 }, + { url = "https://files.pythonhosted.org/packages/14/ca/6bec3df25e4c88432681de94a3531cc738bd85dea6c7aa6ab6f81ad8bd11/pillow-11.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c640e5a06869c75994624551f45e5506e4256562ead981cce820d5ab39ae2192", size = 3101831 }, + { url = "https://files.pythonhosted.org/packages/d4/2c/668e18e5521e46eb9667b09e501d8e07049eb5bfe39d56be0724a43117e6/pillow-11.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a07dba04c5e22824816b2615ad7a7484432d7f540e6fa86af60d2de57b0fcee2", size = 4314074 }, + { url = "https://files.pythonhosted.org/packages/02/80/79f99b714f0fc25f6a8499ecfd1f810df12aec170ea1e32a4f75746051ce/pillow-11.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e267b0ed063341f3e60acd25c05200df4193e15a4a5807075cd71225a2386e26", size = 4394933 }, + { url = "https://files.pythonhosted.org/packages/81/aa/8d4ad25dc11fd10a2001d5b8a80fdc0e564ac33b293bdfe04ed387e0fd95/pillow-11.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd165131fd51697e22421d0e467997ad31621b74bfc0b75956608cb2906dda07", size = 4353349 }, + { url = "https://files.pythonhosted.org/packages/84/7a/cd0c3eaf4a28cb2a74bdd19129f7726277a7f30c4f8424cd27a62987d864/pillow-11.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:abc56501c3fd148d60659aae0af6ddc149660469082859fa7b066a298bde9482", size = 4476532 }, + { url = "https://files.pythonhosted.org/packages/8f/8b/a907fdd3ae8f01c7670dfb1499c53c28e217c338b47a813af8d815e7ce97/pillow-11.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:54ce1c9a16a9561b6d6d8cb30089ab1e5eb66918cb47d457bd996ef34182922e", size = 4279789 }, + { url = "https://files.pythonhosted.org/packages/6f/9a/9f139d9e8cccd661c3efbf6898967a9a337eb2e9be2b454ba0a09533100d/pillow-11.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:73ddde795ee9b06257dac5ad42fcb07f3b9b813f8c1f7f870f402f4dc54b5269", size = 4413131 }, + { url = "https://files.pythonhosted.org/packages/a8/68/0d8d461f42a3f37432203c8e6df94da10ac8081b6d35af1c203bf3111088/pillow-11.1.0-cp310-cp310-win32.whl", hash = "sha256:3a5fe20a7b66e8135d7fd617b13272626a28278d0e578c98720d9ba4b2439d49", size = 2291213 }, + { url = "https://files.pythonhosted.org/packages/14/81/d0dff759a74ba87715509af9f6cb21fa21d93b02b3316ed43bda83664db9/pillow-11.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:b6123aa4a59d75f06e9dd3dac5bf8bc9aa383121bb3dd9a7a612e05eabc9961a", size = 2625725 }, + { url = "https://files.pythonhosted.org/packages/ce/1f/8d50c096a1d58ef0584ddc37e6f602828515219e9d2428e14ce50f5ecad1/pillow-11.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:a76da0a31da6fcae4210aa94fd779c65c75786bc9af06289cd1c184451ef7a65", size = 2375213 }, + { url = "https://files.pythonhosted.org/packages/dd/d6/2000bfd8d5414fb70cbbe52c8332f2283ff30ed66a9cde42716c8ecbe22c/pillow-11.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457", size = 3229968 }, + { url = "https://files.pythonhosted.org/packages/d9/45/3fe487010dd9ce0a06adf9b8ff4f273cc0a44536e234b0fad3532a42c15b/pillow-11.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35", size = 3101806 }, + { url = "https://files.pythonhosted.org/packages/e3/72/776b3629c47d9d5f1c160113158a7a7ad177688d3a1159cd3b62ded5a33a/pillow-11.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2", size = 4322283 }, + { url = "https://files.pythonhosted.org/packages/e4/c2/e25199e7e4e71d64eeb869f5b72c7ddec70e0a87926398785ab944d92375/pillow-11.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070", size = 4402945 }, + { url = "https://files.pythonhosted.org/packages/c1/ed/51d6136c9d5911f78632b1b86c45241c712c5a80ed7fa7f9120a5dff1eba/pillow-11.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6", size = 4361228 }, + { url = "https://files.pythonhosted.org/packages/48/a4/fbfe9d5581d7b111b28f1d8c2762dee92e9821bb209af9fa83c940e507a0/pillow-11.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1", size = 4484021 }, + { url = "https://files.pythonhosted.org/packages/39/db/0b3c1a5018117f3c1d4df671fb8e47d08937f27519e8614bbe86153b65a5/pillow-11.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2", size = 4287449 }, + { url = "https://files.pythonhosted.org/packages/d9/58/bc128da7fea8c89fc85e09f773c4901e95b5936000e6f303222490c052f3/pillow-11.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96", size = 4419972 }, + { url = "https://files.pythonhosted.org/packages/5f/bb/58f34379bde9fe197f51841c5bbe8830c28bbb6d3801f16a83b8f2ad37df/pillow-11.1.0-cp311-cp311-win32.whl", hash = "sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f", size = 2291201 }, + { url = "https://files.pythonhosted.org/packages/3a/c6/fce9255272bcf0c39e15abd2f8fd8429a954cf344469eaceb9d0d1366913/pillow-11.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761", size = 2625686 }, + { url = "https://files.pythonhosted.org/packages/c8/52/8ba066d569d932365509054859f74f2a9abee273edcef5cd75e4bc3e831e/pillow-11.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71", size = 2375194 }, + { url = "https://files.pythonhosted.org/packages/95/20/9ce6ed62c91c073fcaa23d216e68289e19d95fb8188b9fb7a63d36771db8/pillow-11.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a", size = 3226818 }, + { url = "https://files.pythonhosted.org/packages/b9/d8/f6004d98579a2596c098d1e30d10b248798cceff82d2b77aa914875bfea1/pillow-11.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b", size = 3101662 }, + { url = "https://files.pythonhosted.org/packages/08/d9/892e705f90051c7a2574d9f24579c9e100c828700d78a63239676f960b74/pillow-11.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3", size = 4329317 }, + { url = "https://files.pythonhosted.org/packages/8c/aa/7f29711f26680eab0bcd3ecdd6d23ed6bce180d82e3f6380fb7ae35fcf3b/pillow-11.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a", size = 4412999 }, + { url = "https://files.pythonhosted.org/packages/c8/c4/8f0fe3b9e0f7196f6d0bbb151f9fba323d72a41da068610c4c960b16632a/pillow-11.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1", size = 4368819 }, + { url = "https://files.pythonhosted.org/packages/38/0d/84200ed6a871ce386ddc82904bfadc0c6b28b0c0ec78176871a4679e40b3/pillow-11.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f", size = 4496081 }, + { url = "https://files.pythonhosted.org/packages/84/9c/9bcd66f714d7e25b64118e3952d52841a4babc6d97b6d28e2261c52045d4/pillow-11.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91", size = 4296513 }, + { url = "https://files.pythonhosted.org/packages/db/61/ada2a226e22da011b45f7104c95ebda1b63dcbb0c378ad0f7c2a710f8fd2/pillow-11.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c", size = 4431298 }, + { url = "https://files.pythonhosted.org/packages/e7/c4/fc6e86750523f367923522014b821c11ebc5ad402e659d8c9d09b3c9d70c/pillow-11.1.0-cp312-cp312-win32.whl", hash = "sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6", size = 2291630 }, + { url = "https://files.pythonhosted.org/packages/08/5c/2104299949b9d504baf3f4d35f73dbd14ef31bbd1ddc2c1b66a5b7dfda44/pillow-11.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf", size = 2626369 }, + { url = "https://files.pythonhosted.org/packages/37/f3/9b18362206b244167c958984b57c7f70a0289bfb59a530dd8af5f699b910/pillow-11.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5", size = 2375240 }, + { url = "https://files.pythonhosted.org/packages/b3/31/9ca79cafdce364fd5c980cd3416c20ce1bebd235b470d262f9d24d810184/pillow-11.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc", size = 3226640 }, + { url = "https://files.pythonhosted.org/packages/ac/0f/ff07ad45a1f172a497aa393b13a9d81a32e1477ef0e869d030e3c1532521/pillow-11.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0", size = 3101437 }, + { url = "https://files.pythonhosted.org/packages/08/2f/9906fca87a68d29ec4530be1f893149e0cb64a86d1f9f70a7cfcdfe8ae44/pillow-11.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1", size = 4326605 }, + { url = "https://files.pythonhosted.org/packages/b0/0f/f3547ee15b145bc5c8b336401b2d4c9d9da67da9dcb572d7c0d4103d2c69/pillow-11.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec", size = 4411173 }, + { url = "https://files.pythonhosted.org/packages/b1/df/bf8176aa5db515c5de584c5e00df9bab0713548fd780c82a86cba2c2fedb/pillow-11.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5", size = 4369145 }, + { url = "https://files.pythonhosted.org/packages/de/7c/7433122d1cfadc740f577cb55526fdc39129a648ac65ce64db2eb7209277/pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114", size = 4496340 }, + { url = "https://files.pythonhosted.org/packages/25/46/dd94b93ca6bd555588835f2504bd90c00d5438fe131cf01cfa0c5131a19d/pillow-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352", size = 4296906 }, + { url = "https://files.pythonhosted.org/packages/a8/28/2f9d32014dfc7753e586db9add35b8a41b7a3b46540e965cb6d6bc607bd2/pillow-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3", size = 4431759 }, + { url = "https://files.pythonhosted.org/packages/33/48/19c2cbe7403870fbe8b7737d19eb013f46299cdfe4501573367f6396c775/pillow-11.1.0-cp313-cp313-win32.whl", hash = "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9", size = 2291657 }, + { url = "https://files.pythonhosted.org/packages/3b/ad/285c556747d34c399f332ba7c1a595ba245796ef3e22eae190f5364bb62b/pillow-11.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c", size = 2626304 }, + { url = "https://files.pythonhosted.org/packages/e5/7b/ef35a71163bf36db06e9c8729608f78dedf032fc8313d19bd4be5c2588f3/pillow-11.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65", size = 2375117 }, + { url = "https://files.pythonhosted.org/packages/79/30/77f54228401e84d6791354888549b45824ab0ffde659bafa67956303a09f/pillow-11.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861", size = 3230060 }, + { url = "https://files.pythonhosted.org/packages/ce/b1/56723b74b07dd64c1010fee011951ea9c35a43d8020acd03111f14298225/pillow-11.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081", size = 3106192 }, + { url = "https://files.pythonhosted.org/packages/e1/cd/7bf7180e08f80a4dcc6b4c3a0aa9e0b0ae57168562726a05dc8aa8fa66b0/pillow-11.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c", size = 4446805 }, + { url = "https://files.pythonhosted.org/packages/97/42/87c856ea30c8ed97e8efbe672b58c8304dee0573f8c7cab62ae9e31db6ae/pillow-11.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547", size = 4530623 }, + { url = "https://files.pythonhosted.org/packages/ff/41/026879e90c84a88e33fb00cc6bd915ac2743c67e87a18f80270dfe3c2041/pillow-11.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab", size = 4465191 }, + { url = "https://files.pythonhosted.org/packages/e5/fb/a7960e838bc5df57a2ce23183bfd2290d97c33028b96bde332a9057834d3/pillow-11.1.0-cp313-cp313t-win32.whl", hash = "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9", size = 2295494 }, + { url = "https://files.pythonhosted.org/packages/d7/6c/6ec83ee2f6f0fda8d4cf89045c6be4b0373ebfc363ba8538f8c999f63fcd/pillow-11.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe", size = 2631595 }, + { url = "https://files.pythonhosted.org/packages/cf/6c/41c21c6c8af92b9fea313aa47c75de49e2f9a467964ee33eb0135d47eb64/pillow-11.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756", size = 2377651 }, + { url = "https://files.pythonhosted.org/packages/fa/c5/389961578fb677b8b3244fcd934f720ed25a148b9a5cc81c91bdf59d8588/pillow-11.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8c730dc3a83e5ac137fbc92dfcfe1511ce3b2b5d7578315b63dbbb76f7f51d90", size = 3198345 }, + { url = "https://files.pythonhosted.org/packages/c4/fa/803c0e50ffee74d4b965229e816af55276eac1d5806712de86f9371858fd/pillow-11.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:7d33d2fae0e8b170b6a6c57400e077412240f6f5bb2a342cf1ee512a787942bb", size = 3072938 }, + { url = "https://files.pythonhosted.org/packages/dc/67/2a3a5f8012b5d8c63fe53958ba906c1b1d0482ebed5618057ef4d22f8076/pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8d65b38173085f24bc07f8b6c505cbb7418009fa1a1fcb111b1f4961814a442", size = 3400049 }, + { url = "https://files.pythonhosted.org/packages/e5/a0/514f0d317446c98c478d1872497eb92e7cde67003fed74f696441e647446/pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:015c6e863faa4779251436db398ae75051469f7c903b043a48f078e437656f83", size = 3422431 }, + { url = "https://files.pythonhosted.org/packages/cd/00/20f40a935514037b7d3f87adfc87d2c538430ea625b63b3af8c3f5578e72/pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d44ff19eea13ae4acdaaab0179fa68c0c6f2f45d66a4d8ec1eda7d6cecbcc15f", size = 3446208 }, + { url = "https://files.pythonhosted.org/packages/28/3c/7de681727963043e093c72e6c3348411b0185eab3263100d4490234ba2f6/pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d3d8da4a631471dfaf94c10c85f5277b1f8e42ac42bade1ac67da4b4a7359b73", size = 3509746 }, + { url = "https://files.pythonhosted.org/packages/41/67/936f9814bdd74b2dfd4822f1f7725ab5d8ff4103919a1664eb4874c58b2f/pillow-11.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4637b88343166249fe8aa94e7c4a62a180c4b3898283bb5d3d2fd5fe10d8e4e0", size = 2626353 }, +] + +[[package]] +name = "psutil" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051 }, + { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535 }, + { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004 }, + { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986 }, + { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544 }, + { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053 }, + { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885 }, +] + +[[package]] +name = "pymoo" +version = "0.6.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alive-progress" }, + { name = "autograd" }, + { name = "cma" }, + { name = "deprecated" }, + { name = "dill" }, + { name = "matplotlib" }, + { name = "numpy" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/24/330304a9be3e75d45698f9d7c0ad34a5e6006979e5b125601c338d1cb4cf/pymoo-0.6.1.3.tar.gz", hash = "sha256:ab440986cbaede547125ca9d1545781fdee94b719488de44119a86b8e9af526e", size = 1285121 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/d2/2d3a644ca09f3c74c7e5bbf8119b7752166267f2c36cff7cd48abfe21eae/pymoo-0.6.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b50fc36aba4b863d80b275558f303e0bc7ea0e16aa207cd558f65246f527b742", size = 1578127 }, + { url = "https://files.pythonhosted.org/packages/aa/7a/40e569764f48e0a474180c1bc92350666a89c1010bcd07837970330dfffd/pymoo-0.6.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18086aeb45e478adb601c8d6c9d7e0166007256e53617d966e63e9bb7fce7c88", size = 4176056 }, + { url = "https://files.pythonhosted.org/packages/9a/4a/167e7842a5e92512b29ec9fd961806bd41c98a591e8c3532a4b7dd799df3/pymoo-0.6.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:f132285feb7f76ac44659685ffe130a9d8e9a9bd61edecbb7fd1c2d47a3ffa25", size = 909997 }, + { url = "https://files.pythonhosted.org/packages/24/d5/51808c9b220f5449379780e236813df7710fb9be53bf4089f8de0fc0c3f9/pymoo-0.6.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8426a8960a5be090e10fc0bbce0564041d9f2c8d016da6a8d8fdcdf1b6a369c3", size = 1577090 }, + { url = "https://files.pythonhosted.org/packages/43/b8/b709e68f663ef1aaf0187cfcb688de1c31e182a4f55ce90064acec451726/pymoo-0.6.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:959b478890416a0a9a452147110534195e28396ce43b5563c580118b267f4314", size = 4428017 }, + { url = "https://files.pythonhosted.org/packages/b6/28/b64db4ff471455e5342a24e0f009e19d75ca9d9994137466338433e11705/pymoo-0.6.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:8385cd634c1db433a3845c8f6e99241a09470e3a1f38f9e5a4194e35b35b90e2", size = 910367 }, + { url = "https://files.pythonhosted.org/packages/a9/a9/79932ce1606a7b2611a32eb75764538eec08e01e2f0785f4606700ace69c/pymoo-0.6.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:efee5682327dd7a12de2ed78e918f1e1fe1fd9cc1da86f806c432a499508b972", size = 1598241 }, + { url = "https://files.pythonhosted.org/packages/75/95/e63bf0fbddc5a3b1480a3cc97dee7b1281507e4d7831d9246dadd50adf44/pymoo-0.6.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c3f87a65c3ca0935ee38236042bcc287aa37a2acea18831e7f6c00bcd64bb5e7", size = 898163 }, + { url = "https://files.pythonhosted.org/packages/96/a7/e49901261ff73e1775858e3c2ef6ac96ddd4f1de8fac003365d2f8953d2d/pymoo-0.6.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:3500b1edeaa3394187a602259de66f51ae6c73372e2c83629a5ff705d76024f0", size = 914524 }, +] + +[[package]] +name = "pyparsing" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/1a/3544f4f299a47911c2ab3710f534e52fea62a633c96806995da5d25be4b2/pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a", size = 1067694 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/a7/c8a2d361bf89c0d9577c934ebb7421b25dc84bf3a8e3ac0a40aed9acc547/pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1", size = 107716 }, +] + +[[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 } +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 }, +] + +[[package]] +name = "pytz" +version = "2025.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/57/df1c9157c8d5a05117e455d66fd7cf6dbc46974f832b1058ed4856785d8a/pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e", size = 319617 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/38/ac33370d784287baa1c3d538978b5e2ea064d4c1b93ffbd12826c190dd10/pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57", size = 507930 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "scikit-learn" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/a5/4ae3b3a0755f7b35a280ac90b28817d1f380318973cff14075ab41ef50d9/scikit_learn-1.6.1.tar.gz", hash = "sha256:b4fc2525eca2c69a59260f583c56a7557c6ccdf8deafdba6e060f94c1c59738e", size = 7068312 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/3a/f4597eb41049110b21ebcbb0bcb43e4035017545daa5eedcfeb45c08b9c5/scikit_learn-1.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d056391530ccd1e501056160e3c9673b4da4805eb67eb2bdf4e983e1f9c9204e", size = 12067702 }, + { url = "https://files.pythonhosted.org/packages/37/19/0423e5e1fd1c6ec5be2352ba05a537a473c1677f8188b9306097d684b327/scikit_learn-1.6.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0c8d036eb937dbb568c6242fa598d551d88fb4399c0344d95c001980ec1c7d36", size = 11112765 }, + { url = "https://files.pythonhosted.org/packages/70/95/d5cb2297a835b0f5fc9a77042b0a2d029866379091ab8b3f52cc62277808/scikit_learn-1.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8634c4bd21a2a813e0a7e3900464e6d593162a29dd35d25bdf0103b3fce60ed5", size = 12643991 }, + { url = "https://files.pythonhosted.org/packages/b7/91/ab3c697188f224d658969f678be86b0968ccc52774c8ab4a86a07be13c25/scikit_learn-1.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:775da975a471c4f6f467725dff0ced5c7ac7bda5e9316b260225b48475279a1b", size = 13497182 }, + { url = "https://files.pythonhosted.org/packages/17/04/d5d556b6c88886c092cc989433b2bab62488e0f0dafe616a1d5c9cb0efb1/scikit_learn-1.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:8a600c31592bd7dab31e1c61b9bbd6dea1b3433e67d264d17ce1017dbdce8002", size = 11125517 }, + { url = "https://files.pythonhosted.org/packages/6c/2a/e291c29670795406a824567d1dfc91db7b699799a002fdaa452bceea8f6e/scikit_learn-1.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72abc587c75234935e97d09aa4913a82f7b03ee0b74111dcc2881cba3c5a7b33", size = 12102620 }, + { url = "https://files.pythonhosted.org/packages/25/92/ee1d7a00bb6b8c55755d4984fd82608603a3cc59959245068ce32e7fb808/scikit_learn-1.6.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:b3b00cdc8f1317b5f33191df1386c0befd16625f49d979fe77a8d44cae82410d", size = 11116234 }, + { url = "https://files.pythonhosted.org/packages/30/cd/ed4399485ef364bb25f388ab438e3724e60dc218c547a407b6e90ccccaef/scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc4765af3386811c3ca21638f63b9cf5ecf66261cc4815c1db3f1e7dc7b79db2", size = 12592155 }, + { url = "https://files.pythonhosted.org/packages/a8/f3/62fc9a5a659bb58a03cdd7e258956a5824bdc9b4bb3c5d932f55880be569/scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25fc636bdaf1cc2f4a124a116312d837148b5e10872147bdaf4887926b8c03d8", size = 13497069 }, + { url = "https://files.pythonhosted.org/packages/a1/a6/c5b78606743a1f28eae8f11973de6613a5ee87366796583fb74c67d54939/scikit_learn-1.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:fa909b1a36e000a03c382aade0bd2063fd5680ff8b8e501660c0f59f021a6415", size = 11139809 }, + { url = "https://files.pythonhosted.org/packages/0a/18/c797c9b8c10380d05616db3bfb48e2a3358c767affd0857d56c2eb501caa/scikit_learn-1.6.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:926f207c804104677af4857b2c609940b743d04c4c35ce0ddc8ff4f053cddc1b", size = 12104516 }, + { url = "https://files.pythonhosted.org/packages/c4/b7/2e35f8e289ab70108f8cbb2e7a2208f0575dc704749721286519dcf35f6f/scikit_learn-1.6.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2c2cae262064e6a9b77eee1c8e768fc46aa0b8338c6a8297b9b6759720ec0ff2", size = 11167837 }, + { url = "https://files.pythonhosted.org/packages/a4/f6/ff7beaeb644bcad72bcfd5a03ff36d32ee4e53a8b29a639f11bcb65d06cd/scikit_learn-1.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1061b7c028a8663fb9a1a1baf9317b64a257fcb036dae5c8752b2abef31d136f", size = 12253728 }, + { url = "https://files.pythonhosted.org/packages/29/7a/8bce8968883e9465de20be15542f4c7e221952441727c4dad24d534c6d99/scikit_learn-1.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e69fab4ebfc9c9b580a7a80111b43d214ab06250f8a7ef590a4edf72464dd86", size = 13147700 }, + { url = "https://files.pythonhosted.org/packages/62/27/585859e72e117fe861c2079bcba35591a84f801e21bc1ab85bce6ce60305/scikit_learn-1.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:70b1d7e85b1c96383f872a519b3375f92f14731e279a7b4c6cfd650cf5dffc52", size = 11110613 }, + { url = "https://files.pythonhosted.org/packages/2e/59/8eb1872ca87009bdcdb7f3cdc679ad557b992c12f4b61f9250659e592c63/scikit_learn-1.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ffa1e9e25b3d93990e74a4be2c2fc61ee5af85811562f1288d5d055880c4322", size = 12010001 }, + { url = "https://files.pythonhosted.org/packages/9d/05/f2fc4effc5b32e525408524c982c468c29d22f828834f0625c5ef3d601be/scikit_learn-1.6.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dc5cf3d68c5a20ad6d571584c0750ec641cc46aeef1c1507be51300e6003a7e1", size = 11096360 }, + { url = "https://files.pythonhosted.org/packages/c8/e4/4195d52cf4f113573fb8ebc44ed5a81bd511a92c0228889125fac2f4c3d1/scikit_learn-1.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c06beb2e839ecc641366000ca84f3cf6fa9faa1777e29cf0c04be6e4d096a348", size = 12209004 }, + { url = "https://files.pythonhosted.org/packages/94/be/47e16cdd1e7fcf97d95b3cb08bde1abb13e627861af427a3651fcb80b517/scikit_learn-1.6.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8ca8cb270fee8f1f76fa9bfd5c3507d60c6438bbee5687f81042e2bb98e5a97", size = 13171776 }, + { url = "https://files.pythonhosted.org/packages/34/b0/ca92b90859070a1487827dbc672f998da95ce83edce1270fc23f96f1f61a/scikit_learn-1.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:7a1c43c8ec9fde528d664d947dc4c0789be4077a3647f232869f41d9bf50e0fb", size = 11071865 }, + { url = "https://files.pythonhosted.org/packages/12/ae/993b0fb24a356e71e9a894e42b8a9eec528d4c70217353a1cd7a48bc25d4/scikit_learn-1.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a17c1dea1d56dcda2fac315712f3651a1fea86565b64b48fa1bc090249cbf236", size = 11955804 }, + { url = "https://files.pythonhosted.org/packages/d6/54/32fa2ee591af44507eac86406fa6bba968d1eb22831494470d0a2e4a1eb1/scikit_learn-1.6.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6a7aa5f9908f0f28f4edaa6963c0a6183f1911e63a69aa03782f0d924c830a35", size = 11100530 }, + { url = "https://files.pythonhosted.org/packages/3f/58/55856da1adec655bdce77b502e94a267bf40a8c0b89f8622837f89503b5a/scikit_learn-1.6.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0650e730afb87402baa88afbf31c07b84c98272622aaba002559b614600ca691", size = 12433852 }, + { url = "https://files.pythonhosted.org/packages/ff/4f/c83853af13901a574f8f13b645467285a48940f185b690936bb700a50863/scikit_learn-1.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:3f59fe08dc03ea158605170eb52b22a105f238a5d512c4470ddeca71feae8e5f", size = 11337256 }, +] + +[[package]] +name = "scipy" +version = "1.15.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/b9/31ba9cd990e626574baf93fbc1ac61cf9ed54faafd04c479117517661637/scipy-1.15.2.tar.gz", hash = "sha256:cd58a314d92838f7e6f755c8a2167ead4f27e1fd5c1251fd54289569ef3495ec", size = 59417316 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/df/ef233fff6838fe6f7840d69b5ef9f20d2b5c912a8727b21ebf876cb15d54/scipy-1.15.2-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a2ec871edaa863e8213ea5df811cd600734f6400b4af272e1c011e69401218e9", size = 38692502 }, + { url = "https://files.pythonhosted.org/packages/5c/20/acdd4efb8a68b842968f7bc5611b1aeb819794508771ad104de418701422/scipy-1.15.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:6f223753c6ea76983af380787611ae1291e3ceb23917393079dcc746ba60cfb5", size = 30085508 }, + { url = "https://files.pythonhosted.org/packages/42/55/39cf96ca7126f1e78ee72a6344ebdc6702fc47d037319ad93221063e6cf4/scipy-1.15.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:ecf797d2d798cf7c838c6d98321061eb3e72a74710e6c40540f0e8087e3b499e", size = 22359166 }, + { url = "https://files.pythonhosted.org/packages/51/48/708d26a4ab8a1441536bf2dfcad1df0ca14a69f010fba3ccbdfc02df7185/scipy-1.15.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:9b18aa747da280664642997e65aab1dd19d0c3d17068a04b3fe34e2559196cb9", size = 25112047 }, + { url = "https://files.pythonhosted.org/packages/dd/65/f9c5755b995ad892020381b8ae11f16d18616208e388621dfacc11df6de6/scipy-1.15.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87994da02e73549dfecaed9e09a4f9d58a045a053865679aeb8d6d43747d4df3", size = 35536214 }, + { url = "https://files.pythonhosted.org/packages/de/3c/c96d904b9892beec978562f64d8cc43f9cca0842e65bd3cd1b7f7389b0ba/scipy-1.15.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69ea6e56d00977f355c0f84eba69877b6df084516c602d93a33812aa04d90a3d", size = 37646981 }, + { url = "https://files.pythonhosted.org/packages/3d/74/c2d8a24d18acdeae69ed02e132b9bc1bb67b7bee90feee1afe05a68f9d67/scipy-1.15.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:888307125ea0c4466287191e5606a2c910963405ce9671448ff9c81c53f85f58", size = 37230048 }, + { url = "https://files.pythonhosted.org/packages/42/19/0aa4ce80eca82d487987eff0bc754f014dec10d20de2f66754fa4ea70204/scipy-1.15.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9412f5e408b397ff5641080ed1e798623dbe1ec0d78e72c9eca8992976fa65aa", size = 40010322 }, + { url = "https://files.pythonhosted.org/packages/d0/d2/f0683b7e992be44d1475cc144d1f1eeae63c73a14f862974b4db64af635e/scipy-1.15.2-cp310-cp310-win_amd64.whl", hash = "sha256:b5e025e903b4f166ea03b109bb241355b9c42c279ea694d8864d033727205e65", size = 41233385 }, + { url = "https://files.pythonhosted.org/packages/40/1f/bf0a5f338bda7c35c08b4ed0df797e7bafe8a78a97275e9f439aceb46193/scipy-1.15.2-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:92233b2df6938147be6fa8824b8136f29a18f016ecde986666be5f4d686a91a4", size = 38703651 }, + { url = "https://files.pythonhosted.org/packages/de/54/db126aad3874601048c2c20ae3d8a433dbfd7ba8381551e6f62606d9bd8e/scipy-1.15.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:62ca1ff3eb513e09ed17a5736929429189adf16d2d740f44e53270cc800ecff1", size = 30102038 }, + { url = "https://files.pythonhosted.org/packages/61/d8/84da3fffefb6c7d5a16968fe5b9f24c98606b165bb801bb0b8bc3985200f/scipy-1.15.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4c6676490ad76d1c2894d77f976144b41bd1a4052107902238047fb6a473e971", size = 22375518 }, + { url = "https://files.pythonhosted.org/packages/44/78/25535a6e63d3b9c4c90147371aedb5d04c72f3aee3a34451f2dc27c0c07f/scipy-1.15.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:a8bf5cb4a25046ac61d38f8d3c3426ec11ebc350246a4642f2f315fe95bda655", size = 25142523 }, + { url = "https://files.pythonhosted.org/packages/e0/22/4b4a26fe1cd9ed0bc2b2cb87b17d57e32ab72c346949eaf9288001f8aa8e/scipy-1.15.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a8e34cf4c188b6dd004654f88586d78f95639e48a25dfae9c5e34a6dc34547e", size = 35491547 }, + { url = "https://files.pythonhosted.org/packages/32/ea/564bacc26b676c06a00266a3f25fdfe91a9d9a2532ccea7ce6dd394541bc/scipy-1.15.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28a0d2c2075946346e4408b211240764759e0fabaeb08d871639b5f3b1aca8a0", size = 37634077 }, + { url = "https://files.pythonhosted.org/packages/43/c2/bfd4e60668897a303b0ffb7191e965a5da4056f0d98acfb6ba529678f0fb/scipy-1.15.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:42dabaaa798e987c425ed76062794e93a243be8f0f20fff6e7a89f4d61cb3d40", size = 37231657 }, + { url = "https://files.pythonhosted.org/packages/4a/75/5f13050bf4f84c931bcab4f4e83c212a36876c3c2244475db34e4b5fe1a6/scipy-1.15.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6f5e296ec63c5da6ba6fa0343ea73fd51b8b3e1a300b0a8cae3ed4b1122c7462", size = 40035857 }, + { url = "https://files.pythonhosted.org/packages/b9/8b/7ec1832b09dbc88f3db411f8cdd47db04505c4b72c99b11c920a8f0479c3/scipy-1.15.2-cp311-cp311-win_amd64.whl", hash = "sha256:597a0c7008b21c035831c39927406c6181bcf8f60a73f36219b69d010aa04737", size = 41217654 }, + { url = "https://files.pythonhosted.org/packages/4b/5d/3c78815cbab499610f26b5bae6aed33e227225a9fa5290008a733a64f6fc/scipy-1.15.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c4697a10da8f8765bb7c83e24a470da5797e37041edfd77fd95ba3811a47c4fd", size = 38756184 }, + { url = "https://files.pythonhosted.org/packages/37/20/3d04eb066b471b6e171827548b9ddb3c21c6bbea72a4d84fc5989933910b/scipy-1.15.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:869269b767d5ee7ea6991ed7e22b3ca1f22de73ab9a49c44bad338b725603301", size = 30163558 }, + { url = "https://files.pythonhosted.org/packages/a4/98/e5c964526c929ef1f795d4c343b2ff98634ad2051bd2bbadfef9e772e413/scipy-1.15.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:bad78d580270a4d32470563ea86c6590b465cb98f83d760ff5b0990cb5518a93", size = 22437211 }, + { url = "https://files.pythonhosted.org/packages/1d/cd/1dc7371e29195ecbf5222f9afeedb210e0a75057d8afbd942aa6cf8c8eca/scipy-1.15.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:b09ae80010f52efddb15551025f9016c910296cf70adbf03ce2a8704f3a5ad20", size = 25232260 }, + { url = "https://files.pythonhosted.org/packages/f0/24/1a181a9e5050090e0b5138c5f496fee33293c342b788d02586bc410c6477/scipy-1.15.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a6fd6eac1ce74a9f77a7fc724080d507c5812d61e72bd5e4c489b042455865e", size = 35198095 }, + { url = "https://files.pythonhosted.org/packages/c0/53/eaada1a414c026673eb983f8b4a55fe5eb172725d33d62c1b21f63ff6ca4/scipy-1.15.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b871df1fe1a3ba85d90e22742b93584f8d2b8e6124f8372ab15c71b73e428b8", size = 37297371 }, + { url = "https://files.pythonhosted.org/packages/e9/06/0449b744892ed22b7e7b9a1994a866e64895363572677a316a9042af1fe5/scipy-1.15.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:03205d57a28e18dfd39f0377d5002725bf1f19a46f444108c29bdb246b6c8a11", size = 36872390 }, + { url = "https://files.pythonhosted.org/packages/6a/6f/a8ac3cfd9505ec695c1bc35edc034d13afbd2fc1882a7c6b473e280397bb/scipy-1.15.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:601881dfb761311045b03114c5fe718a12634e5608c3b403737ae463c9885d53", size = 39700276 }, + { url = "https://files.pythonhosted.org/packages/f5/6f/e6e5aff77ea2a48dd96808bb51d7450875af154ee7cbe72188afb0b37929/scipy-1.15.2-cp312-cp312-win_amd64.whl", hash = "sha256:e7c68b6a43259ba0aab737237876e5c2c549a031ddb7abc28c7b47f22e202ded", size = 40942317 }, + { url = "https://files.pythonhosted.org/packages/53/40/09319f6e0f276ea2754196185f95cd191cb852288440ce035d5c3a931ea2/scipy-1.15.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01edfac9f0798ad6b46d9c4c9ca0e0ad23dbf0b1eb70e96adb9fa7f525eff0bf", size = 38717587 }, + { url = "https://files.pythonhosted.org/packages/fe/c3/2854f40ecd19585d65afaef601e5e1f8dbf6758b2f95b5ea93d38655a2c6/scipy-1.15.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:08b57a9336b8e79b305a143c3655cc5bdbe6d5ece3378578888d2afbb51c4e37", size = 30100266 }, + { url = "https://files.pythonhosted.org/packages/dd/b1/f9fe6e3c828cb5930b5fe74cb479de5f3d66d682fa8adb77249acaf545b8/scipy-1.15.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:54c462098484e7466362a9f1672d20888f724911a74c22ae35b61f9c5919183d", size = 22373768 }, + { url = "https://files.pythonhosted.org/packages/15/9d/a60db8c795700414c3f681908a2b911e031e024d93214f2d23c6dae174ab/scipy-1.15.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:cf72ff559a53a6a6d77bd8eefd12a17995ffa44ad86c77a5df96f533d4e6c6bb", size = 25154719 }, + { url = "https://files.pythonhosted.org/packages/37/3b/9bda92a85cd93f19f9ed90ade84aa1e51657e29988317fabdd44544f1dd4/scipy-1.15.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9de9d1416b3d9e7df9923ab23cd2fe714244af10b763975bea9e4f2e81cebd27", size = 35163195 }, + { url = "https://files.pythonhosted.org/packages/03/5a/fc34bf1aa14dc7c0e701691fa8685f3faec80e57d816615e3625f28feb43/scipy-1.15.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb530e4794fc8ea76a4a21ccb67dea33e5e0e60f07fc38a49e821e1eae3b71a0", size = 37255404 }, + { url = "https://files.pythonhosted.org/packages/4a/71/472eac45440cee134c8a180dbe4c01b3ec247e0338b7c759e6cd71f199a7/scipy-1.15.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5ea7ed46d437fc52350b028b1d44e002646e28f3e8ddc714011aaf87330f2f32", size = 36860011 }, + { url = "https://files.pythonhosted.org/packages/01/b3/21f890f4f42daf20e4d3aaa18182dddb9192771cd47445aaae2e318f6738/scipy-1.15.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:11e7ad32cf184b74380f43d3c0a706f49358b904fa7d5345f16ddf993609184d", size = 39657406 }, + { url = "https://files.pythonhosted.org/packages/0d/76/77cf2ac1f2a9cc00c073d49e1e16244e389dd88e2490c91d84e1e3e4d126/scipy-1.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:a5080a79dfb9b78b768cebf3c9dcbc7b665c5875793569f48bf0e2b1d7f68f6f", size = 40961243 }, + { url = "https://files.pythonhosted.org/packages/4c/4b/a57f8ddcf48e129e6054fa9899a2a86d1fc6b07a0e15c7eebff7ca94533f/scipy-1.15.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:447ce30cee6a9d5d1379087c9e474628dab3db4a67484be1b7dc3196bfb2fac9", size = 38870286 }, + { url = "https://files.pythonhosted.org/packages/0c/43/c304d69a56c91ad5f188c0714f6a97b9c1fed93128c691148621274a3a68/scipy-1.15.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:c90ebe8aaa4397eaefa8455a8182b164a6cc1d59ad53f79943f266d99f68687f", size = 30141634 }, + { url = "https://files.pythonhosted.org/packages/44/1a/6c21b45d2548eb73be9b9bff421aaaa7e85e22c1f9b3bc44b23485dfce0a/scipy-1.15.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:def751dd08243934c884a3221156d63e15234a3155cf25978b0a668409d45eb6", size = 22415179 }, + { url = "https://files.pythonhosted.org/packages/74/4b/aefac4bba80ef815b64f55da06f62f92be5d03b467f2ce3668071799429a/scipy-1.15.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:302093e7dfb120e55515936cb55618ee0b895f8bcaf18ff81eca086c17bd80af", size = 25126412 }, + { url = "https://files.pythonhosted.org/packages/b1/53/1cbb148e6e8f1660aacd9f0a9dfa2b05e9ff1cb54b4386fe868477972ac2/scipy-1.15.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd5b77413e1855351cdde594eca99c1f4a588c2d63711388b6a1f1c01f62274", size = 34952867 }, + { url = "https://files.pythonhosted.org/packages/2c/23/e0eb7f31a9c13cf2dca083828b97992dd22f8184c6ce4fec5deec0c81fcf/scipy-1.15.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d0194c37037707b2afa7a2f2a924cf7bac3dc292d51b6a925e5fcb89bc5c776", size = 36890009 }, + { url = "https://files.pythonhosted.org/packages/03/f3/e699e19cabe96bbac5189c04aaa970718f0105cff03d458dc5e2b6bd1e8c/scipy-1.15.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:bae43364d600fdc3ac327db99659dcb79e6e7ecd279a75fe1266669d9a652828", size = 36545159 }, + { url = "https://files.pythonhosted.org/packages/af/f5/ab3838e56fe5cc22383d6fcf2336e48c8fe33e944b9037fbf6cbdf5a11f8/scipy-1.15.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f031846580d9acccd0044efd1a90e6f4df3a6e12b4b6bd694a7bc03a89892b28", size = 39136566 }, + { url = "https://files.pythonhosted.org/packages/0a/c8/b3f566db71461cabd4b2d5b39bcc24a7e1c119535c8361f81426be39bb47/scipy-1.15.2-cp313-cp313t-win_amd64.whl", hash = "sha256:fe8a9eb875d430d81755472c5ba75e84acc980e4a8f6204d402849234d3017db", size = 40477705 }, +] + +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638 }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "tzdata" +version = "2025.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/0f/fa4723f22942480be4ca9527bbde8d43f6c3f2fe8412f00e7f5f6746bc8b/tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694", size = 194950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/dd/84f10e23edd882c6f968c21c2434fe67bd4a528967067515feca9e611e5e/tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639", size = 346762 }, +] + +[[package]] +name = "wrapt" +version = "1.17.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/d1/1daec934997e8b160040c78d7b31789f19b122110a75eca3d4e8da0049e1/wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984", size = 53307 }, + { url = "https://files.pythonhosted.org/packages/1b/7b/13369d42651b809389c1a7153baa01d9700430576c81a2f5c5e460df0ed9/wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22", size = 38486 }, + { url = "https://files.pythonhosted.org/packages/62/bf/e0105016f907c30b4bd9e377867c48c34dc9c6c0c104556c9c9126bd89ed/wrapt-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7", size = 38777 }, + { url = "https://files.pythonhosted.org/packages/27/70/0f6e0679845cbf8b165e027d43402a55494779295c4b08414097b258ac87/wrapt-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c", size = 83314 }, + { url = "https://files.pythonhosted.org/packages/0f/77/0576d841bf84af8579124a93d216f55d6f74374e4445264cb378a6ed33eb/wrapt-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72", size = 74947 }, + { url = "https://files.pythonhosted.org/packages/90/ec/00759565518f268ed707dcc40f7eeec38637d46b098a1f5143bff488fe97/wrapt-1.17.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061", size = 82778 }, + { url = "https://files.pythonhosted.org/packages/f8/5a/7cffd26b1c607b0b0c8a9ca9d75757ad7620c9c0a9b4a25d3f8a1480fafc/wrapt-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2", size = 81716 }, + { url = "https://files.pythonhosted.org/packages/7e/09/dccf68fa98e862df7e6a60a61d43d644b7d095a5fc36dbb591bbd4a1c7b2/wrapt-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c", size = 74548 }, + { url = "https://files.pythonhosted.org/packages/b7/8e/067021fa3c8814952c5e228d916963c1115b983e21393289de15128e867e/wrapt-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62", size = 81334 }, + { url = "https://files.pythonhosted.org/packages/4b/0d/9d4b5219ae4393f718699ca1c05f5ebc0c40d076f7e65fd48f5f693294fb/wrapt-1.17.2-cp310-cp310-win32.whl", hash = "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563", size = 36427 }, + { url = "https://files.pythonhosted.org/packages/72/6a/c5a83e8f61aec1e1aeef939807602fb880e5872371e95df2137142f5c58e/wrapt-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f", size = 38774 }, + { url = "https://files.pythonhosted.org/packages/cd/f7/a2aab2cbc7a665efab072344a8949a71081eed1d2f451f7f7d2b966594a2/wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58", size = 53308 }, + { url = "https://files.pythonhosted.org/packages/50/ff/149aba8365fdacef52b31a258c4dc1c57c79759c335eff0b3316a2664a64/wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda", size = 38488 }, + { url = "https://files.pythonhosted.org/packages/65/46/5a917ce85b5c3b490d35c02bf71aedaa9f2f63f2d15d9949cc4ba56e8ba9/wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438", size = 38776 }, + { url = "https://files.pythonhosted.org/packages/ca/74/336c918d2915a4943501c77566db41d1bd6e9f4dbc317f356b9a244dfe83/wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a", size = 83776 }, + { url = "https://files.pythonhosted.org/packages/09/99/c0c844a5ccde0fe5761d4305485297f91d67cf2a1a824c5f282e661ec7ff/wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000", size = 75420 }, + { url = "https://files.pythonhosted.org/packages/b4/b0/9fc566b0fe08b282c850063591a756057c3247b2362b9286429ec5bf1721/wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6", size = 83199 }, + { url = "https://files.pythonhosted.org/packages/9d/4b/71996e62d543b0a0bd95dda485219856def3347e3e9380cc0d6cf10cfb2f/wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b", size = 82307 }, + { url = "https://files.pythonhosted.org/packages/39/35/0282c0d8789c0dc9bcc738911776c762a701f95cfe113fb8f0b40e45c2b9/wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662", size = 75025 }, + { url = "https://files.pythonhosted.org/packages/4f/6d/90c9fd2c3c6fee181feecb620d95105370198b6b98a0770cba090441a828/wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72", size = 81879 }, + { url = "https://files.pythonhosted.org/packages/8f/fa/9fb6e594f2ce03ef03eddbdb5f4f90acb1452221a5351116c7c4708ac865/wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317", size = 36419 }, + { url = "https://files.pythonhosted.org/packages/47/f8/fb1773491a253cbc123c5d5dc15c86041f746ed30416535f2a8df1f4a392/wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3", size = 38773 }, + { url = "https://files.pythonhosted.org/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799 }, + { url = "https://files.pythonhosted.org/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821 }, + { url = "https://files.pythonhosted.org/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919 }, + { url = "https://files.pythonhosted.org/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721 }, + { url = "https://files.pythonhosted.org/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899 }, + { url = "https://files.pythonhosted.org/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222 }, + { url = "https://files.pythonhosted.org/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707 }, + { url = "https://files.pythonhosted.org/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685 }, + { url = "https://files.pythonhosted.org/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567 }, + { url = "https://files.pythonhosted.org/packages/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672 }, + { url = "https://files.pythonhosted.org/packages/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865 }, + { url = "https://files.pythonhosted.org/packages/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800 }, + { url = "https://files.pythonhosted.org/packages/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824 }, + { url = "https://files.pythonhosted.org/packages/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920 }, + { url = "https://files.pythonhosted.org/packages/3b/24/11c4510de906d77e0cfb5197f1b1445d4fec42c9a39ea853d482698ac681/wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8", size = 88690 }, + { url = "https://files.pythonhosted.org/packages/71/d7/cfcf842291267bf455b3e266c0c29dcb675b5540ee8b50ba1699abf3af45/wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6", size = 80861 }, + { url = "https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc", size = 89174 }, + { url = "https://files.pythonhosted.org/packages/a7/d3/8e17bb70f6ae25dabc1aaf990f86824e4fd98ee9cadf197054e068500d27/wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2", size = 86721 }, + { url = "https://files.pythonhosted.org/packages/6f/54/f170dfb278fe1c30d0ff864513cff526d624ab8de3254b20abb9cffedc24/wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b", size = 79763 }, + { url = "https://files.pythonhosted.org/packages/4a/98/de07243751f1c4a9b15c76019250210dd3486ce098c3d80d5f729cba029c/wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504", size = 87585 }, + { url = "https://files.pythonhosted.org/packages/f9/f0/13925f4bd6548013038cdeb11ee2cbd4e37c30f8bfd5db9e5a2a370d6e20/wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a", size = 36676 }, + { url = "https://files.pythonhosted.org/packages/bf/ae/743f16ef8c2e3628df3ddfd652b7d4c555d12c84b53f3d8218498f4ade9b/wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845", size = 38871 }, + { url = "https://files.pythonhosted.org/packages/3d/bc/30f903f891a82d402ffb5fda27ec1d621cc97cb74c16fea0b6141f1d4e87/wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192", size = 56312 }, + { url = "https://files.pythonhosted.org/packages/8a/04/c97273eb491b5f1c918857cd26f314b74fc9b29224521f5b83f872253725/wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b", size = 40062 }, + { url = "https://files.pythonhosted.org/packages/4e/ca/3b7afa1eae3a9e7fefe499db9b96813f41828b9fdb016ee836c4c379dadb/wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0", size = 40155 }, + { url = "https://files.pythonhosted.org/packages/89/be/7c1baed43290775cb9030c774bc53c860db140397047cc49aedaf0a15477/wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306", size = 113471 }, + { url = "https://files.pythonhosted.org/packages/32/98/4ed894cf012b6d6aae5f5cc974006bdeb92f0241775addad3f8cd6ab71c8/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb", size = 101208 }, + { url = "https://files.pythonhosted.org/packages/ea/fd/0c30f2301ca94e655e5e057012e83284ce8c545df7661a78d8bfca2fac7a/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681", size = 109339 }, + { url = "https://files.pythonhosted.org/packages/75/56/05d000de894c4cfcb84bcd6b1df6214297b8089a7bd324c21a4765e49b14/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6", size = 110232 }, + { url = "https://files.pythonhosted.org/packages/53/f8/c3f6b2cf9b9277fb0813418e1503e68414cd036b3b099c823379c9575e6d/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6", size = 100476 }, + { url = "https://files.pythonhosted.org/packages/a7/b1/0bb11e29aa5139d90b770ebbfa167267b1fc548d2302c30c8f7572851738/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", size = 106377 }, + { url = "https://files.pythonhosted.org/packages/6a/e1/0122853035b40b3f333bbb25f1939fc1045e21dd518f7f0922b60c156f7c/wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", size = 37986 }, + { url = "https://files.pythonhosted.org/packages/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750 }, + { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594 }, +]