diff --git a/docs/model_selection/spotoptim_intro.qmd b/docs/model_selection/spotoptim_intro.qmd index 5d502b4..fdd999f 100644 --- a/docs/model_selection/spotoptim_intro.qmd +++ b/docs/model_selection/spotoptim_intro.qmd @@ -167,7 +167,10 @@ results, optimizer = spotoptim_search_forecaster( cfg = ConfigEntsoe(...) cfg.tensorboard_log = True cfg.tensorboard_path = "runs/my_tuning_run" # optional; defaults to runs/ -cfg.tensorboard_clean = False # True wipes old runs/ subdirs first +# tensorboard_clean defaults to True when logging is enabled: the log +# directory is wiped at search start so each run gets a fresh dashboard. +# Set cfg.tensorboard_clean = False to keep accumulating event files — +# do that (or use per-task paths) when several tasks share one directory. ``` Then watch the run live: diff --git a/pyproject.toml b/pyproject.toml index c803d00..00cedc0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ "scikit-learn>=1.8.0", "shap>=0.49.1", "spotforecast2-safe>=18.0.0,<19", - "spotoptim>=0.12.8", + "spotoptim>=0.12.9", "tqdm>=4.67.2", ] diff --git a/src/spotforecast2/multitask/strategies.py b/src/spotforecast2/multitask/strategies.py index 0ffe623..23f71ef 100644 --- a/src/spotforecast2/multitask/strategies.py +++ b/src/spotforecast2/multitask/strategies.py @@ -222,9 +222,14 @@ def prepare_forecaster( SpotOptim constructor and the ongoing search can be watched with ``tensorboard --logdir `` (defaults to ``runs/``). Set them as plain attributes, e.g. ``cfg.tensorboard_log = True`` — no - config-class change is required. Live per-eval scalars under - parallel tuning (``n_jobs_spotoptim != 1``) require - ``spotoptim >= 0.12.8``. + config-class change is required. When logging is enabled and + ``tensorboard_clean`` is unset, it defaults to ``True`` so every + run starts with a fresh dashboard (the configured log directory + 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``. Args: task: A `BaseTask` (or compatible) instance that supplies ``cv_ts``, @@ -347,6 +352,11 @@ def prepare_forecaster( for key in ("tensorboard_log", "tensorboard_path", "tensorboard_clean") if getattr(task.config, key, None) is not None } + if tb_kwargs.get("tensorboard_log") and "tensorboard_clean" not in tb_kwargs: + # Fresh dashboard per run: wipe the configured log directory before + # the search starts (spotoptim >= 0.12.9 cleans tensorboard_path + # itself). Opt out with ``cfg.tensorboard_clean = False``. + tb_kwargs["tensorboard_clean"] = True if tb_kwargs: kwargs_spotoptim.update(tb_kwargs) task.logger.info(" SpotOptim TensorBoard: %s", tb_kwargs) diff --git a/tests/test_multitask_strategies.py b/tests/test_multitask_strategies.py index f2e17a5..16e98a1 100644 --- a/tests/test_multitask_strategies.py +++ b/tests/test_multitask_strategies.py @@ -120,12 +120,69 @@ def fake_search_forecaster(*args, **kwargs): ks = captured["kwargs_spotoptim"] assert ks["tensorboard_log"] is True assert ks["tensorboard_path"] == "runs/test-tb" - # Unset attribute must be skipped so SpotOptim's default stays in charge. - assert "tensorboard_clean" not in ks + # With logging enabled and tensorboard_clean unset, the strategy + # defaults it to True so every run starts with a fresh dashboard. + assert ks["tensorboard_clean"] is True assert isinstance(tuned, _FakeForecaster) assert tuned.params == {"alpha": 1.0} +def test_spotoptim_strategy_respects_explicit_tensorboard_clean_false(monkeypatch): + """An explicit tensorboard_clean=False must not be overridden.""" + import pandas as pd + + import spotforecast2.model_selection as ms + + captured = {} + + def fake_search_forecaster(*args, **kwargs): + captured["kwargs_spotoptim"] = kwargs.get("kwargs_spotoptim") + results = pd.DataFrame({"params": [{"alpha": 1.0}], "lags": [[1, 2]]}) + return results, object() + + monkeypatch.setattr(ms, "spotoptim_search_forecaster", fake_search_forecaster) + + task = _make_fake_task( + tensorboard_log=True, + tensorboard_path="runs/test-tb", + tensorboard_clean=False, + ) + SpotOptimStrategy(search_space={"alpha": (0.1, 1.0)}).prepare_forecaster( + task, "A", _FakeForecaster(), y_train=None + ) + + ks = captured["kwargs_spotoptim"] + assert ks["tensorboard_log"] is True + assert ks["tensorboard_clean"] is False + + +def test_spotoptim_strategy_no_clean_default_without_logging(monkeypatch): + """tensorboard_clean is only defaulted when tensorboard_log is enabled.""" + import pandas as pd + + import spotforecast2.model_selection as ms + + captured = {} + + def fake_search_forecaster(*args, **kwargs): + captured["kwargs_spotoptim"] = kwargs.get("kwargs_spotoptim") + results = pd.DataFrame({"params": [{"alpha": 1.0}], "lags": [[1, 2]]}) + return results, object() + + monkeypatch.setattr(ms, "spotoptim_search_forecaster", fake_search_forecaster) + + # tensorboard_log=False is forwarded (it is not None) but must not + # trigger the clean default. + task = _make_fake_task(tensorboard_log=False) + SpotOptimStrategy(search_space={"alpha": (0.1, 1.0)}).prepare_forecaster( + task, "A", _FakeForecaster(), y_train=None + ) + + ks = captured["kwargs_spotoptim"] + assert ks["tensorboard_log"] is False + assert "tensorboard_clean" not in ks + + def test_spotoptim_strategy_no_tensorboard_attrs_no_kwargs(monkeypatch): """Without tensorboard config attrs, no tensorboard keys are forwarded.""" import pandas as pd diff --git a/uv.lock b/uv.lock index 1fde484..64c0160 100644 --- a/uv.lock +++ b/uv.lock @@ -3604,7 +3604,7 @@ wheels = [ [[package]] name = "spotforecast2" -version = "5.1.0" +version = "5.1.1" source = { editable = "." } dependencies = [ { name = "astral" }, @@ -3684,7 +3684,7 @@ requires-dist = [ { name = "scikit-learn", specifier = ">=1.8.0" }, { name = "shap", specifier = ">=0.49.1" }, { name = "spotforecast2-safe", specifier = ">=18.0.0,<19" }, - { name = "spotoptim", specifier = ">=0.12.8" }, + { name = "spotoptim", specifier = ">=0.12.9" }, { name = "tqdm", specifier = ">=4.67.2" }, ] provides-extras = ["dev"] @@ -3724,7 +3724,7 @@ wheels = [ [[package]] name = "spotoptim" -version = "0.12.8" +version = "0.12.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "black" }, @@ -3747,9 +3747,9 @@ dependencies = [ { name = "ty" }, { name = "xgboost" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/16/5a/a61ed4072c98c69de68854013e49856a241c5875a452b963e4c2d85ea8b3/spotoptim-0.12.8.tar.gz", hash = "sha256:215dc6e9c7718f181f47a0d16b54faea7818dfd26a300f4aaacd83156db28506", size = 316306, upload-time = "2026-06-07T12:46:19.632Z" } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/aa/926c52ee29a5655b9d7f677d231e19fdcf1540558fa26169b0626736e302/spotoptim-0.12.8-py3-none-any.whl", hash = "sha256:9eb2c05232bd2bac16487ecf2f0ae437f8c7e4fca44b706551308be363604a03", size = 355920, upload-time = "2026-06-07T12:46:17.986Z" }, + { 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" }, ] [[package]]