diff --git a/docs/model_selection/spotoptim_intro.qmd b/docs/model_selection/spotoptim_intro.qmd index fdd999fe..1f9307eb 100644 --- a/docs/model_selection/spotoptim_intro.qmd +++ b/docs/model_selection/spotoptim_intro.qmd @@ -181,8 +181,6 @@ tensorboard --logdir runs TensorBoard shows `y_values/min`, `y_values/last`, `X_best/` and `success_rate` advancing as evaluations complete, plus one HParams entry -per evaluated configuration. This also works with parallel tuning -(`n_jobs_spotoptim != 1` / `kwargs_spotoptim={"n_jobs": -1}`): -evaluations run in worker processes and are logged parent-side as their -results arrive (requires `spotoptim >= 0.12.8`; older versions log only -the initial design in parallel mode). +per evaluated configuration. SpotOptim runs the tuning sequentially +(spotoptim ≥ 1.0 is sequential-only), so every evaluation is logged as +soon as it finishes. diff --git a/pyproject.toml b/pyproject.toml index e23ed931..d9f5a861 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,9 +31,17 @@ dependencies = [ "ruff>=0.15.6", "scikit-learn>=1.8.0", "shap>=0.49.1", - "spotforecast2-safe>=19.0.0,<21", - "spotoptim>=0.12.9", + "spotforecast2-safe>=21.0.0,<22", + # spotoptim 1.0 is sequential-only and lean: torch/tensorboard moved to its + # ``[torch]`` extra. sf2 forwards tensorboard_* kwargs into SpotOptim, so we + # pin the extra to keep the TensorBoard tuning dashboards working (they were + # always available via spotoptim's old hard torch dependency). + "spotoptim[torch]>=1.0.0,<2", "tqdm>=4.67.2", + # Directly imported by spotforecast2.tasks.task_entsoe (and the xgb + # forecaster model). Previously satisfied transitively; declared explicitly + # now that spotoptim 1.0 no longer pulls it in. + "xgboost>=3.2.0", ] [build-system] @@ -75,10 +83,10 @@ spotforecast-n2o1-cov-df = "spotforecast2.tasks.task_n_to_1_with_covariates_and_ # pre-release marker. Re-add an rc marker to the spotforecast2-safe specifier # (e.g. >=15.8.0rc1) to let sf2's CI integrate against an sf2-safe release # candidate without waiting for the develop→main promotion; every other -# dependency stays on stable. The current floor tracks the stable release that -# added the holiday-adjacency feature surface — get_holiday_adjacency_features -# and the include_holiday_adjacency_features config flag -# (spotforecast2-safe>=15.9.0), which multitask.base imports unconditionally. +# dependency stays on stable. The current floor tracks the breaking +# spotforecast2-safe 21.0.0 release, which dropped the now-dead +# n_jobs_spotoptim config field in lockstep with spotoptim 1.0 +# (sequential-only); sf2 no longer reads or forwards it. prerelease = "explicit" [tool.pytest.ini_options] diff --git a/src/spotforecast2/model_selection/spotoptim_search.py b/src/spotforecast2/model_selection/spotoptim_search.py index 8ed62b91..9f462386 100644 --- a/src/spotforecast2/model_selection/spotoptim_search.py +++ b/src/spotforecast2/model_selection/spotoptim_search.py @@ -15,7 +15,6 @@ import ast import logging -import multiprocessing import warnings from copy import deepcopy from typing import Any, Callable, Dict @@ -288,8 +287,6 @@ def spotoptim_objective( all_lags: list, all_params: list[dict], n_trials: int | None = None, - config_counter: object | None = None, - config_counter_lock: object | None = None, ) -> np.ndarray: """SpotOptim objective function to evaluate hyperparameter sets. @@ -319,17 +316,6 @@ def spotoptim_objective( shown as a prefix on each per-fold progress bar (when ``show_progress`` is ``True``). When ``None``, the label omits the total and reads "config k". Does not affect the optimisation. - config_counter: Optional shared running count of started config - evaluations (a `multiprocessing.Manager().Value("i", 0)` proxy). - With SpotOptim ``n_jobs > 1`` each evaluation runs in a worker - process holding its own (empty) copy of ``all_params``, so - ``len(all_params) + 1`` is stuck at 1 there; the manager-backed - counter is the only cross-process source for the "config k/N" - label. When ``None``, the label falls back to - ``len(all_params) + 1`` (correct in sequential mode). - config_counter_lock: Lock proxy (`multiprocessing.Manager().Lock()`) - guarding ``config_counter`` increments. Required together with - ``config_counter``. Returns: np.ndarray: 1D array of results for the primary metric. @@ -374,19 +360,12 @@ def set_lags(self, lags): pass if cv_name == "TimeSeriesFold": # Coarse-grained progress: prefix the per-fold bar with the running - # count of evaluated candidate configurations. With a shared - # ``config_counter`` (parallel SpotOptim) the count is incremented - # atomically across worker processes; otherwise ``all_params`` has + # count of evaluated candidate configurations. ``all_params`` has # one entry per already-completed candidate, so this candidate is # number ``len(all_params) + 1``. Built only when the bar is shown. progress_desc = None if show_progress: - if config_counter is not None and config_counter_lock is not None: - with config_counter_lock: - config_counter.value += 1 - config_idx = config_counter.value - else: - config_idx = len(all_params) + 1 + config_idx = len(all_params) + 1 progress_desc = ( f"config {config_idx}/{n_trials}" if n_trials is not None @@ -595,41 +574,14 @@ def spotoptim_search( all_lags: list = [] all_params: list[dict] = [] - # With SpotOptim ``n_jobs != 1`` every objective evaluation runs in a - # worker process that receives a dill copy of the optimizer — including - # this closure and its accumulator lists — so parent-side state never - # reaches the workers and worker-side appends never come back. Two - # consequences for progress display: - # * the "config k/N" label cannot be derived from ``len(all_params)`` - # (stuck at 1 in every worker); a manager-backed shared counter is - # the only cross-process source for k. - # * a parent-side trial bar would never be updated (its ``update()`` - # runs on worker copies), so it is created in sequential mode only. - spotoptim_n_jobs = kwargs_spotoptim_.get("n_jobs", 1) - parallel_eval = isinstance(spotoptim_n_jobs, int) and spotoptim_n_jobs != 1 - - config_counter = None - config_counter_lock = None - counter_manager = None - # NOT gated on ``show_progress``: the per-config fold bars are always on - # (``_objective_wrapper`` passes ``show_progress=True`` to the objective - # below), so the label needs the shared counter in every parallel run — - # the MultiTask pipeline reaches this with ``show_progress=False`` and - # would otherwise show frozen "config 1/N" labels again. - if parallel_eval: - counter_manager = multiprocessing.Manager() - config_counter = counter_manager.Value("i", 0) - config_counter_lock = counter_manager.Lock() - - # Single trial-level progress bar (sequential mode only, see above). - # Each entry in ``X`` is one trial (initial design point or sequential - # proposal), so we advance by ``len(X)`` per objective call. - # ``n_trials`` (== SpotOptim's ``max_iter``) is the total budget — it - # already includes the ``n_initial`` design points, so the total bar - # length is ``n_trials``. + # Single trial-level progress bar. Each entry in ``X`` is one trial + # (initial design point or sequential proposal), so we advance by + # ``len(X)`` per objective call. ``n_trials`` (== SpotOptim's + # ``max_iter``) is the total budget — it already includes the + # ``n_initial`` design points, so the total bar length is ``n_trials``. trial_bar = ( tqdm(total=n_trials, desc="SpotOptim trials", leave=True) - if show_progress and not parallel_eval + if show_progress else None ) @@ -654,8 +606,6 @@ def _objective_wrapper(X: np.ndarray) -> np.ndarray: all_lags=all_lags, all_params=all_params, n_trials=n_trials, - config_counter=config_counter, - config_counter_lock=config_counter_lock, ) if trial_bar is not None: trial_bar.update(len(X)) @@ -680,47 +630,6 @@ def _objective_wrapper(X: np.ndarray) -> np.ndarray: finally: if trial_bar is not None: trial_bar.close() - if counter_manager is not None: - counter_manager.shutdown() - - # --- Parallel-safe result recovery ------------------------------------ - # With ``n_jobs > 1`` SpotOptim evaluates the objective in worker - # processes (ProcessPoolExecutor on a GIL build), so the side-effect - # accumulators above are populated only in the workers and stay empty in - # this (parent) process. Rebuild them from the optimizer's own evaluation - # history, which *is* retained parent-side (``optimizer.X_`` / ``y_``). - # Decoding mirrors the objective: ``array_to_params`` handles both the - # string-label and integer-code factor encodings, and ``X_`` is stored in - # natural scale. Only the primary metric is recoverable this way -- the - # optimizer minimises a single scalar -- so any secondary metrics are - # filled with NaN. - if ( - not all_lags - and getattr(optimizer, "X_", None) is not None - and len(optimizer.X_) > 0 - ): - if len(metric) > 1: - warnings.warn( - "Parallel SpotOptim (n_jobs > 1) records only the primary " - f"metric ('{metric[0] if isinstance(metric[0], str) else metric[0].__name__}'); " - "secondary metrics are reported as NaN.", - IgnoredArgumentWarning, - ) - y_hist = np.asarray(optimizer.y_, dtype=float).ravel() - for row, y_val in zip(np.asarray(optimizer.X_, dtype=object), y_hist): - params_dict = array_to_params(np.asarray(row), var_name, var_type, bounds) - sample_params = {k: v for k, v in params_dict.items() if k != "lags"} - lags_val = params_dict.get( - "lags", - forecaster_search.lags if hasattr(forecaster_search, "lags") else None, - ) - if isinstance(lags_val, str): - lags_val = parse_lags_from_strings(lags_val) - all_lags.append(lags_val) - all_params.append(sample_params) - all_metric_values.append( - [float(y_val)] + [float("nan")] * (len(metric) - 1) - ) # --- Build results DataFrame ------------------------------------------ lags_list = [ diff --git a/src/spotforecast2/multitask/strategies.py b/src/spotforecast2/multitask/strategies.py index 23f71efd..3b042565 100644 --- a/src/spotforecast2/multitask/strategies.py +++ b/src/spotforecast2/multitask/strategies.py @@ -228,8 +228,7 @@ def prepare_forecaster( is wiped at search start); set ``cfg.tensorboard_clean = False`` to keep accumulating event files instead — do that, or give each task its own ``tensorboard_path``, when several tasks share one - log directory. Live per-eval scalars under parallel tuning - (``n_jobs_spotoptim != 1``) require ``spotoptim >= 0.12.8``. + log directory. Args: task: A `BaseTask` (or compatible) instance that supplies ``cv_ts``, @@ -237,8 +236,8 @@ def prepare_forecaster( ``create_forecaster``. The config must expose ``n_trials_spotoptim``, ``n_initial_spotoptim``, ``random_state``, ``warm_start_lags``, and optionally - ``lags_consider``, ``n_jobs_spotoptim``, and the - TensorBoard knobs described above. + ``lags_consider`` and the TensorBoard knobs described + above. target: Target column name; forwarded to ``task.create_forecaster`` and ``task.save_tuning_results``. forecaster: An unfitted forecaster instance used as the search @@ -283,7 +282,6 @@ def prepare_forecaster( n_initial_spotoptim=3, random_state=0, warm_start_lags=False, - n_jobs_spotoptim=None, ) task = types.SimpleNamespace( config=cfg, @@ -334,19 +332,11 @@ def prepare_forecaster( kwargs_spotoptim["x0"] = x0 task.logger.info(" Warm-start lags seeded: %s", seed_str) - # Parallel evaluation: forward the configured worker count straight to - # SpotOptim (``kwargs_spotoptim`` is spread into its constructor). - n_jobs_spotoptim = getattr(task.config, "n_jobs_spotoptim", None) - if n_jobs_spotoptim is not None: - kwargs_spotoptim["n_jobs"] = n_jobs_spotoptim - task.logger.info(" SpotOptim n_jobs: %s", n_jobs_spotoptim) - # Tuning visualization: forward TensorBoard knobs to SpotOptim. Users # set these as plain attributes on the config (the safe-package config # classes carry no __slots__, so no spotforecast2_safe change is # needed). Unset attributes are skipped so SpotOptim's own defaults - # (tensorboard_log=False) stay in charge. Requires spotoptim >= 0.12.8 - # for live per-eval logging under n_jobs != 1. + # (tensorboard_log=False) stay in charge. tb_kwargs = { key: getattr(task.config, key) for key in ("tensorboard_log", "tensorboard_path", "tensorboard_clean") diff --git a/tests/test_multitask_strategies.py b/tests/test_multitask_strategies.py index 16e98a1a..c4e4e15e 100644 --- a/tests/test_multitask_strategies.py +++ b/tests/test_multitask_strategies.py @@ -76,7 +76,6 @@ def _make_fake_task(**config_extra): n_initial_spotoptim=1, random_state=0, warm_start_lags=False, - n_jobs_spotoptim=None, **config_extra, ) return types.SimpleNamespace( @@ -203,5 +202,5 @@ def fake_search_forecaster(*args, **kwargs): task, "A", _FakeForecaster(), y_train=None ) - # n_jobs_spotoptim=None and no tensorboard attrs -> empty dict -> None. + # No tensorboard attrs -> empty kwargs dict -> None passed through. assert captured["kwargs_spotoptim"] is None diff --git a/tests/test_spotoptim_parallel.py b/tests/test_spotoptim_parallel.py deleted file mode 100644 index 21537069..00000000 --- a/tests/test_spotoptim_parallel.py +++ /dev/null @@ -1,177 +0,0 @@ -# SPDX-FileCopyrightText: 2026 bartzbeielstein -# SPDX-License-Identifier: AGPL-3.0-or-later - -"""Parallel SpotOptim tuning (``n_jobs > 1``). - -Two things are exercised: - -1. ``SpotOptimStrategy`` forwards the configured worker count to SpotOptim via - ``kwargs_spotoptim["n_jobs"]``. -2. ``spotoptim_search_forecaster`` returns a complete, correct ``results`` table - in parallel mode. In parallel the objective runs in worker processes, so its - side-effect accumulators never reach the parent; the results are rebuilt from - the optimizer's own evaluation history (``optimizer.X_`` / ``y_``). The - rebuilt table must match a sequential run on the seeded initial design. - -Requires the steady-state ``inverse_transform`` fix in ``spotoptim >= 0.12.3`` -(without it a log10-transformed hyperparameter crashes the GP surrogate). -""" - -import types - -import numpy as np -import pandas as pd -import pytest -from sklearn.linear_model import Ridge -from spotforecast2_safe.forecaster.recursive import ForecasterRecursive -from spotforecast2_safe.splitter import TimeSeriesFold - -from spotforecast2.model_selection import spotoptim_search_forecaster -from spotforecast2.multitask.strategies import SpotOptimStrategy - - -def _series(n=400, seed=0): - rng = np.random.default_rng(seed) - t = np.arange(n) - y = np.sin(2 * np.pi * t / 24) + 0.5 * np.sin(2 * np.pi * t / 168) - y = y + rng.normal(0, 0.1, n) + 100.0 - idx = pd.date_range("2022-01-01", periods=n, freq="h") - return pd.Series(y, index=idx, name="load") - - -def _run(n_jobs): - forecaster = ForecasterRecursive(estimator=Ridge(alpha=1.0), lags=24) - cv = TimeSeriesFold(steps=24, initial_train_size=300, refit=False) - search_space = {"lags": ["24", "[1, 2, 24]"], "alpha": (0.01, 10.0, "log10")} - kwargs = {"n_jobs": n_jobs} if n_jobs and n_jobs > 1 else None - results, optimizer = spotoptim_search_forecaster( - forecaster=forecaster, - y=_series(), - cv=cv, - search_space=search_space, - metric="mean_absolute_error", - n_trials=8, - n_initial=4, - random_state=123, - return_best=False, - verbose=False, - show_progress=False, - kwargs_spotoptim=kwargs, - ) - return results, optimizer - - -def test_parallel_results_are_not_empty_and_decode(): - results, optimizer = _run(n_jobs=2) - # The optimizer evaluated every trial... - assert len(optimizer.X_) == 8 - # ...and the results table is fully reconstructed (not empty -- the bug this - # guards against returned 0 rows in parallel). - assert len(results) == 8 - assert "lags" in results.columns - assert "alpha" in results.columns - assert "mean_absolute_error" in results.columns - # Decoded hyperparameters are sane: alpha in its natural bound, lags parsed - # back to a list of ints. - assert results["alpha"].between(0.01, 10.0).all() - assert all(isinstance(lg, (list, np.ndarray)) for lg in results["lags"]) - assert np.isfinite(results["mean_absolute_error"]).all() - - -def test_parallel_matches_sequential_best(): - seq, _ = _run(n_jobs=1) - par, _ = _run(n_jobs=2) - # The seeded initial design is shared, so the best score agrees closely. - assert seq["mean_absolute_error"].min() == pytest.approx( - par["mean_absolute_error"].min(), rel=1e-3 - ) - - -def test_strategy_forwards_n_jobs_to_spotoptim(monkeypatch): - """``SpotOptimStrategy`` puts ``config.n_jobs_spotoptim`` into the - ``kwargs_spotoptim`` passed to ``spotoptim_search_forecaster``.""" - captured = {} - - class _Stop(Exception): - pass - - def _fake_search(*args, **kwargs): - captured.update(kwargs) - raise _Stop() - - # prepare_forecaster imports the name from spotforecast2.model_selection at - # call time, so patch it on that package. - monkeypatch.setattr( - "spotforecast2.model_selection.spotoptim_search_forecaster", _fake_search - ) - - config = types.SimpleNamespace( - warm_start_lags=False, - lags_consider=None, - n_jobs_spotoptim=-1, - random_state=42, - n_trials_spotoptim=5, - n_initial_spotoptim=3, - ) - task = types.SimpleNamespace( - config=config, - cv_ts=lambda y: object(), - logger=types.SimpleNamespace(info=lambda *a, **k: None), - _show_progress=False, - ) - - strategy = SpotOptimStrategy() - with pytest.raises(_Stop): - strategy.prepare_forecaster( - task=task, - target="Actual Load", - forecaster=object(), - y_train=_series(), - exog_train=None, - ) - - assert captured["kwargs_spotoptim"] == {"n_jobs": -1} - - -def test_strategy_omits_n_jobs_when_unset(monkeypatch): - """With ``n_jobs_spotoptim=None`` (default) no ``n_jobs`` is forwarded, so the - optimizer stays sequential.""" - captured = {} - - class _Stop(Exception): - pass - - def _fake_search(*args, **kwargs): - captured.update(kwargs) - raise _Stop() - - monkeypatch.setattr( - "spotforecast2.model_selection.spotoptim_search_forecaster", _fake_search - ) - - config = types.SimpleNamespace( - warm_start_lags=False, - lags_consider=None, - n_jobs_spotoptim=None, - random_state=42, - n_trials_spotoptim=5, - n_initial_spotoptim=3, - ) - task = types.SimpleNamespace( - config=config, - cv_ts=lambda y: object(), - logger=types.SimpleNamespace(info=lambda *a, **k: None), - _show_progress=False, - ) - - with pytest.raises(_Stop): - SpotOptimStrategy().prepare_forecaster( - task=task, - target="Actual Load", - forecaster=object(), - y_train=_series(), - exog_train=None, - ) - - # No warm start, no n_jobs -> kwargs_spotoptim collapses to None. - assert captured["kwargs_spotoptim"] is None diff --git a/tests/test_spotoptim_search.py b/tests/test_spotoptim_search.py index e95604b0..f24afeeb 100644 --- a/tests/test_spotoptim_search.py +++ b/tests/test_spotoptim_search.py @@ -414,7 +414,7 @@ def optuna_search_space(trial): class TestConfigProgressLabel: - """The per-config progress label must count across worker processes.""" + """The per-config progress label counts completed configurations.""" @staticmethod def _record_descs(monkeypatch): @@ -431,7 +431,7 @@ def fake_backtesting(**kwargs): ) return descs - def _run_objective(self, y_series, cv, counter=None, lock=None): + def _run_objective(self, y_series, cv): from spotforecast2.model_selection.spotoptim_search import spotoptim_objective return spotoptim_objective( @@ -453,66 +453,16 @@ def _run_objective(self, y_series, cv, counter=None, lock=None): all_lags=[], all_params=[], n_trials=10, - config_counter=counter, - config_counter_lock=lock, ) - def test_label_falls_back_to_local_count(self, y_series, cv, monkeypatch): - """Without a shared counter the label counts completed local configs.""" + def test_label_counts_completed_configs(self, y_series, cv, monkeypatch): + """The label counts completed configs (len(all_params) + 1).""" descs = self._record_descs(monkeypatch) self._run_objective(y_series, cv) assert descs == ["config 1/10", "config 2/10"] - def test_label_uses_shared_counter(self, y_series, cv, monkeypatch): - """With a shared counter the label continues the cross-process count.""" - import threading - - class FakeCounter: - value = 5 # five configs already started in other workers - - descs = self._record_descs(monkeypatch) - counter = FakeCounter() - self._run_objective(y_series, cv, counter=counter, lock=threading.Lock()) - assert descs == ["config 6/10", "config 7/10"] - assert counter.value == 7 - - def test_parallel_labels_increment_with_show_progress_false( - self, y_series, forecaster, cv, capfd - ): - """Labels must increment even when the OUTER show_progress is False. - - The MultiTask pipeline calls spotoptim_search with - show_progress=False, yet the per-config fold bars are always shown - (the objective wrapper hardcodes show_progress=True). Gating the - shared counter on the outer flag therefore reintroduced frozen - 'config 1/N' labels in every real pipeline run — this is the exact - scenario from the 2026-06-07 team4_submit report. - """ - spotoptim_search( - forecaster=forecaster, - y=y_series, - cv=cv, - search_space={"alpha": (0.01, 10.0)}, - metric="mean_absolute_error", - n_trials=4, - n_initial=2, - return_best=False, - verbose=False, - show_progress=False, - kwargs_spotoptim={"n_jobs": 2}, - ) - err = capfd.readouterr().err - for k in range(1, 5): - assert f"config {k}/4" in err, f"missing 'config {k}/4' in:\n{err}" - - def test_parallel_search_labels_increment(self, y_series, forecaster, cv, capfd): - """End to end: SpotOptim n_jobs=2 must not repeat 'config 1/N'. - - Each parallel evaluation runs in a worker process holding a dill - copy of the objective closure, so a label derived from local list - length is stuck at 1 (the bug this guards against). The - manager-backed counter must yield one distinct label per config. - """ + def test_search_labels_increment(self, y_series, forecaster, cv, capfd): + """End to end: a sequential search yields one distinct label per config.""" spotoptim_search( forecaster=forecaster, y=y_series, @@ -524,7 +474,6 @@ def test_parallel_search_labels_increment(self, y_series, forecaster, cv, capfd) return_best=False, verbose=False, show_progress=True, - kwargs_spotoptim={"n_jobs": 2}, ) err = capfd.readouterr().err for k in range(1, 5): @@ -539,25 +488,8 @@ def test_parallel_search_labels_increment(self, y_series, forecaster, cv, capfd) class TestTensorboardPassThrough: """tensorboard_* kwargs flow through kwargs_spotoptim into SpotOptim.""" - @staticmethod - def _spotoptim_version(): - import importlib.metadata - - from packaging.version import Version - - return Version(importlib.metadata.version("spotoptim")) - - def test_parallel_run_writes_event_files(self, y_series, forecaster, cv, tmp_path): - """Real tiny parallel run with tensorboard_log=True creates event files. - - Live per-eval logging under n_jobs != 1 requires spotoptim>=0.12.8 - (older versions log only the initial design in parallel mode). - """ - import pytest as _pytest - - if self._spotoptim_version() < type(self._spotoptim_version())("0.12.8"): - _pytest.skip("parallel TensorBoard infill logging needs spotoptim>=0.12.8") - + def test_run_writes_event_files(self, y_series, forecaster, cv, tmp_path): + """A tiny run with tensorboard_log=True creates event files.""" tb_path = tmp_path / "tb" spotoptim_search( forecaster=forecaster, @@ -571,7 +503,6 @@ def test_parallel_run_writes_event_files(self, y_series, forecaster, cv, tmp_pat verbose=False, show_progress=False, kwargs_spotoptim={ - "n_jobs": 2, "tensorboard_log": True, "tensorboard_path": str(tb_path), }, diff --git a/uv.lock b/uv.lock index 946c2058..dae7bada 100644 --- a/uv.lock +++ b/uv.lock @@ -281,20 +281,6 @@ css = [ { name = "tinycss2" }, ] -[[package]] -name = "build" -version = "1.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "(os_name == 'nt' and platform_machine != 'x86_64') or (os_name == 'nt' and sys_platform != 'darwin')" }, - { name = "packaging" }, - { name = "pyproject-hooks" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/78/e0/df5e171f685f82f37b12e1f208064e24244911079d7b767447d1af7e0d70/build-1.5.0.tar.gz", hash = "sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647", size = 89796, upload-time = "2026-04-30T03:18:25.17Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl", hash = "sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f", size = 26018, upload-time = "2026-04-30T03:18:23.644Z" }, -] - [[package]] name = "certifi" version = "2026.5.20" @@ -835,20 +821,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4c/a0/614c5fe402fd88951df45f4dda2fa3b4e17a99ecd92340771929169b3b95/filelock-3.29.1-py3-none-any.whl", hash = "sha256:85199dfd706869641b72b2e8955d5416a4b2b7dc4b0e8e6d97b4cc1299a6983b", size = 40750, upload-time = "2026-06-03T15:19:02.959Z" }, ] -[[package]] -name = "flake8" -version = "7.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mccabe" }, - { name = "pycodestyle" }, - { name = "pyflakes" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" }, -] - [[package]] name = "fonttools" version = "4.63.0" @@ -1820,15 +1792,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6", size = 9534, upload-time = "2026-05-08T17:33:32.055Z" }, ] -[[package]] -name = "mccabe" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, -] - [[package]] name = "mdurl" version = "0.1.2" @@ -2182,11 +2145,11 @@ wheels = [ [[package]] name = "nvidia-nccl-cu12" -version = "2.30.4" +version = "2.30.7" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/2b/1757b6b74ee241de5efee3f35487dcb33e09c07605254809c6ce36aeb783/nvidia_nccl_cu12-2.30.4-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:606fa9aa9215c00367d060188eb1a5bbd28396aff5e11b9200d99d1a6ab79a71", size = 300091935, upload-time = "2026-04-23T03:22:58.024Z" }, - { url = "https://files.pythonhosted.org/packages/6b/c3/0e45ff4dce8401f6ea7c25d80d75738813a47f5ae2691e2478f2fd1e5e93/nvidia_nccl_cu12-2.30.4-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:040974b261edec4b8b793e59e92ab7176fe4ab4bc61b800f9f3bfaeec2d436f3", size = 300164158, upload-time = "2026-04-23T03:23:19.589Z" }, + { url = "https://files.pythonhosted.org/packages/8f/8c/554bb020501d6c04ad8127d83f728137f8f9123f991666efbdcf9095a221/nvidia_nccl_cu12-2.30.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:03ecd776fd1d58fd2c9a0a687dcf8db9ecd0057382dba646fa3d65786d4a9ea1", size = 303277471, upload-time = "2026-06-09T03:24:16.327Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/e7ffa9c324ae260e5dbb4af2cd557bf7a8d155c8ac7b79a785fe1796fb92/nvidia_nccl_cu12-2.30.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:8ce1b8213f61f2bfac132e6df890af6450b77cbd140c6ce4e98cb0c2d8e678c9", size = 303361239, upload-time = "2026-06-09T03:24:53.816Z" }, ] [[package]] @@ -2621,15 +2584,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/be/6f79d55816d5c22557cf27533543d5d70dfe692adfbee4b99f2760674f38/pyarrow-24.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c91d00057f23b8d353039520dc3a6c09d8608164c692e9f59a175a42b2ae0c19", size = 28131282, upload-time = "2026-04-21T10:51:16.815Z" }, ] -[[package]] -name = "pycodestyle" -version = "2.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, -] - [[package]] name = "pycparser" version = "3.0" @@ -2710,15 +2664,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, ] -[[package]] -name = "pyflakes" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, -] - [[package]] name = "pygments" version = "2.20.0" @@ -2737,15 +2682,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] -[[package]] -name = "pyproject-hooks" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, -] - [[package]] name = "pytest" version = "9.0.3" @@ -2810,18 +2746,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/be/0631a861af4d1c875f096c07d34e9a63639560a717130e7a87cbc82b7e3f/python_json_logger-4.1.0-py3-none-any.whl", hash = "sha256:132994765cf75bf44554be9aa49b06ef2345d23661a96720262716438141b6b2", size = 15021, upload-time = "2026-03-29T04:39:55.266Z" }, ] -[[package]] -name = "python-markdown-math" -version = "0.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4c/68/fbea05ec6fb318bdcf56ea47596605614554f51d77bfd14f6fb481139ad8/python_markdown_math-0.9.tar.gz", hash = "sha256:567395553dc4941e79b3789a1096dcabb3fda9539d150d558ef3507948b264a3", size = 8680, upload-time = "2025-04-10T10:10:31.84Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/68/ecf3535c40845de2efd8ac2d092dd5fca0868219fa3684d9e58ef7abeece/python_markdown_math-0.9-py3-none-any.whl", hash = "sha256:ac9932df517a5c0f6d01c56e7a44d065eca4a420893ac45f7a6937c67cb41e86", size = 6046, upload-time = "2025-04-10T10:10:30.318Z" }, -] - [[package]] name = "pytokens" version = "0.4.1" @@ -3391,20 +3315,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, ] -[[package]] -name = "seaborn" -version = "0.13.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "matplotlib" }, - { name = "numpy" }, - { name = "pandas" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/86/59/a451d7420a77ab0b98f7affa3a1d78a313d2f7281a57afb1a34bae8ab412/seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7", size = 1457696, upload-time = "2024-01-25T13:21:52.551Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987", size = 294914, upload-time = "2024-01-25T13:21:49.598Z" }, -] - [[package]] name = "send2trash" version = "2.1.0" @@ -3579,32 +3489,9 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/97/840be4a99531292f8f41069bfcd0654f68296ef4089bfe828607d63ee0ff/sphobjinv-2.4-py3-none-any.whl", hash = "sha256:35f3239e9a6161c20d60146c16645687d06d43e5875d2bda71010c3ee7fd54bc", size = 51315, upload-time = "2026-03-23T05:04:42.434Z" }, ] -[[package]] -name = "spotdesirability" -version = "0.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "build" }, - { name = "matplotlib" }, - { name = "nbformat" }, - { name = "numpy" }, - { name = "pandas" }, - { name = "pillow" }, - { name = "python-markdown-math" }, - { name = "scipy" }, - { name = "seaborn" }, - { name = "tabulate" }, - { name = "typing-extensions" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7b/29/d8ca06f4c063fc279c30dfd34124fa192d03f90c978932b13e70bc48a93a/spotdesirability-0.1.1.tar.gz", hash = "sha256:9fb2c21880faf2f5dca3039345aebb1484fc3d34e34247f2089c7d8edd1c086a", size = 29246, upload-time = "2026-03-30T16:57:32.988Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/ec/b1bda57445775820eb521373c0c29e0aa024da7e30b98068ed66e9b7aa59/spotdesirability-0.1.1-py3-none-any.whl", hash = "sha256:96e300b94c8c55565418d75424199b41ad584f00b79e4d3a95e63dd16fc10b57", size = 30811, upload-time = "2026-03-30T16:57:31.593Z" }, -] - [[package]] name = "spotforecast2" -version = "6.0.0" +version = "6.1.0" source = { editable = "." } dependencies = [ { name = "astral" }, @@ -3628,8 +3515,9 @@ dependencies = [ { name = "shap", version = "0.49.1", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, { name = "shap", version = "0.52.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, { name = "spotforecast2-safe" }, - { name = "spotoptim" }, + { name = "spotoptim", extra = ["torch"] }, { name = "tqdm" }, + { name = "xgboost" }, ] [package.optional-dependencies] @@ -3683,10 +3571,11 @@ requires-dist = [ { name = "safety", marker = "extra == 'dev'", specifier = ">=3.0.0" }, { name = "scikit-learn", specifier = ">=1.8.0" }, { name = "shap", specifier = ">=0.49.1" }, - { name = "spotforecast2-safe", specifier = ">=19.0.0,<21" }, - { name = "spotoptim", specifier = ">=0.12.9" }, + { name = "spotforecast2-safe", specifier = ">=21.0.0,<22" }, + { name = "spotoptim", extras = ["torch"], specifier = ">=1.0.0,<2" }, { name = "tqdm", specifier = ">=4.67.2" }, { name = "ty", marker = "extra == 'dev'", specifier = ">=0.0.29" }, + { name = "xgboost", specifier = ">=3.2.0" }, ] provides-extras = ["dev"] @@ -3701,7 +3590,7 @@ dev = [ [[package]] name = "spotforecast2-safe" -version = "20.0.0" +version = "21.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "astral" }, @@ -3718,39 +3607,32 @@ dependencies = [ { name = "statsmodels" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c8/5c/6af66d76ba594b64540fed675307cf6ada608d99850afdd808ea7d780ea2/spotforecast2_safe-20.0.0.tar.gz", hash = "sha256:2fbb176270dce6d72316a40a6e0311d39fbae89effffa34c1857a7f445aeb371", size = 20624314, upload-time = "2026-06-08T18:57:38.291Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/55/54aed0c9cdbfa8126f7241114bbb26e35c1b712683379573243978bf69bd/spotforecast2_safe-21.0.0.tar.gz", hash = "sha256:642b61b3f08b52e12cd5a24f84878fa761201455b0e8fc2907dabb9ed9022afa", size = 20624222, upload-time = "2026-06-09T19:36:08.747Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/4c/2c76a20c0083e2c70012c95fba1a6950bd24a142d2b475d159ff3de2d77e/spotforecast2_safe-20.0.0-py3-none-any.whl", hash = "sha256:2a4259fdd8078af1cf7a8cf4ef75758091b504494f999f179d3d44f08acebe1b", size = 20688771, upload-time = "2026-06-08T18:57:35.613Z" }, + { url = "https://files.pythonhosted.org/packages/71/ca/8ad0ce36cb44e539e77ea22701e4e8c4a259e45bd851cf41cd406e6cbfa8/spotforecast2_safe-21.0.0-py3-none-any.whl", hash = "sha256:ade1a635333d84097ed59d993dd7cfde5551ff350b125b65fb48699ac3a64cd3", size = 20688561, upload-time = "2026-06-09T19:36:06.457Z" }, ] [[package]] name = "spotoptim" -version = "0.12.9" +version = "1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "black" }, { name = "dill" }, - { name = "flake8" }, - { name = "importlib-metadata" }, - { name = "jupyter" }, - { name = "matplotlib" }, { name = "numpy" }, { name = "pandas" }, - { name = "requests" }, { name = "scikit-learn" }, { name = "scipy" }, - { name = "seaborn" }, - { name = "spotdesirability" }, - { name = "statsmodels" }, { name = "tabulate" }, - { name = "tensorboard" }, - { name = "torch" }, - { name = "ty" }, - { name = "xgboost" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/39/f9/95d6e754898c7d9bd976adb643b7e21d219bb73940bb977c6a0ab5479639/spotoptim-0.12.9.tar.gz", hash = "sha256:190287bd272063bd170229e6c979d69089258363e71a448bbbddea5cf412bada", size = 316604, upload-time = "2026-06-07T16:02:46.901Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/a6/e28deb3814474008095f793d8b0250880c90b30e9a222f3efb682d8b6766/spotoptim-1.0.0.tar.gz", hash = "sha256:b5772a2a699287b4c6c433c59cb18297a839fede7fee1bd28ce01925df3ac576", size = 309635, upload-time = "2026-06-09T18:09:20.56Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/90/6c/40bce16374aeeb6bbc74de705829637fc0671499806d36c2828449b5a5ed/spotoptim-0.12.9-py3-none-any.whl", hash = "sha256:e6319b305acfafb6b8d9a204c152a5060d9f923c8171070b6d210bd827d252cd", size = 356232, upload-time = "2026-06-07T16:02:44.859Z" }, + { url = "https://files.pythonhosted.org/packages/a0/50/997befcf233bc75d38567b877613ac133509cefe33e31691e9cab59b0f38/spotoptim-1.0.0-py3-none-any.whl", hash = "sha256:38e952a2ba8d1430c67b598412cc10716d50b97ada78a0edf9bf85a26876653b", size = 348466, upload-time = "2026-06-09T18:09:18.751Z" }, +] + +[package.optional-dependencies] +torch = [ + { name = "tensorboard" }, + { name = "torch" }, ] [[package]]