From 344eeacda68f11a5bdb1499cae37d853e6beb7c0 Mon Sep 17 00:00:00 2001 From: Romeo Kienzler Date: Mon, 29 Jun 2026 14:34:03 +0200 Subject: [PATCH 1/2] feat(iterate2): support per-metric direction (minimize/maximize) - Add _default_direction() heuristic: metrics containing loss/error/err/mse/mae/rmse default to minimize, all others to maximize - Extend load_metrics() to return (names, directions) tuple; accept both simple string form and extended {name, direction} dict form in the YAML metrics: section - Remove hardcoded ['maximize'] * len(metrics) in main(); use directions from load_metrics() instead - Log directions at startup - Update docs/iterate2.md with new Metric directions subsection Signed-off-by: Romeo Kienzler --- docs/iterate2.md | 27 +++++++- terratorch_iterate/iterate2/_iterate2.py | 81 ++++++++++++++++++++---- 2 files changed, 95 insertions(+), 13 deletions(-) diff --git a/docs/iterate2.md b/docs/iterate2.md index 0a84e261..62c4ed6c 100644 --- a/docs/iterate2.md +++ b/docs/iterate2.md @@ -234,13 +234,38 @@ The **last** occurrence of the pattern `: ` or ` dict: logger.info("Static params: %d key(s): %s", len(static), list(static.keys())) return static -def load_metrics(data: dict, fallback: str = "score") -> List[str]: +_MINIMIZE_KEYWORDS = ("loss", "error", "err", "mse", "mae", "rmse") + +def _default_direction(metric_name: str) -> str: + """Heuristic: metrics whose name contains a loss/error keyword are minimized.""" + lower = metric_name.lower() + if any(kw in lower for kw in _MINIMIZE_KEYWORDS): + return "minimize" + return "maximize" + +def load_metrics(data: dict, fallback: str = "score") -> tuple[List[str], List[str]]: + """Return (metric_names, directions). + + The YAML ``metrics:`` section accepts two forms: + + Simple (direction inferred from name):: + + metrics: + - val_loss # → minimize (contains "loss") + - accuracy # → maximize + + Extended (direction explicit):: + + metrics: + - name: val_loss + direction: minimize + - name: accuracy + direction: maximize + + A mix of both forms is allowed. + """ raw = data.get("metrics", None) if raw is None: logger.warning("No 'metrics:' key in YAML – defaulting to '%s'", fallback) - return [fallback] - if isinstance(raw, list): - return [str(m).strip() for m in raw] - return [m.strip() for m in str(raw).split(",")] + return [fallback], [_default_direction(fallback)] + + if not isinstance(raw, list): + # comma-separated string fallback + names = [m.strip() for m in str(raw).split(",")] + directions = [_default_direction(n) for n in names] + return names, directions + + names: List[str] = [] + directions: List[str] = [] + for item in raw: + if isinstance(item, dict): + name = str(item["name"]).strip() + direction = str(item.get("direction", _default_direction(name))).strip().lower() + if direction not in ("minimize", "maximize"): + raise ValueError( + f"Invalid direction '{direction}' for metric '{name}'. " + "Must be 'minimize' or 'maximize'." + ) + else: + name = str(item).strip() + direction = _default_direction(name) + names.append(name) + directions.append(direction) + return names, directions # ─── OPTUNA PARAM SAMPLING ─────────────────────────────────────────────────── @@ -216,12 +273,12 @@ def main(): args.optuna_study_name, args.optuna_db_path, args.optuna_n_trials, args.parallelism) - data = load_yaml(args.hpo_yaml) - hpo_space = load_hpo_space(data) - static = load_static(data) - metrics = load_metrics(data) - directions = ["maximize"] * len(metrics) + data = load_yaml(args.hpo_yaml) + hpo_space = load_hpo_space(data) + static = load_static(data) + metrics, directions = load_metrics(data) logger.info("Metrics: %s", metrics) + logger.info("Directions: %s", directions) storage = resolve_storage(args.optuna_db_path) study = optuna.create_study( From 29d33f2e9b450845b771c7e3caa74766bdc702dc Mon Sep 17 00:00:00 2001 From: Romeo Kienzler Date: Mon, 29 Jun 2026 15:10:39 +0200 Subject: [PATCH 2/2] =?UTF-8?q?ci:=20fix=20integration-test=20workflow=20?= =?UTF-8?q?=E2=80=93=20install=20deps=20and=20run=20unit=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace broken 'python ./run_tests.py' (LSF/bsub job submitter, not runnable in GitHub Actions) with 'pytest tests/unit/test_iterate2.py' - Install pytest, pyyaml, optuna and the package itself (--no-deps) so the test module can be imported without a ModuleNotFoundError for yaml Signed-off-by: Romeo Kienzler --- .github/workflows/integration-test.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 47ca0c24..191185bf 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -26,9 +26,8 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r test_requirements.txt - - name: Full integration test + pip install pytest pyyaml optuna + pip install -e . --no-deps + - name: Unit tests run: | - # `./component-library` was removed in the repo reorganization; - # run_tests.py is at the repo root. - python ./run_tests.py + pytest tests/unit/test_iterate2.py -v