hofa.rgz
========

.. py:module:: hofa.rgz

.. autoapi-nested-parse::

   This module contains the Numpy implementation for performing higher-order Fourier analysis 
   using the spectral approach developed by Candela, González-Sánchez, and Szegedy.

   Conventions
   -----------
   The following conventions are used for functions and data structures in this file:

   - Any finite abelian group :math:`Z` is isomorphic to :math:`Z/n_1Z \times Z/n_2Z \times ... \times Z/n_kZ`. Therefore, a function 
     :math:`f: Z \to \mathbb{C}` is represented as a ``numpy.ndarray`` tensor with shape ``(n_1, n_2, ..., n_k)``.
     
   - A :math:`Z`-matrix, where :math:`Z = Z/n_1Z \times ... \times Z/n_kZ`, is represented as a ``numpy.ndarray`` tensor with shape 
     ``(n_1, ..., n_k, n_1, ..., n_k)``.



Classes
-------

.. autoapisummary::

   hofa.rgz.LayerRegularizer
   hofa.rgz.StandardLayerRegularizer
   hofa.rgz.RegularizationResult


Functions
---------

.. autoapisummary::

   hofa.rgz.regularize
   hofa.rgz.regularize_after_setup
   hofa.rgz.regularize_and_decompose
   hofa.rgz.weighed_projection
   hofa.rgz.default_reg_list
   hofa.rgz.move_diag_to_rows
   hofa.rgz.move_rows_to_diag
   hofa.rgz.apply_on_diag
   hofa.rgz.t_prod_itself


Module Contents
---------------

.. py:class:: LayerRegularizer

   Bases: :py:obj:`abc.ABC`


   Abstract base class for an abstract cumulative distribution function.

   This class will contain all necessary information to project a function
   onto the eigenspace of a certain matrix where the projection to the 
   different eigenspaces is weighted according to an :py:func:`LayerRegularizer.alpha` method.

   The user must instantiate this class (or use the class :class:`StandardLayerRegularizer` provided
   by this module).


   .. py:method:: setup(f: numpy.ndarray, depth: int, layer: int)
      :abstractmethod:


      Method to be called once at the beginning of the regularization process.

      As input, it takes the function ``f`` to regularize, the total order ``depth`` of
      regularization, (1 for linear/classical Fourier analysis, 2 for quadratic, etc.), 
      and the position ``layer`` of this regularizer in the algorithm. That is, ``layer`` 
      will be an integer between ``0`` and ``depth-1``, representing which part of the regularization 
      process this instance of :class:`LayerRegularizer` will perform. For example, 0 means Fourier 
      regularization (acting on the squared norms of the Fourier coefficients of some multiplicative derivative), 
      1 means quadratic, etc.

      :param f: A anrray containing the function ``f`` to regularize.
      :type f: np.ndarray

      :param depth: An integer representing the total order of regularization. 
                  (1 for Fourier regularization, 2 for quadratic Fourier, etc.)
      :type depth: int

      :param layer: An integer specifying the position of this regularizer in the process. 
                  It will be an integer between 0 and ``depth-1``. 
                  For example, 0 means Fourier regularization, 1 means quadratic regularization, etc.
      :type layer: int

      :return: None

      :raises NotImplementedError: If the method is not overridden in a subclass.



   .. py:method:: update(f: numpy.ndarray, height: int)
      :abstractmethod:


      Update the object with the provided parameters at each step of the regularization.

      This is an abstract method that should be implemented by subclasses 
      to update the state of the :class:`LayerRegularizer` object at each step of the regularization
      process. Specifically, for an instance of :class:`LayerRegularizer` where the :py:func:`setup` method
      was called with ``depth = k`` and ``layer = l``, the :py:func:`update` method will be called
      ``k-l`` times, with ``height`` between ``k`` and ``l+1``. At each call, the implementation
      of the :py:func:`update` method can modify the internal state of the :class:`LayerRegularizer` class
      using the provided function ``f``.

      For example, when doing cubic regularization (``k=3``), the :py:func:`setup` method will be called 
      with ``layer=1`` and the original function to regularize. In this case, the :py:func:`update` method will be called ``|Z| + 1`` times, once with ``height = 3`` (with ``f`` being the original function to regularize :math:`f_0`), and ``|Z|`` times with ``height=2`` (with ``f`` being :math:`\Delta_t f_0` for :math:`t \in Z`). The purpose of calling the method with these different heights is to keep track of all multiplicative derivatives made during the regularization process.

      :param f: A array representing a multiplicative derivative :math:`\Delta_{t_1}\cdots\Delta_{t_{k-h}} f_0`, where :math:`f_0` is the original function to regularize. At each step, ``f`` changes according to the value of ``height`` and the regularization depth.
      :type f: np.ndarray

      :param height: An integer representing the number of multiplicative derivatives taken.
      :type height: int

      :return: None

      :raises NotImplementedError: If the method is not overridden in a subclass.



   .. py:method:: alpha(eigs: numpy.ndarray)
      :abstractmethod:


      This is the regularizer that will be used to weight the different
      eigenspaces.

      The implementation of any class can use the information about the
      specific function to regularize, see :class:`StandardLayerRegularizer`

      :param eigs: An array of eigenvalues to obtain the weights of the different eigenspaces.
      :type eigs: np.ndarray

      :return: An array of the same shape as ``eigs`` with the weights of the different eigenspaces.
      :rtype: np.ndarray

      :raises NotImplementedError: If the method is not overridden in a subclass.



   .. py:method:: eigh(M: numpy.ndarray)
      :abstractmethod:


      A method to obtain a number of eigenvalues and eigenvectors.

      As the :py:func:`LayerRegularizer.alpha` function typically cancels the contribution of eigenspaces
      with small eigenvalues, there is usually no need to compute the full
      eigendecomposition of the matrix ``M``. Thus, instead of using a typical
      method such as ``numpy.linalg.eigh``, which would compute the full
      eigendecomposition, the user may define a custom method which is more efficient.

      :param M: A self-adjoint matrix to perform regularization on.
      :type M: np.ndarray

      :return: A tuple containing the eigenvalues and eigenvectors of the matrix `M`.
          The format of this tuple follows the `numpy` standard of ordering the
          eigenvalues increasingly and returning the corresponding ``i``-th eigenvector
          indexed by ``[...,i]``.
      :rtype: tuple of (np.ndarray, np.ndarray)

      :raises NotImplementedError: If the method is not overridden in a subclass.



   .. py:method:: __deepcopy__(memo)

      General-purpose deepcopy implementation for ABC.

      Handles both __dict__-based and __slots__-based attributes, and
      preserves cycles and shared references.

      :param dict memo: Dictionary of already-copied objects to handle recursion.
      :return: A deep copy of this object.
      :rtype: LayerRegularizer



   .. py:method:: copy()

      Create a deep copy of this object.

      This method returns a new instance of the concrete subclass,
      with all attributes deep-copied. Cycles and shared references
      are handled correctly.

      :return: A new independent deep copy of this object.
      :rtype: Base

      :note: Relies on the object's `__deepcopy__` implementation. Concrete
             subclasses with special internal state (e.g., RNGs) will automatically
             have their custom deepcopy logic executed.



.. py:class:: StandardLayerRegularizer(alpha_method: Callable = hofa.cdf.relu, param: float | int = 1.2, mode: str = 'dynamic-original', lin_alg_method: str = 'sparse', num_eigen: str | int = 'dynamic', rng: int | numpy.random.Generator | None = None)

   Bases: :py:obj:`LayerRegularizer`


   An implementation of the :class:`LayerRegularizer` class with common functionality.

   This class contains the implementation of a :class:`LayerRegularizer` with the
   functionality described in the paper.

   :param alpha_method: A function that defines the alpha calculation method. By default, it uses 
                        :py:func:`hofa.cdf.relu` as the method. Here we can use any user-defined method
                        that has a signature of the form ``function( f : numpy.ndarray , coefficient : float | int) -> numpy.ndarray``. Examples of admisible functions are:   

                        - :py:func:`hofa.cdf.relu`

                        - :py:func:`hofa.cdf.cutoff`

                        - :py:func:`hofa.cdf.avg_cutoff`

                        - :py:func:`hofa.cdf.top_eig`

                        - :py:func:`hofa.cdf.u2_dual` 

   :type alpha_method: Callable
   :param param: A floating-point or integer parameter used for various purposes depending on the ``mode`` below. Default value is 1.2. 
   :type param: float | int, optional
       
   :param mode: A string representing the mode of operation. This determines how the ``alpha_method``
                will interpret parameters and perform regularization.
                For the rest of this explanation :math:`k` will denote the order or regularization
                (1 classical Fourier, 2 quadratic, etc.), :math:`f_0` will be the original function
                to regularize, :math:`f_1` will denote a multiplicative derivative, :math:`f_2` two
                multiplicative derivatives, etc. Note that and an :class:`StandardLayerRegularizer` 
                object of height
                :math:`0 \le j < k` will perform regularization of height :math:`j`, which means that it will act
                on a function :math:`f_{k-j-1}`, i.e., on the :math:`k-j-1` multiplicative derivatives
                of the original function. In order to choose an appropriate parameter, the
                :class:`StandardLayerRegularizer` object will keep track of all the 
                typical deviations of the multiplicative
                derivatives made to compute :math:`f_{k-j-1}`. Those typical deviations will be denoted
                :math:`\sigma_0` (for :math:`f_0`), :math:`\sigma_1` (for :math:`f_1`) etc.
                Possible values:

                - ``'dynamic-original'``: In this mode, we use the variace of the original function but modified using the fact that we are using it on a multiplicative derivative of ome order. In this mode, the parameter passed to the ``alpha_method`` will be ``param`` multiplied by :math:`\sigma_0^{2^{k-j-1}}\sqrt{|Z|/\log|Z|}`.

                - ``'dynamic-strict'``: In this mode, we use the variace of the function we are regularizing. In this mode, the parameter passed to the ``alpha_method`` will be: ``param`` multiplied by :math:`\sigma_{k-j-1}\sqrt{|Z|/\log|Z|}`.

                - ``'literal'``: Uses the parameter ``param`` directly.

                Default is 'dynamic-original'.
   :type mode: str, optional

   :param lin_alg_method: The method for computing the eigendecomposition of a matrix. Possible values:

                           - ``'sparse'``: Uses ``scipy.sparse.linalg.eigsh`` to compute eigenvalues.

                           - ``'full'``: Uses ``numpy.linalg.eigh`` to compute eigenvalues.

                           Default is ``'sparse'``.
   :type lin_alg_method: str, optional

   :param num_eigen: The number of eigenvalues and eigenvectors to compute when using ``'sparse'`` 
                      method in ``lin_alg_method``. Possible values:

                      - ``'dynamic'``: Computed dynamically according to a heuristic.

                      - an ``int``: Specifies the number of eigenvalues to compute.

                      Default is ``'dynamic'``.
   :type num_eigen: str | int, optional
   :param rng: Either an integer seed, a np.random.Generator, or None.
               It is used for deterministic behaviour of scipy.sparse.linalg.eigsh
   :type rng: int | np.random.Generator | None


   .. py:attribute:: alpha_method


   .. py:attribute:: param
      :value: 1.2



   .. py:attribute:: mode
      :value: 'dynamic-original'



   .. py:attribute:: lin_alg_method
      :value: 'sparse'



   .. py:attribute:: num_eigen
      :value: 'dynamic'



   .. py:attribute:: total_depth
      :value: 0



   .. py:attribute:: layer
      :value: 0



   .. py:attribute:: array_sigma_sq


   .. py:attribute:: group
      :value: 'no group'



   .. py:attribute:: group_size
      :value: 0



   .. py:attribute:: _initialized
      :value: False



   .. py:method:: setup(f: numpy.ndarray, depth: int, layer: int)

      Setup method of all :class:`StandardLayerRegularizer` in a list to perform regularization

      This method will records some important information that this object
      will need in order to perform the regularization. Namely, it records 
      the size of the group, the total order of regularization, the position of this 
      regularizer in the process, and initialize an array to keep track of the variances 
      of the different multiplicative derivatives involved in the process (to be used 
      for computing the parameter for the :py:func:`StandardLayerRegularizer.alpha`).

      :param f: An array containing the original function ``f`` to regularize.
      :type f: np.ndarray

      :param depth: An integer representing the total order of regularization, 
          i.e., ``1`` for Fourier regularization, ``2`` for quadratic Fourier, etc.
      :type depth: int

      :param layer: An integer specifying the position of this regularizer in the process.
          It will be an integer between ``0`` and ``depth-1``, with ``0`` meaning that
          it will do the Fourier regularization part, ``1`` the quadratic, etc.
      :type layer: int

      :return: None




   .. py:method:: _require_initialized()

      Ensures that the StandardLayerRegularizer has been properly initialized before use.

      This method checks whether the internal state of the instance of 
      StandardLayerRegularizer has been initialized
      via the :py:func:`StandardLayerRegularizer.setup` method. 
      It is intended to be called at the beginning of any method
      that requires access to the runtime state, such as 
      :py:func:`StandardLayerRegularizer.update` or other internal operations.

      :raises RuntimeError: If the LayerRegularizer has not been initialized with :py:func:`StandardLayerRegularizer.setup`.

      This method does **not** modify any internal state. Its sole purpose is to enforce
      the correct usage pattern: construction → setup → algorithm execution.



   .. py:method:: update(f: numpy.ndarray, height: int)

      Update the object with the provided parameters at each step of the
      regularization.

      This method records the variance of the different multiplicative derivatives
      to be used later in :py:func:`StandardLayerRegularizer.alpha`. More precisely,
      ``depth=k`` and ``layer=l`` be the parameters that were used to
      call the :py:func:`StandardLayerRegularizer.setup` function for this object.
      Let ``height=h`` and :math:`Z` be the group where ``f`` is defined. 
      Recall that this object has to perform regularization of functions
      of the form :math:`\Delta_{t_1}\cdots\Delta_{t_{k-l-1}} f_0`. Then this
      method will keep track of the variances of :math:`f_0`, 
      :math:`\Delta_{t_{k-l-1}} f_0`, ..., :math:`\Delta_{t_1}\cdots\Delta_{t_{k-l-1}} f_0`
      so that this information is used when :py:func:`StandardLayerRegularizer.alpha` is called.

      :param f: A multiplicative derivative of the form :math:`\Delta_{t_1}\cdots\Delta_{t_{k-h}} f_0`.
              
      :type f: np.ndarray

      :param height: An integer between ``l+1`` and ``k`` representing the number 
          of multiplicative derivatives taken.
      :type height: int

      :return: None
      :rtype: None



   .. py:method:: alpha(eigs: numpy.ndarray)

      This method applies the :py:attr:`alpha_method` with a parameter computed according to
      :py:attr:`mode` and :py:attr:`param` as explained in :class:`StandardLayerRegularizer`.

      :param eigs: An array of eigenvalues to obtain the weights of the different eigenspaces.
      :type eigs: np.ndarray

      :return: An array of the same shape as ``eigs`` with the weights of the different eigenspaces.
      :rtype: np.ndarray



   .. py:method:: eigh(M: numpy.ndarray)

      A method to obtain a number of eigenvalues and eigenvectors.

      Depending on :py:attr:`lin_alg_method` given in the constructor, this method will use either
      ``numpy.linalg.eigh`` or ``scipy.sparse.linalg.eigsh`` to compute (part of) the 
      eigendecomposition of a matrix ``M``.

      :param M: A self-adjoint matrix to perform regularization.
      :type M: np.ndarray

      :return: A tuple containing the eigenvalues and eigenvectors.
      :rtype: tuple(np.ndarray, np.ndarray)

      :note: The returned tuple of eigenvalues and eigenvectors follow
          the same format as ``numpy.linalg.eigh``.



   .. py:method:: __deepcopy__(memo)

      Deepcopy implementation that creates a new RNG for the copy.

      :param dict memo: Dictionary of already-copied objects to handle recursion.
      :return: A new copy of this object with an independent RNG.
      :rtype: StandardLayerRegularizer



.. py:class:: RegularizationResult

   Bases: :py:obj:`NamedTuple`


   A class containing the output of the regularition algorithm

   This class contains the 3 outputs of the regularization algorithm.
   That is, the regularization of the function, the eigenvalues
   of the matrix :math:`\mathcal{K}(f \otimes \overline{f})` in 
   increasing order, and their corresponding eigenvectors.

   :param regularization: The result of the regularization.
   :type regularization: np.ndarray

   :param eigenvalues: A 1-dimensional array of shape ``(N,)`` with the eigenvalues in 
       increasing order.
   :type eigenvectors: np.ndarray

   :param eigenvectors: The corresponding eigenvectors. It has shape
       ``(f.shape, N)`` where ``N`` is the number of eigenvalues returned.
   :type mode: np.ndarray



   .. py:attribute:: regularization
      :type:  numpy.ndarray


   .. py:attribute:: eigenvalues
      :type:  numpy.ndarray


   .. py:attribute:: eigenvectors
      :type:  numpy.ndarray


.. py:function:: regularize(f: numpy.ndarray, per_layer_regularizers: List[LayerRegularizer] | int = 2, rng: int | numpy.random.Generator | None = None) -> RegularizationResult

   Perform the regularization of a function using certain :class:`LayerRegularizer` s.

   The behaviour of this function depends on the number and type of ``per_layer_regularizers``.
   If ``per_layer_regularizers`` is an integer, then it is used a default list of 
   :class:`LayerRegularizer` s given by :py:func:`default_reg_list` is used.
   Otherwise it is used the list provided by the user. It returns the result
   of the regularization algorithm and returns a :class:`RegularizationResult` object
   containing the regularization of ``f``, the eigenvalues of the matrix 
   :math:`\mathcal{K}(f \otimes \overline{f})` and its corresponding eigenvectors.
   The eigenvalues are a ``N`` dimensional ``numpy.andarray`` where ``N``
   is the number of eigenvalues returned by the :py:func:`RegularizationResult.eigh` 
   method of ``per_layer_regularizers[-1]`` and are returned in increasing order. 
   The eigenvectors are returned as a ``numpy.andarray`` of shape ``(f.shape,N)``. 
   The eigenvalue in position ``i`` corresponds to the eigenvector indexed at
   ``[...,i]``. There is some special behaviour in the following cases.

   - If ``len(per_layer_regularizers)=0``, 
       this function returns as a regularization an
       ``numpy.andarray`` array with shape equal to that of ``f``, the average of ``f``
       as the sole eigenvalue and the constant 1 ``numpy.andarray`` of shape ``f.shape``
       as the sole eigenvector.

   - If ``len(per_layer_regularizers)=1``, 
       this function returns the squares of the 
       Fourier coefficients along with the full Fourier basis of the group :math:`Z` as
       the eigenvalues (ordered in increasing order) and the full set of Fourier
       characters of :math:`Z` as the eigenvectors.

   :param f: An array representing the function to regularize.
   :type f: numpy.ndarray

   :param per_layer_regularizers: A list of :class:`LayerRegularizer` objects that will be set up using 
       the array ``f`` or an integer to use a default list of such length. The 
       :class:`LayerRegularizer` object at index ``i`` will perform regularization of order ``i+1``. 
       That is, ``per_layer_regularizers[0]`` will act on multiplicative derivatives of the form :math:`\Delta_{t_1}\cdots\Delta_{t_{k-1}} f` where ``len(per_layer_regularizers)=k``. 
       Similarly, ``per_layer_regularizers[1]`` will act on multiplicative derivatives of the form :math:`\Delta_{t_1}\cdots\Delta_{t_{k-2}} f`, etc.

   :type per_layer_regularizers: list of LayerRegularizer objects or an integer.

   :param rng: To have predictable behaviour in case the user uses an integer in the list of
               regularizers.
   :type rng: int | np.random.Generator | None

   :return: The regularization of ``f`` according to ``per_layer_regularizers`` and the eigenvalues 
       and eigenvectors corresponding to the last step in the process.
   :rtype: RegularizationResult



.. py:function:: regularize_after_setup(f: numpy.ndarray, per_layer_regularizers: List[LayerRegularizer])

   Internal process that implements the main steps of the regularization.

   This function, which requires being called with :class:`LayerRegularizer` s
   already set up, performs the main recursive step in the regularization
   algorithm.

   :param f: An array representing the function to regularize.
   :type f: np.ndarray

   :param per_layer_regularizers: A list of :class:`LayerRegularizer` objects.
   :type per_layer_regularizers: List[LayerRegularizer]

   :return: The result of the regularization of ``f``, the eigenvalues, and the
       eigenvectors of the last regularization step.
   :rtype: tuple(np.ndarray,np.ndarray,np.ndarray)


.. py:function:: regularize_and_decompose(f: numpy.ndarray, K: Callable, layer_regularizer: LayerRegularizer)

   Compute the eigenvalues and eigenvectors of a regularized Z-matrix.

   This function takes a Z-function ``f`` (represented as a ``numpy.ndarray``), 
   computes the Z-matrix :math:`M = f \otimes f^*`, applies a regularizer ``K`` 
   to the Z-diagonals of ``M`` to form the Z-matrix :math:`\mathcal{K}(M)`, and 
   then returns the eigenvalues in increasing order and eigenvectors of `K(M)`
   ordered according to its corresponding eigenvalue.

   :param f: A Z-function, represented as a ``numpy.ndarray``.
   :type f: np.ndarray

   :param K: A regularizer, i.e., an operator that acts on Z-functions. 
       The operator ``K`` must be "invariant," meaning 
       :math:`K(f(\cdot+t)) = K(f)(\cdot+t)` and :math:`K(\overline{f}) = \overline{K(f)}`
       If these conditions are not met, the behavior of the function is unpredictable. 
   :type K: Callable

   :param layer_regularizer: The regularizer that contains the method for computing 
       the eigendecomposition of the matrix once the diagonals are regularized.
   :type layer_regularizer: LayerRegularizer

   :return: 
       - A ``numpy.ndarray`` of shape ``(total_dim,)`` , where ``total_dim`` is the 
           product of the elements in ``f.shape``, i.e., the size of the group Z 
           containing the eigenvalues of the Z-matrix :math:`\mathcal{K}(M)`, 
           appropriately normalized and ordered increasingly. 
       - A ``numpy.ndarray`` of shape ``(f.shape, total_dim)``, where ``total_dim`` 
           is as before and ``eigenvectors[...,i]`` is the eigenvector corresponding 
           to ``eigenvalue[i]``.
   :rtype: tuple (np.ndarray, np.ndarray)

   :note: The operator ``K`` must satisfy the invariance conditions for predictable behavior.



.. py:function:: weighed_projection(f: numpy.ndarray, eigvals: numpy.ndarray, eigvects: numpy.ndarray, layer_regularizer: LayerRegularizer)

   Projects a function to certain eigenspaces with weights depending on eigenvalues.

   This function first computes a weight for each eigenvalue, represented by ``eigvals``. 
   Then, it projects ``f`` to all eigenspaces. Such projections are summed in a weighted 
   manner depending on the weights computer by the ``layer_regularizer`` variable according to
   its ``alpha`` method.

   :param f: A :math:`Z`-function, represented as a ``numpy.ndarray``. The function 
       :math:`f: Z \to \mathbb{C}` is assumed to be provided in this form.
   :type f: numpy.ndarray

   :param eigvals: A dimensional array of eigenvalues. It must have shape 
       ``(N,)``.
   :type eigvals: np.ndarray

   :param eigvects: An array of orthonormal eigenvectors. It must have shape 
       ``(f.shape, N)`` where ``N`` is the dimension of ``eigvals``.
   :type eigvects: np.ndarray

   :param layer_regularizer: A regularizer to be used to weight the corresponding eigenspaces.
   :type layer_regularizer: LayerRegularizer

   :return: A ``numpy.ndarray`` of shape equal to ``f.shape`` with the weighted 
       projection of ``f`` to the eigenspaces weighted according to ``layer_regularizer``.
   :rtype: np.ndarray

   :note: This function is meant to be used in conjunction with ``eig(f,K)``. 
       Undefined behaviour otherwise.



.. py:function:: default_reg_list(k: int, rng: int | numpy.random.Generator | None = None)

   Returns a list of pre-defined regularizers.

   This function gives back a list with pre-defined regularizers that have been tested in practice.
   Namely, it returns a list of :class:`StandardLayerRegularizer` where all but the two last ones are
   initialized with ``num_eigen = 6``, the previous to the last one using the empty constructor,
   and the last one uses ``alpha_method = hofa.cdf.cutoff``.

   :param k: The length of the list. If smaller or equal to ``0`` is the same as passing ``0``.
   :type k: int

   :param rng: A random number generator for reproducibilty purposes. It is used to fix the seed of the StandardLayerRegularizer
   :type rng: int | np.random.Generator | None

   :return: A list of default LayerRegularizers of length ``k``
   :rtype: List[LayerRegularizer]


.. py:function:: move_diag_to_rows(M: numpy.ndarray)

   Creates a new Z-matrix where each row is a Z-diagonal of ``M``.

   Inverse of :py:func:`move_rows_to_diag`. This function takes a Z-matrix ``M`` of 
   shape ``(n_1, ..., n_k, n_1, ..., n_k)``, and returns a new matrix 
   whose rows are the Z-diagonals of ``M``. The output matrix has the same 
   shape as ``M``. The returned matrix does not share memory with the input ``M``.

   :param M: A Z-matrix of shape ``(n_1, ..., n_k, n_1, ..., n_k)``, where 
       :math:`n_i \ge 1`. Undefined behavior occurs if this condition is not met.
   :type M: numpy.ndarray

   :return: A Z-matrix of the same shape as ``M``, where the elements of ``M[i, i+z]`` 
       are transferred to the output position ``[z, i]``.
   :rtype: numpy.ndarray

   :note: This function reverses the operation performed by :py:func:`move_rows_to_diag`.



.. py:function:: move_rows_to_diag(M: numpy.ndarray)

   Reconstructs the Z-matrix from its row representation.

   Inverse of :py:func:`move_diag_to_rows`. This function takes a Z-matrix ``M`` of shape 
   ``(n_1, ..., n_k, n_1, ..., n_k)``, and returns a new matrix 
   where the rows of ``M`` become the Z-diagonals of the returned matrix. 
   The output matrix has the same shape as ``M``, and the returned values 
   do not share memory with the input matrix.

   :param M: A Z-matrix of shape ``(n_1, ..., n_k, n_1, ..., n_k)``, 
       where :math:`n_i \ge 1`. Undefined behavior occurs if this condition is not met.
   :type M: numpy.ndarray

   :return: A Z-matrix of the same shape as ``M``, such that for each pair ``[i, z]``, 
       ``output[i, i+z]`` equals ``M[z, i]``.
   :rtype: numpy.ndarray

   :note: This function reverses the operation performed by :py:func:`move_diag_to_rows`.



.. py:function:: apply_on_diag(M: numpy.ndarray, K: Callable)

   Apply a regularizer to all Z-diagonals of a Z-matrix.

   This function applies the operator ``K`` to each Z-diagonal of the input 
   Z-matrix ``M``. The operation is performed on the diagonals indexed by 
   ``[i, i+z]``, where the function ``K`` is applied to the corresponding 
   values of ``M[i, i+z]``.

   :param M: A Z-matrix of shape ``(n_1, ..., n_k, n_1, ..., n_k)``, 
       where :math:`n_i \ge 1`. Undefined behavior occurs if this condition is not met.
   :type M: numpy.ndarray

   :param K: A regularizer operator that acts on Z-functions. The operator ``K`` 
       must be "invariant".
   :type K: Callable

   :return: A Z-matrix of the same shape as ``M``, where for each pair ``[i, z]``, 
       ``output[i, i+z]`` equals ``K(M[i, i+z])``.
   :rtype: np.ndarray

   :note: The operator ``K`` must satisfy the invariance conditions for 
       predictable behavior.



.. py:function:: t_prod_itself(g: numpy.ndarray)

   Compute the outer product of a Z-function with itself conjugate.

   This function takes a Z-function ``g`` and returns the Z-matrix formed 
   by computing the outer product of ``g`` with its conjugate transpose, 
   denoted as :math:`g\otimes g^*`.

   :param g: A Z-function. The shape of ``g`` must be compatible for the outer 
       product operation.
   :type g: numpy.ndarray

   :return: A Z-matrix of shape ``(g.shape, g.shape)``, representing the outer 
       product of ``g`` and its conjugate transpose.
   :rtype: numpy.ndarray

   :note: The outer product is performed element-wise, and the resulting matrix 
       has a shape of ``(g.shape, g.shape)``.



