# Copyright (c) 2020, Anthony Nouy, Erwan Grelier
# This file is part of tensap (tensor approximation package).

# tensap is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# tensap is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.

# You should have received a copy of the GNU Lesser General Public License
# along with tensap.  If not, see <https://www.gnu.org/licenses/>.

"""
Module tensor_learning.

"""

from abc import abstractmethod
from copy import deepcopy
import pprint
import numpy as np
import tensap


class TensorLearning(tensap.Learning):
    """
    Class TensorLearning.

    See also tensap.Learning.

    Attributes
    ----------
    order : int
        Order of the tensor.
    bases : list or numpy.ndarray
        The functional bases used for by the approximation.
    bases_eval : list or numpy.ndarray
        The evaluations of the bases on the training data.
    bases_eval_test : list or numpy.ndarray
        The evaluations of the bases on the test data.
    algorithm : str
        The choice of algorithm.
    initialization_type : str
        The type of initialization.
    initial_guess : tensap.Tensor
        An initial guess for the solver.
    tree_adaptation : bool
        Boolean indicating if tree adaptation is to be performed.
    tree_adaptation_options : dict
        Options for the tree adaptation.
    rank_adaptation : bool
        Boolean indicating if rank adaptation is to be performed.
    rank_adaptation_options : dict
        Options for the rank adaptation.
    tolerance : dict
        Tolerances for the solver.
    linear_model_learning_parameters : dict
        Parameters for the linear solvers.
    alternating_minimization_parameters : dict
        Parameters for the alternating minimization.
    bases_adaptation_path : list or numpy.ndarray
        Paths for each basis for the basis adaptation.
    store_iterates : bool
        Boolean indicating if the iterates are to be stored.
    rank : int or list or numpy.ndarray
        The rank of the expected approximation.
    output_dimension : int
        The dimension of the outputs of the function to be approximated.
    _number_of_parameters : int
        The number of parameters of the approximation.
    _exploration_strategy : numpy.ndarray
        The exploration strategy.
    _warnings : dict
        Dictionnary containing the state of some warnings to be displayed.

    """

    def __init__(self, loss):
        """
        Constructor for the class TensorLearning.

        See also tensap.Learning.

        Parameters
        ----------
        loss : tensap.LossFunction
            The loss function associated with the solver.

        Returns
        -------
        None.

        """
        super().__init__(loss)
        self.linear_model_learning = tensap.Learning.linear_model(loss)
        self.order = None
        self.bases = None
        self.bases_eval = None
        self.bases_eval_test = None
        self.algorithm = "standard"
        self.initialization_type = None
        self.initial_guess = None
        self.tree_adaptation = False
        self.tree_adaptation_options = {
            "tolerance": None,
            "max_iterations": 100,
            "force_rank_adaptation": True,
        }
        self.rank_adaptation = False
        self.rank_adaptation_options = {
            "max_iterations": 10,
            "early_stopping": False,
            "early_stopping_factor": 10,
        }
        self.tolerance = {"on_error": 1e-6, "on_stagnation": 1e-6}
        self.linear_model_learning_parameters = {"identical_for_all_parameters": True}
        self.alternating_minimization_parameters = {
            "display": False,
            "max_iterations": 30,
            "stagnation": 1e-6,
            "random": False,
        }
        self.bases_adaptation_path = None
        self.store_iterates = True
        self.rank = 1
        self.output_dimension = 1

        self.model_selection = False
        # Possible choices: 'test_error', 'cv_error'
        self.model_selection_options["type"] = "test_error"

        self._number_of_parameters = None
        self._exploration_strategy = None
        self._warnings = {
            "orthonormality_warning_display": True,
            "empty_bases_warning_display": True,
        }

    def __repr__(self):
        return pprint.pformat(self.__dict__, indent=4, width=1)

    def solve(self, *args):
        """
        Solver for the learning problem with tensor formats.

        Parameters
        ----------
        *args : misc
            Additional arguments, if needed.

        Raises
        ------
        NotImplementedError
            If the required solver is not implemented.
        ValueError
            If the required data is not provided.

        Returns
        -------
        f : tensap.FunctionalTensor
            The learned approximation.
        output : dict
            The outputs of the solver.

        """
        # If possible, deduce from training_data the output dimension
        if (
            isinstance(self.training_data, list)
            and len(self.training_data) == 2
            and np.ndim(self.training_data[1]) == 2
        ):
            self.output_dimension = self.training_data[1].shape[1]
        if self.output_dimension > 1 and (
            not isinstance(self, tensap.TreeBasedTensorLearning)
            or not isinstance(
                self.loss_function,
                (tensap.SquareLossFunction, tensap.CustomLossFunction),
            )
        ):
            raise NotImplementedError(
                "Solver not implemented for vector-valued functions "
                + "approximation, use TreeBasedTensorLearning with a "
                + "SquareLossFunction or a CustomLossFunction instead."
            )

        if (
            self.output_dimension > 1
            and "type" in self.rank_adaptation_options
            and self.rank_adaptation_options["type"] == "inner"
        ):
            print(
                "Inner rank adaptation not implemented for "
                + "output_dimension greater than 1, disabling it."
            )
            del self.rank_adaptation_options["type"]

        if self._warnings["orthonormality_warning_display"] and (
            self.bases is None
            or (
                self.bases is not None
                and not np.all([x.is_orthonormal for x in self.bases.bases])
            )
        ):
            self._warnings["orthonormality_warning_display"] = False
            print(
                "The implemented learning algorithms are designed "
                + "for orthonormal bases. These algorithms work with "
                + "non-orthonormal bases, but without some guarantees "
                + "on their results."
            )

        # If no bases are provided, warn that the returned functions are
        # evaluated on the training data
        if self.bases is None and self._warnings["empty_bases_warning_display"]:
            self._warnings["empty_bases_warning_display"] = False
            print(
                "The returned functions are evaluated on the training "
                + "data. To evaluate them at other points, assign to the "
                + "FunctionalTensor a nonempty field bases and set the "
                + "attribute evaluatedBases to False."
            )

        # If the test error cannot be computed, it is disabled
        if (
            self.test_error
            and not isinstance(self.bases, tensap.FunctionalBases)
            and self.bases_eval_test is None
        ):
            print("The test error cannot be computed.")
            self.test_error = False

        # Assert if basis adaptation can be performed
        if self.bases_adaptation_path is None and not hasattr(
            self.bases, "adaptation_path"
        ):
            if isinstance(self.linear_model_learning, (list, np.ndarray)) and np.any(
                [x.basis_adaptation for x in self.linear_model_learning]
            ):
                print("Cannot perform basis adaptation, disabling it.")
                for linear_solver in self.linear_model_learning:
                    setattr(linear_solver, "basis_adaptation", False)

            elif (
                not isinstance(self.linear_model_learning, (list, np.ndarray))
                and self.linear_model_learning.basis_adaptation
            ):
                print("Cannot perform basis adaptation, disabling it.")
                self.linear_model_learning.basis_adaptation = False

        # Bases evaluation
        if hasattr(self.bases, "eval"):
            if self.training_data is not None and self.bases_eval is None:
                if (
                    isinstance(self.training_data, list)
                    and self.training_data[0] is not None
                ):
                    self.bases_eval = self.bases.eval(self.training_data[0])
                elif (
                    not isinstance(self.training_data, list)
                    and self.training_data is not None
                ):
                    self.bases_eval = self.bases.eval(self.training_data)
                else:
                    raise ValueError("Must provide input training data.")

            if (
                self.test_error
                and self.test_data is not None
                and self.bases_eval_test is None
            ):
                if isinstance(self.test_data, list) and self.test_data[0] is not None:
                    self.bases_eval_test = self.bases.eval(self.test_data[0])
                elif (
                    not isinstance(self.test_data, list) and self.test_data is not None
                ):
                    self.bases_eval_test = self.bases.eval(self.test_data)
                else:
                    raise ValueError("Must provide input test data.")

        if self.model_selection:
            self.store_iterates = True
            if (
                self.model_selection_options["type"] == "test_error"
                and not self.test_error
            ):
                print(
                    "Cannot perform test error based model selection, "
                    + "disabling it."
                )
                self.model_selection = False
            elif (
                self.model_selection_options["type"] == "cv_error"
                and not self.error_estimation
            ):
                self.error_estimation = True

        if self.rank_adaptation:
            if (
                "type" not in self.rank_adaptation_options
                or self.rank_adaptation_options["type"] == "standard"
            ):
                fun, output = self._solve_adaptation()
            elif isinstance(self.rank_adaptation_options["type"], str):
                adapt_type = self.rank_adaptation_options["type"]
                if adapt_type == "dmrg" or adapt_type == "dmrg_low_rank":
                    adapt_type = "dmrg"

                # Call the method corresponding to the rank adaptation option
                fun, output = eval(
                    "self._solve_" + adapt_type.lower() + "_rank_adaptation()"
                )
            else:
                raise ValueError(
                    "The rank_adaptation_options attribute "
                    + "must be either inexistant or a string."
                )
        elif self.algorithm == "standard":
            fun, output = self._solve_standard()
        else:
            fun, output = eval("self._solve_" + self.algorithm.lower() + "()")

        if self.model_selection:
            if self.model_selection_options["type"] == "test_error":
                ind = np.argmin(output["test_error_iterations"])
                fun = output["iterates"][ind]
                output["selected_model_number"] = ind
                if (
                    "error_iterations" in output
                    and np.size(output["error_iterations"]) > 0
                ):
                    output["error"] = output["error_iterations"][ind]
                output["test_error"] = output["test_error_iterations"][ind]
                if self.display:
                    print(
                        "\nModel selection using the test error: model "
                        + "#%i selected" % ind
                    )
            elif self.model_selection_options["type"] == "cv_error":
                ind = np.argmin(output["error_iterations"])
                fun = output["iterates"][ind]
                output["selected_model_number"] = ind
                if (
                    "test_error_iterations" in output
                    and np.size(output["test_error_iterations"]) > 0
                ):
                    output["test_error"] = output["test_error_iterations"][ind]
                output["error"] = output["error_iterations"][ind]
                if self.display:
                    print(
                        "\nModel selection using the cross-validation "
                        + " error: model #%i selected" % ind
                    )
            else:
                print("Wrong model selection type, returning the last " + "iterate.")

            if self.display:
                if self.alternating_minimization_parameters["display"]:
                    print("")
                self.final_display(fun)
                if "error" in output:
                    print(", error = %2.5e" % output["error"], end="")
                if "test_error" in output:
                    print(", test error = %2.5e" % output["test_error"], end="")
                print("")

        return fun, output

    def _solve_standard(self):
        """
        Solver for the learning problem with tensor formats using the standard
        algorithm (without adaptation).

        Raises
        ------
        ValueError
            If the number of LinearModelLearning objects is not equal to
            _numberOfParameters.

        Returns
        -------
        f : tensap.FunctionalTensor
            The learned approximation.
        output : dict
            The outputs of the solver.

        """
        output = {"flag": 0}

        self, f = self.initialize()
        f = tensap.FunctionalTensor(f, self.bases_eval)

        # Replication of the LinearModelLearning objects
        if self.linear_model_learning_parameters[
            "identical_for_all_parameters"
        ] and not isinstance(self.linear_model_learning, (list, np.ndarray)):
            self.linear_model_learning = list(
                map(deepcopy, [self.linear_model_learning] * self._number_of_parameters)
            )
        elif (
            isinstance(self.linear_model_learning, (list, np.ndarray))
            and len(self.linear_model_learning) != self._number_of_parameters
        ):
            raise ValueError(
                "Must provide self._numberOfParameters "
                + "LinearModelLearning objects."
            )

        if self.error_estimation:
            for x in self.linear_model_learning:
                setattr(x, "error_estimation", True)

        # Working set paths
        if (
            isinstance(self.linear_model_learning, (list, np.ndarray))
            and np.any([x.basis_adaptation for x in self.linear_model_learning])
            and self.bases_adaptation_path is None
        ):
            self.bases_adaptation_path = self.bases.adaptation_path()

        if self.alternating_minimization_parameters["max_iterations"] == 0:
            return f, output

        output["stagnation_criterion"] = []
        output["iterates"] = []
        output["error_iterations"] = []
        output["test_error_iterations"] = []

        # Alternating minimization loop
        for iteration in range(
            self.alternating_minimization_parameters["max_iterations"]
        ):
            self, f = self.pre_processing(f)
            f0 = deepcopy(f)

            if self.alternating_minimization_parameters["random"]:
                # Randomize the exploration strategy
                alpha_list = self.randomize_exploration_strategy()
            else:
                alpha_list = self._exploration_strategy

            for alpha in alpha_list:
                self, A, b, f = self.prepare_alternating_minimization_system(f, alpha)
                self.linear_model_learning[alpha - 1].training_data = [None, b]
                self.linear_model_learning[alpha - 1].basis = None
                self.linear_model_learning[alpha - 1].basis_eval = A

                coef, output_tmp = self.linear_model_learning[alpha - 1].solve()
                if (
                    coef is None
                    or np.count_nonzero(coef) == 0
                    or not np.all(np.isfinite(coef))
                ):
                    print(
                        "Empty, zero or NaN solution, returning to the "
                        + "previous iteration."
                    )
                    output["flag"] = -2
                    output["error"] = np.inf
                    break

                f = self.set_parameter(f, alpha, coef)

            stagnation = self.stagnation_criterion(f, f0)
            output["stagnation_criterion"].append(stagnation)

            if self.store_iterates:
                if isinstance(self.bases, tensap.FunctionalBases):
                    output["iterates"].append(
                        tensap.FunctionalTensor(f.tensor, self.bases)
                    )
                else:
                    output["iterates"].append(f)

            if "error" in output_tmp:
                output["error"] = output_tmp["error"]
                output["error_iterations"].append(output["error"])

            if self.test_error:
                f_eval_test = tensap.FunctionalTensor(f, self.bases_eval_test)
                output["test_error"] = self.loss_function.test_error(
                    f_eval_test, self.test_data
                )
                output["test_error_iterations"].append(output["test_error"])

            if self.alternating_minimization_parameters["display"]:
                print(
                    "\tAlt. min. iteration %s: stagnation = %2.5e"
                    % (
                        str(iteration).zfill(
                            len(
                                str(
                                    self.alternating_minimization_parameters[
                                        "max_iterations"
                                    ]
                                    - 1
                                )
                            )
                        ),
                        stagnation,
                    ),
                    end="",
                )
                if "error" in output:
                    print(", error = %2.5e" % output["error"], end="")
                if self.test_error:
                    if not np.isscalar(output["test_error"]):
                        output["test_error"] = output["test_error"].numpy()
                    print(", test error = %2.5e" % output["test_error"], end="")
                print("")

            if (
                iteration > 0
                and stagnation < self.alternating_minimization_parameters["stagnation"]
            ):
                output["flag"] = 1
                break

            if self.test_error and output["test_error"] < self.tolerance["on_error"]:
                output["flag"] = 1
                break

        if isinstance(self.bases, tensap.FunctionalBases):
            f = tensap.FunctionalTensor(f.tensor, self.bases)
        output["iter"] = iteration

        if self.display and not self.model_selection:
            if self.alternating_minimization_parameters["display"]:
                print("")
            self.final_display(f)
            if "error" in output:
                print(", error = %2.5e" % output["error"], end="")
            if "test_error" in output:
                print(", test error = %2.5e" % output["test_error"], end="")
            print("")

        return f, output

    def _solve_adaptation(self):
        """
        Solver for the learning problem with tensor formats using the adaptive
        algorithm.

        Returns
        -------
        f : tensap.FunctionalTensor
            The learned approximation.
        output : dict
            The outputs of the solver.

        """

        flag = 0
        output = {
            "enriched_nodes_iterations": np.empty(
                self.rank_adaptation_options["max_iterations"], dtype=object
            )
        }
        tree_adapt = False

        f = None
        errors = np.zeros(self.rank_adaptation_options["max_iterations"])
        test_errors = np.zeros(self.rank_adaptation_options["max_iterations"])
        iterates = np.empty(
            self.rank_adaptation_options["max_iterations"], dtype=object
        )

        # new_rank = s_local.rank
        new_rank = self.rank
        s_local = self.local_solver()
        s_local.model_selection = False

        enriched_nodes = np.array([])
        for iteration in range(self.rank_adaptation_options["max_iterations"]):
            s_local.bases = self.bases
            s_local.bases_eval = self.bases_eval
            s_local.bases_eval_test = self.bases_eval_test
            s_local.training_data = self.training_data
            s_local.test_data = self.test_data
            s_local.rank = new_rank

            f_old = deepcopy(f)
            f, output_local = s_local.solve()
            s_local = self.local_solver()

            if "error" in output_local:
                errors[iteration] = output_local["error"]
                if np.isinf(errors[iteration]):
                    print("Infinite error, returning to the previous iterate.")
                    f = f_old
                    iteration -= 1
                    flag = -2
                    break

            if self.test_error:
                f_eval_test = tensap.FunctionalTensor(f, self.bases_eval_test)
                test_errors[iteration] = self.loss_function.test_error(
                    f_eval_test, self.test_data
                )

            if self.store_iterates:
                if isinstance(self.bases, tensap.FunctionalBases):
                    iterates[iteration] = tensap.FunctionalTensor(f.tensor, self.bases)
                else:
                    iterates[iteration] = f

            if self.display:
                if self.alternating_minimization_parameters["display"]:
                    print("")
                print("\nRank adaptation, iteration %i:" % (iteration))
                self.adaptation_display(f, enriched_nodes)
                print("\tStorage complexity = %i" % f.tensor.storage(), flush=True)

                if errors[iteration] != 0:
                    print("\tError      = %2.5e" % errors[iteration])
                if test_errors[iteration] != 0:
                    print("\tTest error = %2.5e" % test_errors[iteration])
                if self.alternating_minimization_parameters["display"]:
                    print("")

            if iteration == self.rank_adaptation_options["max_iterations"] - 1:
                break

            if (
                self.test_error and test_errors[iteration] < self.tolerance["on_error"]
            ) or (
                "error" in output_local
                and errors[iteration] < self.tolerance["on_error"]
            ):
                flag = 1
                break

            fac = self.rank_adaptation_options["early_stopping_factor"]
            cond = iteration > 0 and (
                self.test_error
                and (
                    np.isnan(test_errors[iteration])
                    or fac * np.min(test_errors[:iteration]) < test_errors[iteration]
                )
                or (
                    "error" in output_local
                    and (
                        np.isnan(errors[iteration])
                        or fac * np.min(errors[:iteration]) < errors[iteration]
                    )
                )
            )
            if self.rank_adaptation_options["early_stopping"] and cond:
                print("Early stopping", end="")
                if "error" in output_local:
                    print(", error = %2.5e" % errors[iteration], end="")
                if self.test_error:
                    print(", test error = %2.5e" % test_errors[iteration], end="")
                print("\n")
                iteration -= 1
                f = f_old
                flag = -1
                break

            adapted_tree = False
            if (
                s_local.tree_adaptation
                and iteration > 0
                and (
                    not self.tree_adaptation_options["force_rank_adaptation"]
                    or not tree_adapt
                )
            ):
                C_old = f.tensor.storage()
                self, f, output = self.adapt_tree(
                    f, errors[iteration], None, output, iteration
                )
                adapted_tree = output["adapted_tree"]
                if adapted_tree:
                    if self.display:
                        print(
                            "\t\tStorage complexity before permutation "
                            + "= %i" % C_old
                        )
                        print(
                            "\t\tStorage complexity after permutation "
                            + "= %i" % f.tensor.storage()
                        )
                    if self.test_error:
                        f_eval_test = tensap.FunctionalTensor(f, self.bases_eval_test)
                        test_errors[iteration] = self.loss_function.test_error(
                            f_eval_test, self.test_data
                        )
                        if self.display:
                            print(
                                "\t\tTest error after permutation "
                                + "= %2.5e" % test_errors[iteration]
                            )

                    if self.alternating_minimization_parameters["display"]:
                        print("")

            if not self.tree_adaptation or not adapted_tree:
                if iteration > 0 and not tree_adapt:
                    stagnation = self.stagnation_criterion(
                        tensap.FunctionalTensor(f.tensor, self.bases_eval),
                        tensap.FunctionalTensor(f_old.tensor, self.bases_eval),
                    )
                    if stagnation < self.tolerance["on_stagnation"] or np.isnan(
                        stagnation
                    ):
                        break
                tree_adapt = False
                (
                    new_rank,
                    enriched_nodes,
                    tensor_for_initialization,
                ) = self.new_rank_selection(f)
                output["enriched_nodes_iterations"][iteration] = enriched_nodes
                s_local = self.initial_guess_new_rank(
                    s_local, tensor_for_initialization, new_rank
                )
            else:
                tree_adapt = True
                enriched_nodes = []
                new_rank = f.tensor.ranks
                s_local.initialization_type = "initial_guess"
                s_local.initial_guess = f.tensor

        if isinstance(self.bases, tensap.FunctionalBases):
            f = tensap.FunctionalTensor(f.tensor, self.bases)

        if self.store_iterates:
            output["iterates"] = iterates[: iteration + 1]

        output["flag"] = flag
        output["enriched_nodes_iterations"] = output["enriched_nodes_iterations"][
            : iteration + 1
        ]
        if "error" in output_local:
            output["error_iterations"] = errors[: iteration + 1]
            output["error"] = errors[iteration]

        if self.test_error:
            output["test_error_iterations"] = test_errors[: iteration + 1]
            output["test_error"] = test_errors[iteration]

        if "adapted_tree" in output:
            del output["adapted_tree"]

        return f, output

    def adapt_tree(self, f, cv_error, test_error, output, *args):
        """
        Tree adaptation algorithm.

        Parameters
        ----------
        f : tensap.Tensor
            The current tensor approximation.
        cv_error : float
            The current cross-validation error.
        test_error : float
            The current test error.
        output : dict
            The outputs of the solver.
        args : tuple
            Additional parameters.

        Returns
        -------
        tensap.TensorLearning
            The TensorLearning object.
        f : tensap.Tensor
            The current tensor approximation, perhaps with an adapted tree.
        output : dict
            The outputs of the solver.

        """
        return self, f, output

    @abstractmethod
    def initialize(self):
        """
        Initialization of the learning algorithm.

        Returns
        -------
        tensap.TensorLearning
            The TensorLearning object.
        f : tensap.FunctionalTensor
            The initialization of the solver.

        """

    @abstractmethod
    def pre_processing(self, f):
        """
        Initialization of the alternating minimization algorithm.

        Parameters
        ----------
        f : tensap.Tensor
            The current tensor approximation.

        Returns
        -------
        tensap.TensorLearning
            The TensorLearning object.
        f : tensap.Tensor
            The pre-processed tensor approximation.

        """

    @abstractmethod
    def randomize_exploration_strategy(self):
        """
        Randomization of the exploration strategy.

        Returns
        -------
        alpha_list : numpy.ndarray
            The randomized exploration strategy.

        """

    @abstractmethod
    def prepare_alternating_minimization_system(self, f, mu):
        """
        Preparation of the alternating minimization algorithm.

        Parameters
        ----------
        f : tensap.FunctionalTensor
            The current approximation.
        mu : int
            The number of the parameter to be optimized.

        Returns
        -------
        tensap.TensorLearning
            The TensorLearning object.
        A : numpy.ndarray
            The matricized partial evaluation of f.
        b : numpy.ndarray
            The target training data.
        f : tensap.FunctionalTensor
            The current approximation.

        """

    @abstractmethod
    def set_parameter(self, f, mu, coef):
        """
        Update of the parameter mu of the tensor f.

        Parameters
        ----------
        f : tensap.FunctionalTensor
            The current approximation.
        mu : int
            The number of the optimized parameter.
        coef : numpy.ndarray
            The new value of the parameter mu of f.

        Returns
        -------
        f : tensap.FunctionalTensor
            The updated approximation.

        """

    @abstractmethod
    def stagnation_criterion(self, f, f0):
        """
        Computation of the stagnation criterion.

        Parameters
        ----------
        f : tensap.FunctionalTensor
            The current approximation.
        f0 : tensap.FunctionalTensor
            The previous approximation.

        Returns
        -------
        stagnation : float
            The value of the stagnation criterion.

        """

    @abstractmethod
    def final_display(self, f):
        """
        Display at the end of the computation.

        Parameters
        ----------
        f : tensap.FunctionalTensor
            The current approximation.

        Returns
        -------
        None.

        """

    @abstractmethod
    def local_solver(self):
        """
         Extraction of the solver for the adaptive algorithm

        Returns
        -------
        s_local : tensap.TensorLearning
            The local solver.

        """

    @abstractmethod
    def new_rank_selection(self, f):
        """
        Selection of a new rank in the adaptive algorithm.

        Parameters
        ----------
        f : tensap.FunctionalTensor
            The current approximation.

        Returns
        -------
        new_rank : numpy.ndarray
            The new tensor rank.
        enriched_nodes : numpy.ndarray
            The enriched parameters.
        tensor_for_initialization : tensap.Tensor
            A tensor used for the initialization of the next iteration.

        """

    @abstractmethod
    def initial_guess_new_rank(self, s_local, f, new_rank):
        """
        Computation of the initial guess with the new selected rank.

        Parameters
        ----------
        s_local : tensap.TensorLearning
            The local solver.
        f : tensap.FunctionalTensor
            The current approximation.
        new_rank : numpy.ndarray
            The new tensor rank.

        Returns
        -------
        s_local : tensap.TensorLearning
            The local solver with the initial guess in s_local.initial_guess.

        """

    @abstractmethod
    def adaptation_display(self, f, enriched_nodes):
        """
        Display during the adaptation.

        Parameters
        ----------
        f : tensap.FunctionalTensor
            The current approximation.
        enriched_nodes : numpy.ndarray
            The enriched parameters.
        """
        pass
