From 21b908af3d0a264558ca1f4ad37895b0dc5e4dc0 Mon Sep 17 00:00:00 2001 From: Tony Bagnall Date: Fri, 10 Apr 2026 09:45:44 +0100 Subject: [PATCH 1/5] add a log function --- aeon/classification/hybrid/_hivecote_v2.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/aeon/classification/hybrid/_hivecote_v2.py b/aeon/classification/hybrid/_hivecote_v2.py index 090df74573..e4922879e6 100644 --- a/aeon/classification/hybrid/_hivecote_v2.py +++ b/aeon/classification/hybrid/_hivecote_v2.py @@ -327,6 +327,11 @@ def _predict_proba(self, X, return_component_probas=False) -> np.ndarray: # Make each instances probability array sum to 1 and return return dists / dists.sum(axis=1, keepdims=True) + def _log(self, message, level=1): + """Print a verbose message if the configured verbosity is high enough.""" + if self.verbose >= level: + print(message, flush=True) # noqa + @classmethod def _get_test_params(cls, parameter_set="default"): """Return testing parameter settings for the estimator. From c487567c8b7d31a1a67bc8ec56b5f523569282de Mon Sep 17 00:00:00 2001 From: Tony Bagnall Date: Fri, 10 Apr 2026 10:08:57 +0100 Subject: [PATCH 2/5] add log for each component --- aeon/classification/hybrid/_hivecote_v2.py | 109 +++++++++++++-------- 1 file changed, 66 insertions(+), 43 deletions(-) diff --git a/aeon/classification/hybrid/_hivecote_v2.py b/aeon/classification/hybrid/_hivecote_v2.py index e4922879e6..694a01d421 100644 --- a/aeon/classification/hybrid/_hivecote_v2.py +++ b/aeon/classification/hybrid/_hivecote_v2.py @@ -7,7 +7,7 @@ __maintainer__ = ["MatthewMiddlehurst", "TonyBagnall"] __all__ = ["HIVECOTEV2"] -from datetime import datetime +from time import perf_counter import numpy as np from sklearn.metrics import accuracy_score @@ -177,6 +177,14 @@ def _fit(self, X, y): ending in "_" and sets is_fitted flag to True. """ self._n_jobs = check_n_jobs(self.n_jobs) + total_start = perf_counter() + + self._log( + f"[HC2] Starting fit: n_cases={X.shape[0]}, " + f"n_channels={X.shape[1]}, n_timepoints={X.shape[2]}, " + f"n_jobs={self._n_jobs}", + level=1, + ) if self.stc_params is None: self._stc_params = {"n_shapelet_samples": HIVECOTEV2._DEFAULT_N_SHAPELETS} @@ -204,56 +212,41 @@ def _fit(self, X, y): self._tde_params["time_limit_in_minutes"] = ct # Build STC - self._stc = ShapeletTransformClassifier( - **self._stc_params, - random_state=self.random_state, - n_jobs=self._n_jobs, + self._stc, self.stc_weight_, train_acc = self._fit_component( + "STC", + ShapeletTransformClassifier, + self._stc_params, + X, + y, ) - train_preds = self._stc.fit_predict(X, y) - self.stc_weight_ = accuracy_score(y, train_preds) ** 4 - - if self.verbose > 0: - print("STC ", datetime.now().strftime("%H:%M:%S %d/%m/%Y")) # noqa - print("STC weight = " + str(self.stc_weight_)) # noqa - # Build DrCIF - self._drcif = DrCIFClassifier( - **self._drcif_params, - random_state=self.random_state, - n_jobs=self._n_jobs, + self._drcif, self.drcif_weight_, train_acc = self._fit_component( + "DrCIF", + DrCIFClassifier, + self._drcif_params, + X, + y, ) - train_preds = self._drcif.fit_predict(X, y) - self.drcif_weight_ = accuracy_score(y, train_preds) ** 4 - - if self.verbose > 0: - print("DrCIF ", datetime.now().strftime("%H:%M:%S %d/%m/%Y")) # noqa - print("DrCIF weight = " + str(self.drcif_weight_)) # noqa # Build Arsenal - self._arsenal = Arsenal( - **self._arsenal_params, - random_state=self.random_state, - n_jobs=self._n_jobs, + self._arsenal, self.arsenal_weight_, train_acc = self._fit_component( + "Arsenal", + Arsenal, + self._arsenal_params, + X, + y, ) - train_preds = self._arsenal.fit_predict(X, y) - self.arsenal_weight_ = accuracy_score(y, train_preds) ** 4 - - if self.verbose > 0: - print("Arsenal ", datetime.now().strftime("%H:%M:%S %d/%m/%Y")) # noqa - print("Arsenal weight = " + str(self.arsenal_weight_)) # noqa - - # Build TDE - self._tde = TemporalDictionaryEnsemble( - **self._tde_params, - random_state=self.random_state, - n_jobs=self._n_jobs, + + self._tde, self.tde_weight_, train_acc = self._fit_component( + "TDE", + TemporalDictionaryEnsemble, + self._tde_params, + X, + y, ) - train_preds = self._tde.fit_predict(X, y) - self.tde_weight_ = accuracy_score(y, train_preds) ** 4 - if self.verbose > 0: - print("TDE ", datetime.now().strftime("%H:%M:%S %d/%m/%Y")) # noqa - print("TDE weight = " + str(self.tde_weight_)) # noqa + total_elapsed = perf_counter() - total_start + self._log(f"[HC2] Finished fit in {total_elapsed:.2f}s", level=1) return self @@ -332,6 +325,36 @@ def _log(self, message, level=1): if self.verbose >= level: print(message, flush=True) # noqa + def _fit_component(self, name, estimator_cls, params, X, y): + """Fit a single HC2 component.""" + build_params = params.copy() + build_params.setdefault("random_state", self.random_state) + build_params.setdefault("n_jobs", self._n_jobs) + + if estimator_cls is DrCIFClassifier: + build_params.setdefault("parallel_backend", self.parallel_backend) + + self._log(f"[HC2] Starting {name}...", level=1) + if self.verbose >= 2: + self._log(f"[HC2] {name} params: {build_params}", level=2) + + start = perf_counter() + + estimator = estimator_cls(**build_params) + train_preds = estimator.fit_predict(X, y) + + train_acc = accuracy_score(y, train_preds) + weight = train_acc**4 + elapsed = perf_counter() - start + + self._log( + f"[HC2] Finished {name} in {elapsed:.2f}s, " + f"train_acc={train_acc:.4f}, weight={weight:.4f}", + level=1, + ) + + return estimator, weight, train_acc + @classmethod def _get_test_params(cls, parameter_set="default"): """Return testing parameter settings for the estimator. From c346d3c8fbd7ae44901a1d193aed8108cff19fe5 Mon Sep 17 00:00:00 2001 From: Tony Bagnall Date: Fri, 10 Apr 2026 10:42:53 +0100 Subject: [PATCH 3/5] restructure verbose, copy parameters --- aeon/classification/hybrid/_hivecote_v2.py | 98 +++++++++++++--------- 1 file changed, 59 insertions(+), 39 deletions(-) diff --git a/aeon/classification/hybrid/_hivecote_v2.py b/aeon/classification/hybrid/_hivecote_v2.py index 694a01d421..2af2242a74 100644 --- a/aeon/classification/hybrid/_hivecote_v2.py +++ b/aeon/classification/hybrid/_hivecote_v2.py @@ -185,34 +185,20 @@ def _fit(self, X, y): f"n_jobs={self._n_jobs}", level=1, ) + self._initialise_component_params() - if self.stc_params is None: - self._stc_params = {"n_shapelet_samples": HIVECOTEV2._DEFAULT_N_SHAPELETS} - if self.drcif_params is None: - self._drcif_params = {"n_estimators": HIVECOTEV2._DEFAULT_N_TREES} - if self.arsenal_params is None: - self._arsenal_params = { - "n_kernels": HIVECOTEV2._DEFAULT_N_KERNELS, - "n_estimators": HIVECOTEV2._DEFAULT_N_ESTIMATORS, - } - if self.tde_params is None: - self._tde_params = { - "n_parameter_samples": HIVECOTEV2._DEFAULT_N_PARA_SAMPLES, - "max_ensemble_size": HIVECOTEV2._DEFAULT_MAX_ENSEMBLE_SIZE, - "randomly_selected_params": HIVECOTEV2._DEFAULT_RAND_PARAMS, - } - - # If we are contracting split the contract time between each algorithm - if self.time_limit_in_minutes > 0: - # Leave 1/3 for train estimates - ct = self.time_limit_in_minutes / 6 - self._stc_params["time_limit_in_minutes"] = ct - self._drcif_params["time_limit_in_minutes"] = ct - self._arsenal_params["time_limit_in_minutes"] = ct - self._tde_params["time_limit_in_minutes"] = ct - + # Build Arsenal + self._arsenal, self.arsenal_weight_, self.arsenal_train_acc_ = ( + self._fit_component( + "Arsenal", + Arsenal, + self._arsenal_params, + X, + y, + ) + ) # Build STC - self._stc, self.stc_weight_, train_acc = self._fit_component( + self._stc, self.stc_weight_, self.stc_train_acc_ = self._fit_component( "STC", ShapeletTransformClassifier, self._stc_params, @@ -220,24 +206,14 @@ def _fit(self, X, y): y, ) # Build DrCIF - self._drcif, self.drcif_weight_, train_acc = self._fit_component( + self._drcif, self.drcif_weight_, self.drcif_train_acc_ = self._fit_component( "DrCIF", DrCIFClassifier, self._drcif_params, X, y, ) - - # Build Arsenal - self._arsenal, self.arsenal_weight_, train_acc = self._fit_component( - "Arsenal", - Arsenal, - self._arsenal_params, - X, - y, - ) - - self._tde, self.tde_weight_, train_acc = self._fit_component( + self._tde, self.tde_weight_, self.tde_train_acc_ = self._fit_component( "TDE", TemporalDictionaryEnsemble, self._tde_params, @@ -247,7 +223,14 @@ def _fit(self, X, y): total_elapsed = perf_counter() - total_start self._log(f"[HC2] Finished fit in {total_elapsed:.2f}s", level=1) - + self._log( + "[HC2] Component train accuracies and weights: " + f"STC={self.stc_train_acc_:.4f}, {self.stc_weight_:.4f}, " + f"DrCIF={self.drcif_train_acc_:.4f}, {self.drcif_weight_:.4f}, " + f"Arsenal={self.arsenal_train_acc_:.4f}, {self.arsenal_weight_:.4f}, " + f"TDE={self.tde_train_acc_:.4f}, {self.tde_weight_:.4f}", + level=1, + ) return self def _predict(self, X) -> np.ndarray: @@ -320,6 +303,43 @@ def _predict_proba(self, X, return_component_probas=False) -> np.ndarray: # Make each instances probability array sum to 1 and return return dists / dists.sum(axis=1, keepdims=True) + def _get_component_params(self, params, default_params): + """Return a working parameter dict for a HC2 component.""" + return default_params.copy() if params is None else params.copy() + + def _initialise_component_params(self): + """Initialise working parameter dictionaries for HC2 components.""" + self._stc_params = self._get_component_params( + self.stc_params, + {"n_shapelet_samples": HIVECOTEV2._DEFAULT_N_SHAPELETS}, + ) + self._drcif_params = self._get_component_params( + self.drcif_params, + {"n_estimators": HIVECOTEV2._DEFAULT_N_TREES}, + ) + self._arsenal_params = self._get_component_params( + self.arsenal_params, + { + "n_kernels": HIVECOTEV2._DEFAULT_N_KERNELS, + "n_estimators": HIVECOTEV2._DEFAULT_N_ESTIMATORS, + }, + ) + self._tde_params = self._get_component_params( + self.tde_params, + { + "n_parameter_samples": HIVECOTEV2._DEFAULT_N_PARA_SAMPLES, + "max_ensemble_size": HIVECOTEV2._DEFAULT_MAX_ENSEMBLE_SIZE, + "randomly_selected_params": HIVECOTEV2._DEFAULT_RAND_PARAMS, + }, + ) + + if self.time_limit_in_minutes > 0: + ct = self.time_limit_in_minutes / 6 + self._stc_params["time_limit_in_minutes"] = ct + self._drcif_params["time_limit_in_minutes"] = ct + self._arsenal_params["time_limit_in_minutes"] = ct + self._tde_params["time_limit_in_minutes"] = ct + def _log(self, message, level=1): """Print a verbose message if the configured verbosity is high enough.""" if self.verbose >= level: From 8ab32b640e515ffbfd11edae8fffc7a3f2e24801 Mon Sep 17 00:00:00 2001 From: Tony Bagnall Date: Fri, 10 Apr 2026 10:49:47 +0100 Subject: [PATCH 4/5] restructure verbose, copy parameters --- aeon/classification/hybrid/_hivecote_v2.py | 50 ++++++++++++---------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/aeon/classification/hybrid/_hivecote_v2.py b/aeon/classification/hybrid/_hivecote_v2.py index 2af2242a74..e16e9a9a9a 100644 --- a/aeon/classification/hybrid/_hivecote_v2.py +++ b/aeon/classification/hybrid/_hivecote_v2.py @@ -49,7 +49,10 @@ class HIVECOTEV2(BaseClassifier): When predict/predict_proba is called, save each HIVE-COTEV2 component probability predictions in component_probas. verbose : int, default=0 - Level of output printed to the console (for information only). + Level of output printed to the console + - 0: no output + - 1: HC2 level progress + - 2: also print component parameter summaries random_state : int, RandomState instance or None, default=None If `int`, random_state is the seed used by the random number generator; If `RandomState` instance, random_state is the random number generator; @@ -183,22 +186,11 @@ def _fit(self, X, y): f"[HC2] Starting fit: n_cases={X.shape[0]}, " f"n_channels={X.shape[1]}, n_timepoints={X.shape[2]}, " f"n_jobs={self._n_jobs}", - level=1, ) self._initialise_component_params() - # Build Arsenal - self._arsenal, self.arsenal_weight_, self.arsenal_train_acc_ = ( - self._fit_component( - "Arsenal", - Arsenal, - self._arsenal_params, - X, - y, - ) - ) # Build STC - self._stc, self.stc_weight_, self.stc_train_acc_ = self._fit_component( + self._stc, self.stc_weight_, stc_train_acc_ = self._fit_component( "STC", ShapeletTransformClassifier, self._stc_params, @@ -206,14 +198,23 @@ def _fit(self, X, y): y, ) # Build DrCIF - self._drcif, self.drcif_weight_, self.drcif_train_acc_ = self._fit_component( + self._drcif, self.drcif_weight_, drcif_train_acc_ = self._fit_component( "DrCIF", DrCIFClassifier, self._drcif_params, X, y, ) - self._tde, self.tde_weight_, self.tde_train_acc_ = self._fit_component( + # Build Arsenal + self._arsenal, self.arsenal_weight_, arsenal_train_acc_ = self._fit_component( + "Arsenal", + Arsenal, + self._arsenal_params, + X, + y, + ) + # Build TDE + self._tde, self.tde_weight_, tde_train_acc_ = self._fit_component( "TDE", TemporalDictionaryEnsemble, self._tde_params, @@ -222,14 +223,14 @@ def _fit(self, X, y): ) total_elapsed = perf_counter() - total_start - self._log(f"[HC2] Finished fit in {total_elapsed:.2f}s", level=1) + self._log(f"[HC2] Finished fit in {total_elapsed:.2f}s") self._log( - "[HC2] Component train accuracies and weights: " - f"STC={self.stc_train_acc_:.4f}, {self.stc_weight_:.4f}, " - f"DrCIF={self.drcif_train_acc_:.4f}, {self.drcif_weight_:.4f}, " - f"Arsenal={self.arsenal_train_acc_:.4f}, {self.arsenal_weight_:.4f}, " - f"TDE={self.tde_train_acc_:.4f}, {self.tde_weight_:.4f}", - level=1, + "[HC2] Component summary: " + f"STC(train_acc={stc_train_acc_:.4f}, weight={self.stc_weight_:.4f}), " + f"DrCIF(train_acc={drcif_train_acc_:.4f}, weight={self.drcif_weight_:.4f})," + f"Arsenal(train_acc={arsenal_train_acc_:.4f}, weight" + f"={self.arsenal_weight_:.4f}), " + f"TDE(train_acc={tde_train_acc_:.4f}, weight={self.tde_weight_:.4f})", ) return self @@ -335,6 +336,11 @@ def _initialise_component_params(self): if self.time_limit_in_minutes > 0: ct = self.time_limit_in_minutes / 6 + self._log( + f"[HC2] Contract time = {self.time_limit_in_minutes} minutes, " + f"per-component allocation = {ct:.4f} minutes", + level=1, + ) self._stc_params["time_limit_in_minutes"] = ct self._drcif_params["time_limit_in_minutes"] = ct self._arsenal_params["time_limit_in_minutes"] = ct From e744b39ed8dda2a93c9f6a3018c01a424f3fce6a Mon Sep 17 00:00:00 2001 From: Tony Bagnall Date: Fri, 10 Apr 2026 11:10:39 +0100 Subject: [PATCH 5/5] test logging --- aeon/classification/hybrid/_hivecote_v2.py | 3 ++- aeon/classification/hybrid/tests/test_hc.py | 27 +++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/aeon/classification/hybrid/_hivecote_v2.py b/aeon/classification/hybrid/_hivecote_v2.py index e16e9a9a9a..4a3f4e920e 100644 --- a/aeon/classification/hybrid/_hivecote_v2.py +++ b/aeon/classification/hybrid/_hivecote_v2.py @@ -49,7 +49,8 @@ class HIVECOTEV2(BaseClassifier): When predict/predict_proba is called, save each HIVE-COTEV2 component probability predictions in component_probas. verbose : int, default=0 - Level of output printed to the console + Level of output printed to the console. + - 0: no output - 1: HC2 level progress - 2: also print component parameter summaries diff --git a/aeon/classification/hybrid/tests/test_hc.py b/aeon/classification/hybrid/tests/test_hc.py index 651e10556f..bb7f959343 100644 --- a/aeon/classification/hybrid/tests/test_hc.py +++ b/aeon/classification/hybrid/tests/test_hc.py @@ -59,3 +59,30 @@ def test_hc2_defaults_and_verbosity(): HIVECOTEV2._DEFAULT_N_PARA_SAMPLES = 250 HIVECOTEV2._DEFAULT_MAX_ENSEMBLE_SIZE = 50 HIVECOTEV2._DEFAULT_RAND_PARAMS = 50 + + +@pytest.mark.skipif(PR_TESTING, reason="slow test, run overnight only") +def test_hc2_logging(capsys): + """Test logging for HC2.""" + X, y = make_example_3d_numpy(n_cases=10, n_timepoints=10, n_labels=2) + hc2 = HIVECOTEV2(verbose=1) + hc2.fit(X, y) + + out = capsys.readouterr().out + + assert "[HC2] Starting fit:" in out + + assert "[HC2] Starting STC..." in out + assert "[HC2] Finished STC in " in out + + assert "[HC2] Starting DrCIF..." in out + assert "[HC2] Finished DrCIF in " in out + + assert "[HC2] Starting Arsenal..." in out + assert "[HC2] Finished Arsenal in " in out + + assert "[HC2] Starting TDE..." in out + assert "[HC2] Finished TDE in " in out + + assert "[HC2] Finished fit in " in out + assert "[HC2] Component summary:" in out