Repository: sktime/sktime
Pull request #6289 (2024-04-11 21:12:30)

[ENH] enhancements to tag system, systematic API docs for tags
This PR upgrades the tag system with improved inspectability, and adds systematic API docs for the tags.

The changes are as following:

* tags are now changed to be (private) first-class objects. This allows to attach more extensive documentation to tags, including examples or references. It also allows to attach tags to tags, e.g., the meta-tag fields currently in `registry._tags`, or a property such as whether a tag is meant to be user facing.
    * longer-term, it could also allow to attach validation functionality to a tag, in the form of methods, or enable dispatch, or nested dispatch.
* an API reference page for tags is added. This (ab)uses sphinx autodoc to generate tables and individual glossary pages for tags, by using the class template.


Patch:
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 7695c0f52..5f567c577 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -2756,6 +2756,15 @@
         "doc"
       ]
     },
+    {
+      "login": "ssabarwal",
+      "name": "Shlok Sabarwal",
+      "avatar_url": "https://gravatar.com/avatar/cbdbaac712ae282d730cd3028e862d45?s=400&d=robohash&r=x",
+      "prifle": "https://www.github.com/shlok191/",
+      "contributions": [
+        "code"
+      ]
+    },
     {
       "login": "mobley-trent",
       "name": "Eddy Oyieko",
diff --git a/README.md b/README.md
index 9d74745e2..8181992d5 100644
--- a/README.md
+++ b/README.md
@@ -54,7 +54,7 @@ Questions and feedback are extremely welcome! We strongly believe in the value o
 | :woman_technologist: **Usage Questions**          | [GitHub Discussions] · [Stack Overflow] |
 | :speech_balloon: **General Discussion**        | [GitHub Discussions] |
 | :factory: **Contribution & Development** | `dev-chat` channel · [Discord] |
-| :globe_with_meridians: **Community collaboration session** | [Discord] - Fridays 3 pm UTC, dev/meet-ups channel |
+| :globe_with_meridians: **Meet-ups and collaboration sessions** | [Discord] - Fridays 4 pm UTC, dev/meet-ups channel |
 
 [github issue tracker]: https://github.com/sktime/sktime/issues
 [github discussions]: https://github.com/sktime/sktime/discussions
diff --git a/docs/Makefile b/docs/Makefile
deleted file mode 100644
index 957e8aa08..000000000
--- a/docs/Makefile
+++ /dev/null
@@ -1,29 +0,0 @@
-# Minimal makefile for Sphinx documentation
-
-# You can set these variables from the command line.
-PREPROCESS    = sphinx-apidoc
-APIDOCTEMPLATEDIR = source/_templates/apidoc
-AUTOGENDIR    = source/api_reference/auto_generated
-SPHINXOPTS    =
-SPHINXBUILD   = sphinx-build
-SOURCEDIR     = source
-BUILDDIR      = _build
-PROJECTDIR    = "../sktime/"
-
-# exclude unit tests from API reference
-EXCLUDEPATHS = "../*tests* ../*test* ../*contrib* ../*setup* ../*all*"
-
-# Put it first so that "make" without argument is like "make help".
-help:
-	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
-
-.PHONY: help Makefile
-
-# Catch-all target: route all unknown targets to Sphinx using the new
-# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
-%: Makefile
-	./symlink_examples.sh
-	rm -rf $(AUTOGENDIR)
-	@$(PREPROCESS) -eMT --templatedir $(APIDOCTEMPLATEDIR) -o $(AUTOGENDIR) $(PROJECTDIR) "$(EXCLUDEPATHS)"
-	-rm -rf $(BUILDDIR)
-	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/docs/source/Makefile b/docs/source/Makefile
new file mode 100644
index 000000000..f6a880f8f
--- /dev/null
+++ b/docs/source/Makefile
@@ -0,0 +1,32 @@
+# Minimal makefile for Sphinx documentation
+
+# You can set these variables from the command line.
+SPHINXBUILD        = sphinx-build
+SPHINXOPTS         =
+SPHINXAUTOBUILD    = sphinx-autobuild
+SPHINXAUTOOPTS     =
+SOURCEDIR          = .
+BUILDDIR           = ../build
+SKTIMEDIR          = ../../sktime
+
+.PHONY: help build autobuild
+
+# "make" without argument is like "make help".
+help:
+	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)"
+
+clean:
+	rm -rf $(BUILDDIR)
+	@echo "Deleted directory $(BUILDDIR)."
+
+# $(O) is meant as a shortcut for custom options.
+# i.e to log stderr into a seprate file:
+# make build O="--no-color 2> build_warnings.log"
+html:
+#	./symlink_examples.sh
+	$(SPHINXBUILD) -M html "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+# $(O) is meant as a shortcut for custom options.
+autobuild:
+#   use --pre-build COMMAND to call ./symlink_examples.sh
+	$(SPHINXAUTOBUILD) "$(SOURCEDIR)" "$(BUILDDIR)/html" -d "$(BUILDDIR)/doctrees" $(SPHINXAUTOOPTS) $(O) --watch "$(SKTIMEDIR)" --re-ignore ".*\.json"
diff --git a/docs/source/api_reference.rst b/docs/source/api_reference.rst
index 9dc6307d5..122ea2138 100644
--- a/docs/source/api_reference.rst
+++ b/docs/source/api_reference.rst
@@ -8,8 +8,11 @@ Welcome to the API reference for ``sktime``.
 
 The API reference provides a technical manual.
 It describes the classes and functions included in sktime.
+
 For a scientific manual, see the :ref:`user_guide`.
 
+For a list of object and estimator tags, see :ref:`tags_ref`.
+
 .. include:: includes/api_css.rst
 
 .. toctree::
@@ -32,4 +35,5 @@ For a scientific manual, see the :ref:`user_guide`.
     api_reference/deployment
     api_reference/utils
     api_reference/exceptions
+    api_reference/tags
     api_reference/pipeline
diff --git a/docs/source/api_reference/tags.rst b/docs/source/api_reference/tags.rst
new file mode 100644
index 000000000..37de75a25
--- /dev/null
+++ b/docs/source/api_reference/tags.rst
@@ -0,0 +1,112 @@
+.. _tags_ref:
+
+Object and estimator tags
+=========================
+
+Every first-class object in ``sktime``
+is tagged with a set of tags that describe its properties and capabilities,
+or control its behavior.
+
+Tags are key-value pairs, where the key is a string with the name of the tag.
+The value of the tag can have arbitrary type, and describes a property, capability,
+or controls behaviour of the object, depending on the value.
+
+For instance, a forecaster may have the tag ``capability:pred_int: True`` if it can
+make probabilistic predictions.
+Users can find all forecasters that can make probabilistic predictions by filtering
+for this tag.
+
+This API reference lists all tags available in ``sktime``, and key utilities
+for their usage.
+
+
+Inspecting tags, retrieving by tags
+-----------------------------------
+
+* to get the tags of an object, use the ``get_tags`` method.
+  An object's tags can depend on its hyper-parameters.
+* to get the tags of a class, use the ``get_tags`` method of the class.
+  A class's tags are static and do not depend on its hyper-parameters.
+  By default, class tags that may vary for instances take the most "capable" value,
+  in the case of capabilities.
+* to programmatically retrieve all tags available in ``sktime``
+  or for a particular type of object, at runtime, use the ``registry.all_tags`` utility
+* to programmatically retrieve all objects or estimators in ``sktime``,
+  filtered for values of tags, use the ``registry.all_estimators`` utility
+
+
+.. currentmodule:: sktime.registry
+
+.. autosummary::
+    :toctree: auto_generated/
+    :template: function.rst
+
+    all_tags
+    all_estimators
+
+
+General tags, packaging
+-----------------------
+
+This section lists tags that are general and apply to all objects in ``sktime``.
+These tags are typically used for typing, packaging and documentation purposes.
+
+.. currentmodule:: sktime.registry._tags
+
+.. autosummary::
+    :toctree: auto_generated/
+    :template: function.rst
+    :nosignatures:
+
+    object_type
+    maintainers
+    authors
+    python_version
+    python_dependencies
+    python_dependencies_alias
+    requires_cython
+
+
+.. _forecaster_tags:
+
+Forecaster tags
+---------------
+
+This section lists tags applying to forecasters.
+These tags are used to describe capabilities, properties, and behavior of forecasters.
+
+The list also includes some developer facing tags that are used to
+control internal behavior of the forecaster.
+
+.. currentmodule:: sktime.registry._tags
+
+.. autosummary::
+    :toctree: auto_generated/
+    :template: function.rst
+    :nosignatures:
+
+    capability__exogeneous
+    capability__insample
+    capability__pred_int
+    capability__pred_int__insample
+    capability__missing_values
+
+
+.. _panel_tags:
+
+Tags for classifiers, regressors, clustering
+--------------------------------------------
+
+.. currentmodule:: sktime.registry._tags
+
+.. autosummary::
+    :toctree: auto_generated/
+    :template: function.rst
+    :nosignatures:
+
+    capability__multivariate
+    capability__missing_values
+    capability__unequal_length
+    capability__feature_importance
+    capability__contractable
+    capability__train_estimate
diff --git a/docs/source/developer_guide/documentation.rst b/docs/source/developer_guide/documentation.rst
index 53b651e87..4b270de78 100644
--- a/docs/source/developer_guide/documentation.rst
+++ b/docs/source/developer_guide/documentation.rst
@@ -197,15 +197,36 @@ To build the documentation locally, you need to install a few extra
 dependencies listed in
 `pyproject.toml <https://github.com/sktime/sktime/blob/main/pyproject.toml>`__.
 
-1. To install extra dependencies from the root directory, run:
+1. To install extra dependencies for building documentation, run:
 
    .. code:: bash
 
-      pip install .[docs]
+      pip install --editable .[docs]
 
-2. To build the website locally, run:
+2. To build the website locally, navigate to ``docs/source`` and execute:
 
    .. code:: bash
 
-      cd docs
       make html
+
+   Please note that the initial build may take up to 20 minutes. Subsequent builds for incremental changes will be significantly faster.
+
+3. For a clean build, run:
+
+   .. code:: bash
+
+      make clean html
+
+4. For live-reloading while editing the documentation, you can use ``sphinx-autobuild``.
+
+   First, install ``sphinx-autobuild`` by running:
+
+   .. code:: bash
+
+      pip install sphinx-autobuild
+
+   Then in ``docs/source``, run:
+
+   .. code:: bash
+
+      make autobuild
diff --git a/docs/make.bat b/docs/source/make.bat
similarity index 94%
rename from docs/make.bat
rename to docs/source/make.bat
index 4d9eb83d9..9b565142a 100644
--- a/docs/make.bat
+++ b/docs/source/make.bat
@@ -7,8 +7,8 @@ REM Command file for Sphinx documentation
 if "%SPHINXBUILD%" == "" (
 	set SPHINXBUILD=sphinx-build
 )
-set SOURCEDIR=source
-set BUILDDIR=build
+set SOURCEDIR=.
+set BUILDDIR=../build
 
 if "%1" == "" goto help
 
diff --git a/docs/symlink_examples.sh b/docs/source/symlink_examples.sh
similarity index 71%
rename from docs/symlink_examples.sh
rename to docs/source/symlink_examples.sh
index 09cecf225..d9aae6cff 100755
--- a/docs/symlink_examples.sh
+++ b/docs/source/symlink_examples.sh
@@ -4,13 +4,13 @@
 # notebooks in example folder
 
 # remove all
-rm -rf source/examples/*
+rm -rf examples/*
 
 # cd into website folder
-cd source/examples/ || return
+cd examples/ || return
 
 # create symbolic links in website folder
-ln -s ../../../examples/* .
+ln -s ../../examples/* .
 
 # return to initial folder
 cd - || exit
diff --git a/pyproject.toml b/pyproject.toml
index 306802c43..03e5f0853 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -207,7 +207,7 @@ regression = [
 transformations = [
   'esig<0.10,>=0.9.7; python_version < "3.11"',
   "filterpy<1.5,>=1.4.5",
-  "holidays>=0.29,<0.47",
+  "holidays>=0.29,<0.48",
   "mne>=1.5,<1.7",
   'numba<0.60,>=0.53',
   "pycatch22>=0.4,<0.4.4",
diff --git a/sktime/clustering/k_means/_k_means_tslearn.py b/sktime/clustering/k_means/_k_means_tslearn.py
index 62cb2ff5c..af37ec34a 100644
--- a/sktime/clustering/k_means/_k_means_tslearn.py
+++ b/sktime/clustering/k_means/_k_means_tslearn.py
@@ -107,7 +107,7 @@ class TimeSeriesKMeansTslearn(_TslearnAdapter, BaseClusterer):
         # estimator type
         # --------------
         "capability:multivariate": True,
-        "capability:unequal_length": True,
+        "capability:unequal_length": False,
     }
 
     # defines the name of the attribute containing the tslearn estimator
diff --git a/sktime/clustering/k_shapes.py b/sktime/clustering/k_shapes.py
index 0167087f8..aa53222e3 100755
--- a/sktime/clustering/k_shapes.py
+++ b/sktime/clustering/k_shapes.py
@@ -57,7 +57,7 @@ class TimeSeriesKShapes(_TslearnAdapter, BaseClusterer):
         # estimator type
         # --------------
         "capability:multivariate": True,
-        "capability:unequal_length": True,
+        "capability:unequal_length": False,
     }
 
     # defines the name of the attribute containing the tslearn estimator
diff --git a/sktime/forecasting/base/adapters/_neuralforecast.py b/sktime/forecasting/base/adapters/_neuralforecast.py
index 4de57629b..a33a6f233 100644
--- a/sktime/forecasting/base/adapters/_neuralforecast.py
+++ b/sktime/forecasting/base/adapters/_neuralforecast.py
@@ -2,15 +2,18 @@
 """Implements adapter for NeuralForecast models."""
 import abc
 import functools
+from copy import deepcopy
+from inspect import signature
 from typing import List, Literal, Optional, Union
 
 import numpy as np
 import pandas
 
 from sktime.forecasting.base import BaseForecaster, ForecastingHorizon
+from sktime.utils.warnings import warn
 
 __all__ = ["_NeuralForecastAdapter"]
-__author__ = ["yarnabrina", "geetu040"]
+__author__ = ["yarnabrina", "geetu040", "pranavvp16"]
 
 
 class _NeuralForecastAdapter(BaseForecaster):
@@ -134,16 +137,81 @@ class _NeuralForecastAdapter(BaseForecaster):
         - custom model name (``alias``) - used from ``algorithm_name``
         """
 
+    def _get_valid_parameters(self: "_NeuralForecastAdapter") -> dict:
+        """Get valid parameters for the underlying NeuralForecast algorithm class.
+
+        Returns
+        -------
+        dict
+            valid list of arguments for the underlying algorithm class
+        """
+        from pytorch_lightning import Trainer
+
+        # get valid init parameters from the model class and Trainer
+        model_class = self.algorithm_class
+        trainer_params = list(signature(Trainer.__init__).parameters.keys())
+        valid_parameters = list(signature(model_class.__init__).parameters.keys())
+        valid_parameters += trainer_params
+
+        sktime_parameters = self.algorithm_parameters
+        default_parameters = self.get_param_defaults()
+
+        valid_parameters = set(valid_parameters)
+        unsupported_parameters = set(sktime_parameters.keys()) - valid_parameters
+
+        # iterate through neuralforecast parameters and check for non default values
+        user_parameters = [
+            key
+            for key in sktime_parameters
+            if sktime_parameters[key] != default_parameters[key]
+        ]
+
+        # filter out unsupported parameters
+        sktime_trainer_params = set(self.algorithm_parameters["trainer_kwargs"])
+        invalid_trainer_params = sktime_trainer_params - set(trainer_params)
+
+        filter_params = deepcopy(self.algorithm_parameters)
+        for invalid_param in invalid_trainer_params:
+            warn(
+                f"Keyword argument '{invalid_param}' will be omitted as it is"
+                f" not found in the __init__ method "
+                f"from {Trainer}. "
+                f"Check your pytorch_lightning version "
+                f"to find out the right API parameters.",
+                obj=self,
+                stacklevel=2,
+            )
+            filter_params["trainer_kwargs"].pop(invalid_param)
+
+        for unsupported_param in unsupported_parameters:
+            if unsupported_param in user_parameters:
+                warn(
+                    f"Keyword argument '{unsupported_param}' will be omitted as it is"
+                    f" not found in the __init__ method "
+                    f"from {self.algorithm_class}. "
+                    f"Check your neuralforecast version "
+                    f"to find out the right API parameters.",
+                    obj=self,
+                    stacklevel=2,
+                )
+            filter_params.pop(unsupported_param)
+
+        return filter_params
+
     def _instantiate_model(self: "_NeuralForecastAdapter", fh: ForecastingHorizon):
         """Instantiate the model."""
         exogenous_parameters = (
             {"futr_exog_list": self.futr_exog_list} if self.needs_X else {}
         )
 
+        # filter params according to neuralforecast version
+        params = self._get_valid_parameters()
+        trainer_kwargs = params.pop("trainer_kwargs")
         algorithm_instance = self.algorithm_class(
             fh,
             alias=self.algorithm_name,
-            **self.algorithm_parameters,
+            **params,
+            **trainer_kwargs,
             **exogenous_parameters,
         )
 
diff --git a/sktime/forecasting/naive.py b/sktime/forecasting/naive.py
index 74a6b7233..c1314efd5 100644
--- a/sktime/forecasting/naive.py
+++ b/sktime/forecasting/naive.py
@@ -357,7 +357,6 @@ class NaiveForecaster(_BaseWindowForecaster):
         lagger = Lag(1, keep_column_names=True, freq=freq)
 
         expected_index = fh.to_absolute(cutoff).to_pandas()
-
         if strategy == "last" and sp == 1:
             y_old = lagger.fit_transform(_y)
             y_new = pd.DataFrame(index=expected_index, columns=[0], dtype="float64")
diff --git a/sktime/forecasting/neuralforecast.py b/sktime/forecasting/neuralforecast.py
index f7de9b950..fbf556853 100644
--- a/sktime/forecasting/neuralforecast.py
+++ b/sktime/forecasting/neuralforecast.py
@@ -6,7 +6,7 @@ from typing import List, Literal, Optional, Union
 from sktime.forecasting.base.adapters._neuralforecast import _NeuralForecastAdapter
 from sktime.utils.validation._dependencies import _check_soft_dependencies
 
-__author__ = ["yarnabrina", "geetu040"]
+__author__ = ["yarnabrina", "geetu040", "pranavvp16"]
 
 
 class NeuralForecastRNN(_NeuralForecastAdapter):
@@ -96,6 +96,10 @@ class NeuralForecastRNN(_NeuralForecastAdapter):
         whether ``TimeSeriesDataLoader`` drops last non-full batch
     trainer_kwargs : dict (default=None)
         keyword trainer arguments inherited from PyTorch Lighning's trainer [6]_
+    optimizer : pytorch optimizer (default=None) [7]_
+        optimizer to use for training, if passed with None defaults to Adam
+    optimizer_kwargs : dict (default=None) [8]_
+        dict of parameters to pass to the user defined optimizer
 
     Notes
     -----
@@ -147,6 +151,8 @@ class NeuralForecastRNN(_NeuralForecastAdapter):
     .. [5] https://nixtlaverse.nixtla.io/neuralforecast/losses.pytorch.html
     .. [6]
     https://lightning.ai/docs/pytorch/stable/api/pytorch_lightning.trainer.trainer.Trainer.html#lightning.pytorch.trainer.trainer.Trainer
+    .. [7] https://pytorch.org/docs/stable/optim.html
+    .. [8] https://pytorch.org/docs/stable/generated/torch.optim.Adam.html#torch.optim.Adam
     """  # noqa: E501
 
     _tags = {
@@ -194,6 +200,8 @@ class NeuralForecastRNN(_NeuralForecastAdapter):
         num_workers_loader=0,
         drop_last_loader=False,
         trainer_kwargs: Optional[dict] = None,
+        optimizer=None,
+        optimizer_kwargs: dict = None,
     ):
         self.input_size = input_size
         self.inference_input_size = inference_input_size
@@ -218,6 +226,8 @@ class NeuralForecastRNN(_NeuralForecastAdapter):
         self.random_seed = random_seed
         self.num_workers_loader = num_workers_loader
         self.drop_last_loader = drop_last_loader
+        self.optimizer = optimizer
+        self.optimizer_kwargs = optimizer_kwargs
         self.trainer_kwargs = trainer_kwargs
 
         super().__init__(
@@ -297,7 +307,9 @@ class NeuralForecastRNN(_NeuralForecastAdapter):
             "random_seed": self.random_seed,
             "num_workers_loader": self.num_workers_loader,
             "drop_last_loader": self.drop_last_loader,
-            **self._trainer_kwargs,
+            "optimizer": self.optimizer,
+            "optimizer_kwargs": self.optimizer_kwargs,
+            "trainer_kwargs": self._trainer_kwargs,
         }
 
     @classmethod
@@ -324,6 +336,7 @@ class NeuralForecastRNN(_NeuralForecastAdapter):
 
         try:
             _check_soft_dependencies("neuralforecast", severity="error")
+            _check_soft_dependencies("torch", severity="error")
         except ModuleNotFoundError:
             params = [
                 {
@@ -346,6 +359,7 @@ class NeuralForecastRNN(_NeuralForecastAdapter):
             ]
         else:
             from neuralforecast.losses.pytorch import SMAPE, QuantileLoss
+            from torch.optim import Adam
 
             params = [
                 {
@@ -366,6 +380,8 @@ class NeuralForecastRNN(_NeuralForecastAdapter):
                     "max_steps": 4,
                     "val_check_steps": 2,
                     "trainer_kwargs": {"logger": False},
+                    "optimizer": Adam,
+                    "optimizer_kwargs": {"lr": 0.001},
                 },
             ]
 
@@ -456,6 +472,10 @@ class NeuralForecastLSTM(_NeuralForecastAdapter):
         whether `TimeSeriesDataLoader` drops last non-full batch
     trainer_kwargs : dict (default=None)
         keyword trainer arguments inherited from PyTorch Lighning's trainer [6]_
+    optimizer : pytorch optimizer (default=None) [7]_
+        optimizer to use for training, if passed with None defaults to Adam
+    optimizer_kwargs : dict (default=None) [8]_
+        dict of parameters to pass to the user defined optimizer
 
     Notes
     -----
@@ -503,6 +523,8 @@ class NeuralForecastLSTM(_NeuralForecastAdapter):
     .. [4] https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases
     .. [5] https://nixtlaverse.nixtla.io/neuralforecast/losses.pytorch.html
     .. [6] https://lightning.ai/docs/pytorch/stable/api/pytorch_lightning.trainer.trainer.Trainer.html#lightning.pytorch.trainer.trainer.Trainer
+    .. [7] https://pytorch.org/docs/stable/optim.html
+    .. [8] https://pytorch.org/docs/stable/generated/torch.optim.Adam.html#torch.optim.Adam
     """  # noqa: E501
 
     _tags = {
@@ -549,6 +571,8 @@ class NeuralForecastLSTM(_NeuralForecastAdapter):
         num_workers_loader=0,
         drop_last_loader=False,
         trainer_kwargs: Optional[dict] = None,
+        optimizer=None,
+        optimizer_kwargs: dict = None,
     ):
         self.input_size = input_size
         self.inference_input_size = inference_input_size
@@ -572,6 +596,8 @@ class NeuralForecastLSTM(_NeuralForecastAdapter):
         self.random_seed = random_seed
         self.num_workers_loader = num_workers_loader
         self.drop_last_loader = drop_last_loader
+        self.optimizer = optimizer
+        self.optimizer_kwargs = optimizer_kwargs
         self.trainer_kwargs = trainer_kwargs
 
         super().__init__(
@@ -649,7 +675,9 @@ class NeuralForecastLSTM(_NeuralForecastAdapter):
             "random_seed": self.random_seed,
             "num_workers_loader": self.num_workers_loader,
             "drop_last_loader": self.drop_last_loader,
-            **self._trainer_kwargs,
+            "optimizer": self.optimizer,
+            "optimizer_kwargs": self.optimizer_kwargs,
+            "trainer_kwargs": self._trainer_kwargs,
         }
 
     @classmethod
@@ -672,6 +700,7 @@ class NeuralForecastLSTM(_NeuralForecastAdapter):
 
         try:
             _check_soft_dependencies("neuralforecast", severity="error")
+            _check_soft_dependencies("torch", severity="error")
         except ModuleNotFoundError:
             params = [
                 {
@@ -694,6 +723,7 @@ class NeuralForecastLSTM(_NeuralForecastAdapter):
             ]
         else:
             from neuralforecast.losses.pytorch import SMAPE, QuantileLoss
+            from torch.optim import Adam
 
             params = [
                 {
@@ -714,6 +744,8 @@ class NeuralForecastLSTM(_NeuralForecastAdapter):
                     "max_steps": 4,
                     "val_check_steps": 2,
                     "trainer_kwargs": {"logger": False},
+                    "optimizer": Adam,
+                    "optimizer_kwargs": {"lr": 0.001},
                 },
             ]
 
diff --git a/sktime/networks/lstmfcn.py b/sktime/networks/lstmfcn.py
index b2b80e5c4..991b5c472 100644
--- a/sktime/networks/lstmfcn.py
+++ b/sktime/networks/lstmfcn.py
@@ -123,3 +123,43 @@ class LSTMFCNNetwork(BaseDeepNetwork):
         output_layer = keras.layers.concatenate([x, y])
 
         return input_layer, output_layer
+
+    @classmethod
+    def get_test_params(cls, parameter_set="default"):
+        """Return testing parameter settings for the estimator.
+
+        Parameters
+        ----------
+        parameter_set : str, default="default"
+            Name of the set of test parameters to return, for use in tests. If no
+            special parameters are defined for a value, will return `"default"` set.
+
+        Returns
+        -------
+        params : dict or list of dict, default = {}
+            Parameters to create testing instances of the class
+            Each dict are parameters to construct an "interesting" test instance, i.e.,
+            `MyClass(**params)` or `MyClass(**params[i])` creates a valid test instance.
+            `create_test_instance` uses the first (or only) dictionary in `params`
+        """
+        params = [
+            # Advanced model version
+            {
+                "kernel_sizes": (8, 5, 3),  # Keep standard kernel sizes
+                "filter_sizes": (128, 256, 128),  # Keep standard kernel counts
+                "lstm_size": 8,
+                "dropout": 0.25,  # Maintain lower dropout rate for attention model
+                "attention": True,
+            },
+            # Simpler model version
+            {
+                "kernel_sizes": (4, 2, 1),  # Reduce kernel sizes
+                "filter_sizes": (32, 64, 32),  # Reduc filter sizes for cheaper model
+                "lstm_size": 8,  # Keeping LSTM output size fixed
+                "dropout": 0.75,  # Maintain higher dropout rate for non attention model
+                "attention": False,
+            },
+            {},
+        ]
+
+        return params
diff --git a/sktime/registry/_lookup.py b/sktime/registry/_lookup.py
index 4952ffc57..1adbcf7b6 100644
--- a/sktime/registry/_lookup.py
+++ b/sktime/registry/_lookup.py
@@ -48,7 +48,7 @@ def all_estimators(
     return_tags=None,
     suppress_import_stdout=True,
 ):
-    """Get a list of all estimators from sktime.
+    """List all estimators or objects in sktime, by scitype or tag.
 
     This function crawls the module and gets all classes that inherit
     from sktime's and sklearn's base classes.
@@ -156,6 +156,7 @@ def all_estimators(
         "plotting",
         "_split",
         "test_split",
+        "registry",
     )
 
     ROOT = str(Path(__file__).parent.parent)  # sktime package root directory
@@ -274,7 +275,7 @@ def all_tags(
     estimator_types=None,
     as_dataframe=False,
 ):
-    """Get a list of all tags from sktime.
+    """List all tags in sktime, for objects of a certain type.
 
     All objects in ``sktime`` are tagged with a set of :term``tag``s.
     This function allows to list all tags, optionally filtered by
diff --git a/sktime/registry/_tags.py b/sktime/registry/_tags.py
index 22933dc08..cdbee7d27 100644
--- a/sktime/registry/_tags.py
+++ b/sktime/registry/_tags.py
@@ -38,23 +38,742 @@ ESTIMATOR_TAG_LIST - list of string
 check_tag_is_valid(tag_name, tag_value) - checks whether tag_value is valid for tag_name
 """
 
-__author__ = ["fkiraly", "victordremov"]
+import inspect
+import sys
 
 import pandas as pd
 
+from sktime.base import BaseObject
+from sktime.registry._base_classes import BASE_CLASS_REGISTER
+
+
+class _BaseTag(BaseObject):
+    """Base class for all tags."""
+
+    _tags = {
+        "object_type": "tag",
+        "tag_name": "fill_this_in",  # name of the tag used in the _tags dictionary
+        "parent_type": "object",  # scitype of the parent object, str or list of str
+        "tag_type": "str",  # type of the tag value
+        "short_descr": "describe the tag here",  # short tag description, max 80 chars
+        "user_facing": True,  # whether the tag is user-facing
+    }
+
+
+# General tags, for all objects
+# -----------------------------
+
+
+class object_type(_BaseTag):
+    """Scientific type of the object.
+
+    Typing tag for all objects in ``sktime``.
+
+    - String name: ``"object_type"``
+    - Public metadata tag
+    - Values:  string or list of strings
+    - Example: ``"forecaster"``
+    - Example 2: ``["transformer", "clusterer"]`` (polymorphic object)
+    - Default: ``"object"``
+
+    In ``sktime``, every object has a scientific type (scitype),
+    determining the type of object and unified interface,
+    e.g., forecaster, time series classifier, time series regressor.
+
+    The ``object_type`` tag of an object is a string, or list of strings,
+    specifying the scitpye of the object.
+    For instance, a forecaster has scitype `"forecaster"`.
+
+    In case of a list, the object is polymorphic, and can assume (class),
+    or simultaneously satisfy different interfaces (object).
+
+    Valid scitypes are defined in ``sktime.registry.BASE_CLASS_SCITYPE_LIST``,
+    or ``sktime.registry.BASE_CLASS_REGISTER``.
+
+    The full list of scitypes in the current version is:
+    """
+
+    _tags = {
+        "tag_name": "object_type",
+        "parent_type": "object",
+        "tag_type": "str",
+        "short_descr": "type of object: estimator, transformer, regressor, etc",
+        "user_facing": True,
+    }
+
+
+# dynamically add a pretty printd list of scitypes to the docstring
+for name, _, desc in BASE_CLASS_REGISTER:
+    object_type.__doc__ += f'\n    - ``"{name}"``: {desc}'
+
+
+class maintainers(_BaseTag):
+    """Current maintainers of the object, GitHub IDs.
+
+    Part of packaging metadata for the object.
+
+    - String name: ``"maintainers"``
+    - Public metadata tag
+    - Values:  string or list of strings
+    - Example: ``["benheid", "fkiraly", "yarnabrina"]``
+    - Example 2: ``"yarnabrina"``
+    - Default: ``"sktime developers"``
+
+    The ``maintainers`` tag of an object is a string or list of strings,
+    each string being a GitHub handle of a maintainer of the object.
+
+    Maintenance extends to the specific class in ``sktime`` only,
+    and not interfaced packages or dependencies.
+
+    Maintainers should be tagged on issues and PRs related to the object,
+    and have rights and responsibilities in accordance with
+    the ``sktime`` governance model, see :ref:`algorithm-maintainers`.
+
+    To find an algorithm's maintainer, use ``get_tag("maintainers")`` on the object,
+    or use the search function in the
+    `estimator overview <https://www.sktime.net/en/stable/estimator_overview.html>`_.
+
+    In case of classes not owned by specific algorithm maintainers,
+    the tag defaults to the ``sktime`` core team.
+
+    Maintainers are given prominent visibility in the object's metadata,
+    and in automatically generated documentation.
+    """
+
+    _tags = {
+        "tag_name": "maintainers",
+        "parent_type": "object",
+        "tag_type": ("list", "str"),
+        "short_descr": "current maintainers of the object, each maintainer a GitHub handle",  # noqa: E501
+        "user_facing": True,
+    }
+
+
+class authors(_BaseTag):
+    """Authors of the object, GitHub IDs.
+
+    Part of packaging metadata for the object.
+
+    - String name: ``"authors"``
+    - Public metadata tag
+    - Values:  string or list of strings
+    - Example: ``["benheid", "fkiraly", "yarnabrina"]``
+    - Example 2: ``"fkiraly"``
+    - Default: ``"sktime developers"``
+
+    The ``authors`` tag of an object is a string or list of strings,
+    each string being a GitHub handle of an author of the object.
+
+    Authors are credited for the original implementation of the object,
+    and contributions to the object.
+
+    In case of light wrappers around third or second party packages,
+    author credits should include authors of the wrapped object.
+
+    Authors are not necessarily maintainers of the object,
+    and do not need to be tagged on issues and PRs related to the object.
+
+    Authors are given prominent visibility in the object's metadata,
+    and in automatically generated documentation.
+
+    To find an algorithm's authors, use ``get_tag("authors")`` on the object,
+    or use the search function in the
+    `estimator overview <https://www.sktime.net/en/stable/estimator_overview.html>`_.
+    """
+
+    _tags = {
+        "tag_name": "authors",
+        "parent_type": "object",
+        "tag_type": ("list", "str"),
+        "short_descr": "list of authors of the object, each author a GitHub handle",
+        "user_facing": True,
+    }
+
+
+class python_version(_BaseTag):
+    """Python version requirement specifier for the object (PEP 440).
+
+    Part of packaging metadata for the object.
+
+    - String name: ``"python_version"``
+    - Private tag, developer and framework facing
+    - Values: PEP 440 compliant version specifier
+    - Example: ``">=3.10"``
+    - Default: no restriction
+
+    ``sktime`` manages objects and estimators like mini-packages,
+    with their own dependencies and compatibility requirements.
+    Dependencies are specified in the tags:
+
+    - ``"python_version"``: Python version specifier (PEP 440) for the object,
+    - ``"python_dependencies"``: list of required Python packages (PEP 440)
+    - ``"python_dependencies_alias"``: alias for package names,
+      if different from import names
+    - ``"env_marker"``: environment marker for the object (PEP 508)
+    - ``"requires_cython"``: whether the object requires a C compiler present
+
+    The ``python_version`` tag of an object is a PEP 440 compliant version specifier
+    string, specifying python version compatibility of the object.
+
+    The tag is used in packaging metadata for the object,
+    and is used internally to check compatibility of the object with
+    the build environment, to raise informative error messages.
+
+    Developers can use ``_check_python_version`` from ``skbase.utils.dependencies``
+    to check compatibility of the python constraint of the object
+    with the current build environment, or
+    ``_check_estimator_deps`` to check compatibility of the object
+    (including further checks) with the current build environment.
+    """
+
+    _tags = {
+        "tag_name": "python_version",
+        "parent_type": "object",
+        "tag_type": "str",
+        "short_descr": "python version specifier (PEP 440) for estimator, or None = all versions ok",  # noqa: E501
+        "user_facing": False,
+    }
+
+
+class python_dependencies(_BaseTag):
+    """Python package dependency requirement specifiers for the object (PEP 440).
+
+    Part of packaging metadata for the object.
+
+    - String name: ``"python_dependencies"``
+    - Private tag, developer and framework facing
+    - Values: str or list of str, each str a PEP 440 compliant dependency specifier
+    - Example: ``"numpy>=1.20.0"``
+    - Example 2: ``["numpy>=1.20.0", "pandas>=1.3.0"]``
+    - Default: no requirements beyond ``sktime`` core dependencies (``None``)
+
+    ``sktime`` manages objects and estimators like mini-packages,
+    with their own dependencies and compatibility requirements.
+    Dependencies are specified in the tags:
+
+    - ``"python_version"``: Python version specifier (PEP 440) for the object,
+    - ``"python_dependencies"``: list of required Python packages (PEP 440)
+    - ``"python_dependencies_alias"``: alias for package names,
+      if different from import names
+    - ``"env_marker"``: environment marker for the object (PEP 508)
+    - ``"requires_cython"``: whether the object requires a C compiler present
+
+    The ``python_dependencies`` tag of an object is string or list of strings,
+    each string a PEP 440 compliant version specifier,
+    specifying python dependency requirements of the object.
+
+    The tag is used in packaging metadata for the object,
+    and is used internally to check compatibility of the object with
+    the build environment, to raise informative error messages.
+
+    Developers should note that package names in PEP 440 specifier strings
+    are identical with the package names used in ``pip install`` commands,
+    which in general is not the same as the import name of the package,
+    e.g., ``"scikit-learn"`` and not ``"sklearn"``.
+
+    Developers can use ``_check_soft_dependencies`` from ``skbase.utils.dependencies``
+    to check compatibility of the python constraint of the object
+    with the current build environment, or
+    ``_check_estimator_deps`` to check compatibility of the object
+    (including further checks) with the current build environment.
+    """
+
+    _tags = {
+        "tag_name": "python_dependencies",
+        "parent_type": "object",
+        "tag_type": ("list", "str"),
+        "short_descr": "python dependencies of estimator as str or list of str (PEP 440)",  # noqa: E501
+        "user_facing": False,
+    }
+
+
+class python_dependencies_alias(_BaseTag):
+    """Alias for Python package dependency names for the object.
+
+    Part of packaging metadata for the object.
+
+    - String name: ``"python_dependencies_alias"``
+    - Private tag, developer and framework facing
+    - Values: dict of str, key = PEP 440 package name, value = import name
+    - Example: ``{"scikit-learn": "sklearn"}``
+    - Example 2: ``{"dtw-python": "dtw", "scikit-learn": "sklearn"}``
+    - Default: no aliases (``None``)
+
+    ``sktime`` manages objects and estimators like mini-packages,
+    with their own dependencies and compatibility requirements.
+    Dependencies are specified in the tags:
+
+    - ``"python_version"``: Python version specifier (PEP 440) for the object,
+    - ``"python_dependencies"``: list of required Python packages (PEP 440)
+    - ``"python_dependencies_alias"``: alias for package names,
+      if different from import names
+    - ``"env_marker"``: environment marker for the object (PEP 508)
+    - ``"requires_cython"``: whether the object requires a C compiler present
+
+    The ``python_dependencies_alias`` tag of an object is dict,
+    providing import name aliases for package names in the ``python_dependencies`` tag,
+    if the package name differs from the import name.
+
+    The tag is used in packaging metadata for the object,
+    and is used internally to check compatibility of the object with
+    the build environment, to raise informative error messages.
+
+    This tag is required if the package name of a dependency is different
+    from the import name of the package, e.g., ``"dtw-python"`` and ``"dtw"``.
+    If not set, the package name is assumed to be identical with the import name.
+
+    Developers should note that elements of this ``dict`` are not passed on
+    via field inheritance, unlike the tags themselves.
+    Hence, if multiple aliases are required, they need to be set in the same tag.
+    """
+
+    _tags = {
+        "tag_name": "python_dependencies_alias",
+        "parent_type": "object",
+        "tag_type": "dict",
+        "short_descr": "alias for package names in python_dependencies, key-value pairs are package name, import name",  # noqa: E501
+        "user_facing": False,
+    }
+
+
+class requires_cython(_BaseTag):
+    """Whether the object requires a C compiler present, such as libomp, gcc.
+
+    Part of packaging metadata for the object.
+
+    - String name: ``"requires_cython"``
+    - Private tag, developer and framework facing
+    - Values: boolean, ``True`` / ``False``
+    - Example: ``True``
+    - Default: ``False``
+
+    ``sktime`` manages objects and estimators like mini-packages,
+    with their own dependencies and compatibility requirements.
+    Dependencies are specified in the tags:
+
+    - ``"python_version"``: Python version specifier (PEP 440) for the object,
+    - ``"python_dependencies"``: list of required Python packages (PEP 440)
+    - ``"python_dependencies_alias"``: alias for package names,
+      if different from import names
+    - ``"env_marker"``: environment marker for the object (PEP 508)
+    - ``"requires_cython"``: whether the object requires a C compiler present
+
+    The ``requires_cython`` tag of an object is a boolean,
+    specifying whether the object requires a C compiler present.
+    True means that a C compiler is required, False means it is not required.
+
+    The tag is used in packaging metadata for the object,
+    and primarily in the continuous integration and testing setup of the ``sktime``
+    package, which ensures that objects with this tag are
+    tested in specific environments with a C compiler present.
+
+    It is not used in user facing checks, error messages,
+    or recommended build processes otherwise.
+    """
+
+    _tags = {
+        "tag_name": "requires_cython",
+        "parent_type": "object",
+        "tag_type": "bool",
+        "short_descr": "whether the object requires a C compiler present such as libomp, gcc",  # noqa: E501
+        "user_facing": False,
+    }
+
+
+# Estimator tags
+# --------------
+
+# These tags are applicable to a wide range of objects,
+# most tags in this group apply to estimators
+
+# "capability:missing_values" is same as "handles-missing-data" tag.
+# They are kept distinct intentionally for easier TSC refactoring.
+# Will be merged after refactor completion.
+
+
+class capability__missing_values(_BaseTag):
+    """Capability: the estimator can handle missing data, e.g,, NaNs.
+
+    - String name: ``"capability:missing_values"``
+    - Public capability tag
+    - Values: boolean, ``True`` / ``False``
+    - Example: ``True``
+    - Default: ``False``
+    - Alias: ``handles-missing-data``  (forecasters, transformations)
+
+    If the tag is ``True``, the estimator can handle missing data,
+    e.g., NaNs in input data.
+
+    This applies to main and secondary input data where applicable,
+    e.g., ``X`` in ``fit`` of transformations and classifieres, or ``y`` in forecasters,
+    but not to target labels
+    in the case of labelling of entire time series, such as in
+    classification or regression.
+
+    If the tag is ``False``, the estimator cannot handle missing data,
+    and will raise an error if missing data is encountered.
+    """
+
+    _tags = {
+        "tag_name": "capability:missing_values",
+        "parent_type": "object",
+        "tag_type": "bool",
+        "short_descr": "can the estimator handle missing data (NA, np.nan) in inputs?",  # noqa: E501
+        "user_facing": True,
+    }
+
+
+class capability__feature_importance(_BaseTag):
+    """Capability: the estimator can provide feature importance.
+
+    - String name: ``"capability:feature_importance"``
+    - Public capability tag
+    - Values: boolean, ``True`` / ``False``
+    - Example: ``True``
+    - Default: ``False``
+
+    If the tag is ``True``, the estimator can produce feature importances.
+
+    Feature importances are queriable by the fitted parameter interface
+    via ``get_fitted_params``, after calling ``fit`` of the respective estimator.
+
+    If the tag is ``False``, the estimator does not produce feature importances.
+    The method ``get_fitted_params`` can be called,
+    but the list of fitted parameters will not contain feature importances.
+    """
+
+    _tags = {
+        "tag_name": "capability:feature_importance",
+        "parent_type": "estimator",
+        "tag_type": "bool",
+        "short_descr": "Can the estimator provide feature importance?",
+        "user_facing": True,
+    }
+
+
+class capability__contractable(_BaseTag):
+    """Capability: the estimator can be asked to satisfy a maximum time contract.
+
+    To avoid confusion, users should note that the literature term "contractable"
+    unusually derives its meaning from "contract", i.e., a time contract,
+    and not from "contraction", i.e., reducing the size of something.
+
+    - String name: ``"capability:contractable"``
+    - Public capability tag
+    - Values: boolean, ``True`` / ``False``
+    - Example: ``True``
+    - Default: ``False``
+
+    If the tag is ``True``, the estimator can be contracted in time,
+    by using some of its parameters, to limit the maximum time spent in fitting.
+
+    Currently, there is no unified naming or selection of parameters controlling
+    the contract time setting, as this can apply to different parts of the algorithm.
+    Users should consult the documentation of the specific estimator for details.
+
+    If the tag is ``False``, the estimator does not support a contract time setting.
+    """
+
+    _tags = {
+        "tag_name": "capability:contractable",
+        "parent_type": "estimator",
+        "tag_type": "bool",
+        "short_descr": "contract time setting, does the estimator support limiting max fit time?",  # noqa: E501
+        "user_facing": True,
+    }
+
+
+class capability__train_estimate(_BaseTag):
+    """Capability: the algorithm can estimate its performance on the training set.
+
+    - String name: ``"capability:train_estimate"``
+    - Public capability tag
+    - Values: boolean, ``True`` / ``False``
+    - Example: ``True``
+    - Default: ``False``
+
+    If the tag is ``True``, the estimator can estimate its performance on
+    the training set.
+
+    More precisely, this tag describes algorithms that, when calling ``fit``,
+    produce and store an estimate of their own statistical performance,
+    e.g., via out-of-bag estimates, or cross-validation.
+
+    Training performance estimates are queriable by the fitted parameter interface
+    via ``get_fitted_params``, after calling ``fit`` of the respective estimator.
+
+    If the tag is ``False``, the estimator does not produce
+    training performance estimates.
+    The method ``get_fitted_params`` can be called,
+    but the list of fitted parameters will not contain training performance estimates.
+    """
+
+    _tags = {
+        "tag_name": "capability:train_estimate",
+        "parent_type": "estimator",
+        "tag_type": "bool",
+        "short_descr": "can the estimator estimate its performance on the training set?",  # noqa: E501
+        "user_facing": True,
+    }
+
+
+# Forecasters
+# -----------
+
+
+class capability__exogeneous(_BaseTag):
+    """Capability: the forecaster can use exogeneous data.
+
+    The tag is currently named ``ignores-exogeneous-X``, and will be renamed.
+
+    ``False`` = does use exogeneous data, ``True`` = does not use exogeneous data.
+
+    - String name: ``"ignores-exogeneous-X"``
+    - Public capability tag
+    - Values: boolean, ``True`` / ``False``
+    - Example: ``True``
+    - Default: ``False``
+    - Alias: ``capability:exogeneous`` (currently not used)
+
+    Exogeneous data are additional time series,
+    that can be used to improve forecasting accuracy.
+
+    If the forecaster uses exogeneous data (``ignore-exogeneous-X=False``),
+    the ``X`` parmameter in ``fit``, ``predict``, and other methods
+    can be used to pass exogeneous data to the forecaster.
+
+    If the ``X-y-must-have-same-index`` tag is ``True``,
+    then such data must always have an index that contains that of the target series,
+    i.e., ``y`` in ``fit``, or the indices specified by ``fh`` in ``predict``.
+
+    If the tag is ``False``, the forecaster does not make use of exogeneous data.
+    ``X`` parameters can still be passed to methods, to ensure a uniform interface,
+    but the data will be ignored,
+    i.e., not used in the internal logic of the forecaster.
+
+    """
+
+    _tags = {
+        "tag_name": "ignores-exogeneous-X",
+        "parent_type": "forecaster",
+        "tag_type": "bool",
+        "short_descr": "does forecaster make use of exogeneous data?",
+        "user_facing": True,
+    }
+
+
+class capability__insample(_BaseTag):
+    """Capability: the forecaster can make in-sample predictions.
+
+    - String name: ``"capability:insample"``
+    - Public capability tag
+    - Values: boolean, ``True`` / ``False``
+    - Example: ``True``
+    - Default: ``False``
+
+    If the tag is ``True``, the forecaster can make in-sample predictions,
+    i.e., predict the target series for time points that are part of the training set.
+
+    In-sample predictions are useful for model evaluation,
+    and for making predictions for the training set itself.
+
+    Mechanically, in-sample predictions are made by calling the ``predict`` method
+    and specifying a forecasting horizon ``fh`` such that at least one index
+    is queried that is equal or earlier to the latest index in the training set,
+    i.e., any data previously passed in ``fit`` or ``update``.
+
+    If the tag is ``False``, the forecaster cannot make in-sample predictions,
+    and will raise an error if an in-sample prediction is attempted.
+    """
+
+    _tags = {
+        "tag_name": "capability:insample",
+        "parent_type": "forecaster",
+        "tag_type": "bool",
+        "short_descr": "can the forecaster make in-sample predictions?",
+        "user_facing": True,
+    }
+
+
+class capability__pred_int(_BaseTag):
+    """Capability: the forecaster can make probabilistic or interval forecasts.
+
+    - String name: ``"capability:pred_int"``
+    - Public capability tag
+    - Values: boolean, ``True`` / ``False``
+    - Example: ``True``
+    - Default: ``False``
+
+    ``sktime`` supports a range of ways to make probabilistic type forecasts,
+    via the following methods of any forecaster:
+
+    * ``predict_interval``: prediction intervals
+    * ``predict_quantiles``: quantile forecasts
+    * ``predict_var``: variance forecasts
+    * ``predict_proba``: distribution forecasts
+
+    If the ``capability:pred_int`` tag is ``True``, the forecaster can make
+    probabilistic type forecasts using all of the above methods.
+
+    Even if the forecaster natively implements only one of the above methods,
+    all are available to the user:
+
+    * interval and quantile forecasts are of equivalent information,
+      with intervals of a coverage assumed symmetric
+    * a forecaster with available distribution forecasts obtains
+      prediction intervals and quantile forecasts from the distribution
+    * a forecaster with available variance forecasts assumes a normal distribution
+      around the ``predict`` output as mean,
+      and derives prediction intervals and quantiles
+      from that normal distribution
+    * a forecaster with available interval or quantile uses the IQR to
+      to derive variance forecasts under normality assumptions.
+      Users should note that this may lead to a distribution forecast which
+      is not consistent with interval or quantile forecasts.
+
+    If the tag is ``False``, the forecaster cannot make probabilistic forecasts,
+    and will raise an error if a probabilistic forecast is attempted.
+    """
+
+    _tags = {
+        "tag_name": "capability:pred_int",
+        "parent_type": "forecaster",
+        "tag_type": "bool",
+        "short_descr": "does the forecaster implement predict_interval or predict_quantiles?",  # noqa: E501
+        "user_facing": True,
+    }
+
+
+class capability__pred_int__insample(_BaseTag):
+    """Capability: the forecaster can make in-sample probabilistic forecasts.
+
+    Only relevant if the ``capability:pred_int`` tag is ``True``,
+    i.e., the forecaster can make probabilistic forecasts.
+
+    - String name: ``"capability:pred_int:insample"``
+    - Public capability tag
+    - Values: boolean, ``True`` / ``False``
+    - Example: ``False``
+    - Default: ``True``
+
+    If the tag is ``True``, the forecaster can make
+    its probabilistic forecasts in-sample, i.e.,
+    ``predict_interval``, ``predict_quantiles``, ``predict_var``,
+    or ``predict_proba`` can be called with a forecasting horizon ``fh``
+    that includes in-sample indices, i.e., indices that are not later than
+    the latest index in the training set.
+
+    If the tag ``capability:pred_int`` is ``False``,
+    then the tag ``capability:pred_int:insample`` is irrelevant,
+    as the forecaster cannot make probabilistic forecasts at all.
+    In such a case, the tag ``capability:pred_int:insample`` should be ignored.
+
+    If the tag ``capability:pred_int`` is ``True``,
+    and is the tag ``capability:pred_int:insample`` is ``False``,
+    the forecaster can make probabilistic forecasts that are out-of-sample,
+    but cannot make in-sample probabilistic forecasts,
+    and will raise an error if an in-sample probabilistic forecast is attempted.
+    """
+
+    _tags = {
+        "tag_name": "capability:pred_int:insample",
+        "parent_type": "forecaster",
+        "tag_type": "bool",
+        "short_descr": "can the forecaster make in-sample predictions in predict_interval/quantiles?",  # noqa: E501
+        "user_facing": True,
+    }
+
+
+# Panel data related tags
+# -----------------------
+
+# tags related to panel data, typically:
+# classification, regression, clustering, and transformations
+
+
+class capability__multivariate(_BaseTag):
+    """Capability: the estimator can handle multivariate time series.
+
+    - String name: ``"capability:multivariate"``
+    - Public capability tag
+    - Values: boolean, ``True`` / ``False``
+    - Example: ``True``
+    - Default: ``False``
+
+    If the tag is ``True``, the estimator can handle multivariate time series,
+    for its main input data, i.e., the ``X`` parameter in ``fit`` of classifiers,
+    regressors, clusterers, ordinary transformers, and pairwise transformers.
+
+    If the tag is ``False``, the estimator can only handle univariate time series,
+    and will broadcast to variables (ordinary transformers), or raise an error (others).
+
+    This condition is specific to the main input data representation,
+    target data (e.g., classifier or transformation ``y``) are not considered.
+
+    The condition is also specific to the data type used, in terms of how
+    being "multivariate" is represented.
+    For instance, a ``pandas`` based time series specification is considered
+    multivariate if it has more than one column.
+    """
+
+    _tags = {
+        "tag_name": "capability:multivariate",
+        "parent_type": [
+            "classifier",
+            "clusterer",
+            "early_classifier",
+            "param_est",
+            "regressor",
+            "transformer-pairwise",
+            "transformer-pairwise-panel",
+        ],
+        "tag_type": "bool",
+        "short_descr": "can the estimator be applied to time series with 2 or more variables?",  # noqa: E501
+        "user_facing": True,
+    }
+
+
+class capability__unequal_length(_BaseTag):
+    """Capability: the estimator can handle unequal length time series.
+
+    - String name: ``"capability:unequal_length"``
+    - Public capability tag
+    - Values: boolean, ``True`` / ``False``
+    - Example: ``True``
+    - Default: ``False``
+
+    Tag applicable to estimators which can accept panel data,
+    i.e., collections of time series.
+
+    If the tag is ``True``, the estimator can handle panels of time series
+    with unequal index set, including panels of time series with
+    unequal length.
+
+    It should be noted that the capability implied by the tag is
+    strictly more general than the capability implied by the name of the tag,
+    as panels of time series of equal length can have unequal index sets.
+
+    If the tag is ``False``, the estimator requires all time series in the panel
+    to have equal length and index set, and will otherwise raise an error.
+    """
+
+    _tags = {
+        "tag_name": "capability:unequal_length",
+        "parent_type": [
+            "classifier",
+            "clusterer",
+            "early_classifier",
+            "regressor",
+            "transformer",
+            "transformer-pairwise-panel",
+        ],
+        "tag_type": "bool",
+        "short_descr": "can the estimator handle unequal length time series?",
+        "user_facing": True,
+    }
+
+
 ESTIMATOR_TAG_REGISTER = [
-    (
-        "object_type",
-        "object",
-        "str",
-        "type of object: estimator, transformer, regressor, etc",
-    ),
-    (
-        "ignores-exogeneous-X",
-        "forecaster",
-        "bool",
-        "does forecaster ignore exogeneous data (X)?",
-    ),
     (
         "univariate-only",
         "transformer",
@@ -221,30 +940,12 @@ ESTIMATOR_TAG_REGISTER = [
         "bool",
         "whether inverse_transform is expected to be an exact inverse to transform",
     ),
-    (
-        "capability:pred_int",
-        "forecaster",
-        "bool",
-        "does the forecaster implement predict_interval or predict_quantiles?",
-    ),
     (
         "capability:pred_var",
         "forecaster",
         "bool",
         "does the forecaster implement predict_variance?",
     ),
-    (
-        "capability:insample",
-        "forecaster",
-        "bool",
-        "can the forecaster make in-sample predictions?",
-    ),
-    (
-        "capability:pred_int:insample",
-        "forecaster",
-        "bool",
-        "can the forecaster make in-sample predictions in predict_interval/quantiles?",
-    ),
     (
         "capability:predict_proba",
         "classifier",
@@ -252,53 +953,6 @@ ESTIMATOR_TAG_REGISTER = [
         "does the classifier implement a non-default predict_proba, "
         "i.e., not just 0/1 probabilities obtained from predict?",
     ),
-    (
-        "capability:feature_importance",
-        "classifier",
-        "bool",
-        "Can the estimator provide feature importance?",
-    ),
-    (
-        "capability:multivariate",
-        [
-            "classifier",
-            "early_classifier",
-            "param_est",
-            "regressor",
-            "transformer-pairwise",
-            "transformer-pairwise-panel",
-        ],
-        "bool",
-        "can the classifier classify time series with 2 or more variables?",
-    ),
-    (
-        "capability:unequal_length",
-        [
-            "classifier",
-            "early_classifier",
-            "regressor",
-            "transformer",
-            "transformer-pairwise-panel",
-        ],
-        "bool",
-        "can the estimator handle unequal length time series?",
-    ),
-    # "capability:missing_values" is same as "handles-missing-data" tag.
-    # They are kept distinct intentionally for easier TSC refactoring.
-    # Will be merged after refactor completion.
-    (
-        "capability:missing_values",
-        [
-            "classifier",
-            "early_classifier",
-            "param_est",
-            "regressor",
-            "transformer-pairwise",
-            "transformer-pairwise-panel",
-        ],
-        "bool",
-        "can the classifier handle missing data (NA, np.nan) in inputs?",
-    ),
     (
         "capability:unequal_length:removes",
         "transformer",
@@ -311,18 +965,6 @@ ESTIMATOR_TAG_REGISTER = [
         "bool",
         "is the transformer result guaranteed to have no missing values?",
     ),
-    (
-        "capability:train_estimate",
-        "classifier",
-        "bool",
-        "can the classifier estimate its performance on the training set?",
-    ),
-    (
-        "capability:contractable",
-        "classifier",
-        "bool",
-        "contract time setting, does the estimator support limiting max fit time?",
-    ),
     (
         "capability:multithreading",
         ["classifier", "early_classifier"],
@@ -407,31 +1049,6 @@ ESTIMATOR_TAG_REGISTER = [
         "bool",
         "whether inner _evaluate can deal with multilevel (Panel/Hierarchical)",
     ),
-    (
-        "python_version",
-        "estimator",
-        "str",
-        "python version specifier (PEP 440) for estimator, or None = all versions ok",
-    ),
-    (
-        "python_dependencies",
-        "estimator",
-        ("list", "str"),
-        "python dependencies of estimator as str or list of str",
-    ),
-    (
-        "python_dependencies_alias",
-        "estimator",
-        "dict",
-        "should be provided if import name differs from package name, \
-        key-value pairs are package name, import name",
-    ),
-    (
-        "requires_cython",
-        "estimator",
-        "bool",
-        "whether the estimator requires a C compiler present such as libomp, gcc",
-    ),
     (
         "remember_data",
         ["forecaster", "transformer"],
@@ -522,20 +1139,24 @@ ESTIMATOR_TAG_REGISTER = [
         "bool",
         "can the estimator handle multioutput data?",
     ),
-    (
-        "maintainers",
-        "object",
-        ("list", "str"),
-        "list of current maintainers of the object, each maintainer a GitHub handle",
-    ),
-    (
-        "authors",
-        "object",
-        ("list", "str"),
-        "list of authors of the object, each author a GitHub handle",
-    ),
 ]
 
+# construct the tag register from all classes in this module
+tag_clses = inspect.getmembers(sys.modules[__name__], inspect.isclass)
+for _, cl in tag_clses:
+    # skip the base class
+    if cl.__name__ == "_BaseTag" or not issubclass(cl, _BaseTag):
+        continue
+
+    cl_tags = cl.get_class_tags()
+
+    tag_name = cl_tags["tag_name"]
+    parent_type = cl_tags["parent_type"]
+    tag_type = cl_tags["tag_type"]
+    short_descr = cl_tags["short_descr"]
+
+    ESTIMATOR_TAG_REGISTER.append((tag_name, parent_type, tag_type, short_descr))
+
 ESTIMATOR_TAG_TABLE = pd.DataFrame(ESTIMATOR_TAG_REGISTER)
 ESTIMATOR_TAG_LIST = ESTIMATOR_TAG_TABLE[0].tolist()
 
diff --git a/sktime/utils/seasonality.py b/sktime/utils/seasonality.py
index e91856798..8e5ce1769 100644
--- a/sktime/utils/seasonality.py
+++ b/sktime/utils/seasonality.py
@@ -111,12 +111,8 @@ def _pivot_sp(df, sp, anchor=None, freq=None, anchor_side="start"):
     if freq is None and hasattr(anchor.index, "freq"):
         freq = anchor.index.freq
 
-    if isinstance(df.index, pd.DatetimeIndex):
-        df = df.copy()
-        df.index = df.index.to_period(freq=freq)
-        was_datetime = True
-    else:
-        was_datetime = False
+    # Make df to period index if it datetime index
+    df, was_datetime = _make_period_index_df(df, freq)
 
     if isinstance(anchor.index, pd.DatetimeIndex):
         aix = anchor.index.to_period(freq=freq)
@@ -124,11 +120,14 @@ def _pivot_sp(df, sp, anchor=None, freq=None, anchor_side="start"):
         aix = anchor.index
     aix_int = aix.astype("int64")
 
+    if pd.api.types.is_integer_dtype(anchor.index) or len(anchor) <= 1:
+        period_len = 1
+        period_len_int = 1
+    else:
+        period_len = anchor.index[1] - anchor.index[0]
+        period_len_int = aix_int[1] - aix_int[0]
+
     if not isinstance(df.index, pd.PeriodIndex):
-        if pd.api.types.is_integer_dtype(anchor.index) and len(anchor) <= 1:
-            period_len = 1
-        else:
-            period_len = anchor.index[1] - anchor.index[0]
         if anchor_side == "start":
             ix = (df.index - anchor.index[0]) / period_len
         else:
@@ -145,8 +144,8 @@ def _pivot_sp(df, sp, anchor=None, freq=None, anchor_side="start"):
     df = pd.DataFrame(df)
     df_pivot = pd.pivot_table(
         data=df,
-        index=ix // sp,  # Upper level
-        columns=ix % sp,  # Lower level
+        index=ix // (sp * period_len_int),  # Upper level
+        columns=ix % (sp * period_len_int),  # Lower level
         dropna=False,
     )
 
@@ -159,13 +158,25 @@ def _pivot_sp(df, sp, anchor=None, freq=None, anchor_side="start"):
     df_pivot.index = pivot_ix
 
     if was_datetime:
-        df_pivot.index = df_pivot.index.to_timestamp()
+        df_pivot.index = df_pivot.index.to_timestamp(
+            freq=anchor.index.freq
+        ).tz_localize(anchor.index.tz)
 
     df_pivot.columns = df_pivot.columns.droplevel(0)
 
     return df_pivot
 
 
+def _make_period_index_df(df, freq):
+    if isinstance(df.index, pd.DatetimeIndex):
+        df = df.copy()
+        df.index = df.index.to_period(freq=freq)
+        was_datetime = True
+    else:
+        was_datetime = False
+    return df, was_datetime
+
+
 def _unpivot_sp(df, template=None):
     """Unpivot DataFrame with multivariate-by-seasonal-offset, invert _pivot_sp.
 
@@ -193,24 +204,36 @@ def _unpivot_sp(df, template=None):
             freq = template.freq
         else:
             freq = template
-
+    else:
+        freq = None
+    if isinstance(df.index, pd.DatetimeIndex):
+        tz = df.index.tz
+    else:
+        tz = None
+    df_copied, _ = _make_period_index_df(template, freq)
+    ix = df_copied.index.astype("int64")
     df_melt = df.melt(ignore_index=False)
 
+    if len(ix) <= 1:
+        period_len_int = 1
+    else:
+        period_len_int = ix[1] - ix[0]
+
     offset = df_melt[df_melt.columns[0]]
     if isinstance(df_melt.index, pd.DatetimeIndex):
         a = df_melt.index.to_period(freq=freq)
-        res = a + offset
+        res = a + offset // period_len_int
         df_melt.index = res
         was_datetime = True
     else:
-        df_melt.index = df_melt.index + offset
+        df_melt.index = df_melt.index + offset // period_len_int
         was_datetime = False
     df_melt = df_melt.drop(columns=df_melt.columns[0])
     df_melt = df_melt.sort_index()
     df_melt = df_melt.dropna()
 
     if was_datetime:
-        df_melt.index = df_melt.index.to_timestamp()
+        df_melt.index = df_melt.index.to_timestamp(template.index.freq).tz_localize(tz)
 
     if template is not None:
         if hasattr(template, "columns"):


Test patch:
diff --git a/sktime/forecasting/tests/test_naive.py b/sktime/forecasting/tests/test_naive.py
index 8b39121d4..96df16d64 100644
--- a/sktime/forecasting/tests/test_naive.py
+++ b/sktime/forecasting/tests/test_naive.py
@@ -403,6 +403,22 @@ def test_naive_predict_interval_against_R_naive(strategy, sp, lower, upper):
     pd.testing.assert_frame_equal(y_pred_ints, expected)
 
 
+@pytest.mark.parametrize("freq", ["2D", "W", "W-TUE", "M"])
+def test_naive_sp_greater_1_not_nan(freq):
+    sample_dates = pd.date_range(start="2001-01-01", periods=30, freq=freq)
+    sample_values = np.random.default_rng(seed=0).random(size=len(sample_dates))
+
+    sample_dataset = pd.Series(sample_values, index=sample_dates)
+
+    model = NaiveForecaster(sp=2)
+    model.fit(sample_dataset)
+
+    predictions: pd.Series = model.predict(fh=[1, 2, 3])
+    null_predictions_count = predictions.isna().sum()
+
+    assert null_predictions_count == 0
+
+
 def test_insample_with_numpy_input():
     """Test insample prediction with numpy input."""
     y = np.random.random(1000)
diff --git a/sktime/utils/_testing/scenarios_clustering.py b/sktime/utils/_testing/scenarios_clustering.py
index 42921d3a7..c687432a8 100644
--- a/sktime/utils/_testing/scenarios_clustering.py
+++ b/sktime/utils/_testing/scenarios_clustering.py
@@ -7,7 +7,11 @@ __author__ = ["fkiraly"]
 
 __all__ = ["scenarios_clustering"]
 
+from inspect import isclass
+
 from sktime.base import BaseObject
+from sktime.registry import scitype
+from sktime.utils._testing.hierarchical import _make_hierarchical
 from sktime.utils._testing.panel import _make_panel_X, make_clustering_problem
 from sktime.utils._testing.scenarios import TestScenario
 
@@ -43,11 +47,52 @@ class ClustererTestScenario(TestScenario, BaseObject):
 
         return super().get_args(key=key, obj=obj, deepcopy_args=deepcopy_args)
 
+    def is_applicable(self, obj):
+        """Check whether scenario is applicable to obj.
+
+        Parameters
+        ----------
+        obj : class or object to check against scenario
+
+        Returns
+        -------
+        applicable: bool
+            True if self is applicable to obj, False if not
+        """
+
+        def get_tag(obj, tag_name):
+            if isclass(obj):
+                return obj.get_class_tag(tag_name)
+            else:
+                return obj.get_tag(tag_name)
+
+        # applicable only if obj inherits from BaseClassifier, BaseEarlyClassifier or
+        #   BaseRegressor. currently we test both classifiers and regressors using these
+        #   scenarios
+        if scitype(obj) != "clusterer":
+            return False
+
+        # if X is multivariate, applicable only if can handle multivariate
+        is_multivariate = not self.get_tag("X_univariate")
+        if is_multivariate and not get_tag(obj, "capability:multivariate"):
+            return False
+
+        # if X is unequal length, applicable only if can handle unequal length
+        is_unequal_length = self.get_tag("X_unequal_length")
+        if is_unequal_length and not get_tag(obj, "capability:unequal_length"):
+            return False
+
+        return True
+
 
 class ClustererFitPredict(ClustererTestScenario):
     """Fit/predict with panel Xmake_clustering_problem."""
 
-    _tags = {"X_univariate": True, "is_enabled": True}
+    _tags = {
+        "X_univariate": True,
+        "X_unequal_length": False,
+        "is_enabled": True,
+    }
 
     @property
     def args(self):
@@ -59,6 +104,39 @@ class ClustererFitPredict(ClustererTestScenario):
     default_method_sequence = ["fit", "predict"]
 
 
+class ClustererFitPredictUnequalLength(ClustererTestScenario):
+    """Fit/predict with univariate panel X, unequal length series."""
+
+    _tags = {
+        "X_univariate": True,
+        "X_unequal_length": True,
+        "is_enabled": True,
+    }
+
+    @property
+    def args(self):
+        X_unequal_length = _make_hierarchical(
+            hierarchy_levels=(10,),
+            min_timepoints=10,
+            max_timepoints=15,
+            random_state=RAND_SEED,
+        )
+        X_unequal_length_test = _make_hierarchical(
+            hierarchy_levels=(5,),
+            min_timepoints=10,
+            max_timepoints=15,
+            random_state=RAND_SEED,
+        )
+        return {
+            "fit": {"X": X_unequal_length},
+            "predict": {"X": X_unequal_length_test},
+        }
+
+    default_method_sequence = ["fit", "predict", "predict_proba", "decision_function"]
+    default_arg_sequence = ["fit", "predict", "predict", "predict"]
+
+
 scenarios_clustering = [
     ClustererFitPredict,
+    ClustererFitPredictUnequalLength,
 ]