From 87a2d0d7c0e838985fed9d621ac9878710f071b2 Mon Sep 17 00:00:00 2001 From: Lucas Teixeira Date: Thu, 23 Jul 2026 16:13:08 +0200 Subject: [PATCH 1/2] fix: sphinx auto documentation and doc updates on algorithm and device --- .github/workflows/documentation.yml | 7 +- docs/source/GC.rst | 115 +------- docs/source/GroupedTransforms.rst | 75 ++--- docs/source/_static/custom.css | 27 ++ docs/source/conf.py | 47 +++- .../GroupedCoefficients.py | 262 +++++++++++++----- src/pyGroupedTransforms/GroupedTransform.py | 200 +++++++++---- 7 files changed, 445 insertions(+), 288 deletions(-) create mode 100644 docs/source/_static/custom.css diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index a5da121..7aa8cea 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -11,13 +11,18 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 + with: + python-version: "3.12" - name: Install dependencies run: | pip install sphinx sphinx_rtd_theme myst_parser + # autodoc importiert den Code, um die Docstrings zu lesen. + # Ohne diese Zeile schlaegt der Import fehl und die Seiten bleiben leer. + # torch/pykeops werden via autodoc_mock_imports in conf.py simuliert. + pip install -e . - name: Sphinx build run: | cd docs - sphinx-apidoc -o ./source ../src -f make html - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v3 diff --git a/docs/source/GC.rst b/docs/source/GC.rst index 18f4ace..7b69433 100644 --- a/docs/source/GC.rst +++ b/docs/source/GC.rst @@ -1,106 +1,9 @@ -GroupedCoefficients -======================= - -.. py:class:: GC(settings, data) - - Superclass of GroupedCoefficientsComplex and GroupedCoefficientsReal. - A class to hold coefficients belonging to indices in a grouped index set. - - .. rubric:: Attributes: - - .. py:attribute:: settings - - List of setting objects (see bellow) - uniquely describes the setting such as the bandlimits - - .. py:attribute:: data - - Numpy array of dtype float or dtype complex - the vector of coefficients - - .. rubric:: Constructor: - - .. py:method:: GroupedCoefficients( setting, data = nothing ) - - - .. rubric:: Functions: - - .. py:method:: __getitem__(idx) - - If `idx` is a tuple that contains integer, - this function overloads getitem of GC such that you can do ``fhat[(1,3)]`` to obtain the basis coefficients of the corresponding ANOVA term defined by `u`. - - If `idx` is an integer, - this function overloads getitem of GC such that you can do ``fhat[1]`` to obtain the basis coefficient determined by `idx`. - - .. py:method:: __setitem__(idx, Number) - - If `idx` is a tuple that contains integer, - this function overloads setitem of GC such that you can do ``fhat[(1,3)] = [1 2 3]``. - - If `idx` is an integer, - this function overloads setitem of GC such that you can do ``fhat[1] = 3``. - - .. py:method:: vec() - - This function returns the vector of the basis coefficients of `self`. - - .. py:method:: __rmul__(alpha) - - This function defines multiplication of a number with a GC object. - - .. py:method:: __add__(other) - - This function defines the addition of two GC objects. - - .. py:method:: __sub__(other) - - This function defines the subtraction of two GC objects. - - .. py:method:: set_data(data) - - With this function one can set the data of a GC object. - - -.. py:function:: variances(j, m) - - matrix of variances between two basis functions, needed for wavelet basis, since they are not orthonormal. - - -.. py:class:: GroupedCoefficientsComplex(GC) - - A class to hold complex coefficients belonging to indices in a grouped index set. - - .. rubric:: Constructor: - - .. py:method:: GroupedCoefficientsComplex( setting, data = nothing ) - - -.. py:class:: GroupedCoefficientsReal(GC) - - A class to hold real coefficients belonging to indices in a grouped index set. - - .. rubric:: Constructor: - - .. py:method:: GroupedCoefficientsReal( setting, data = nothing ) - - -.. py:class:: Setting - - .. rubric:: Attributes: - - .. py:attribute:: u - - tuple of ints - - .. py:attribute:: mode - - string - - .. py:attribute:: bandwidths - - numpy array of dtype "int32" - - - .. py:attribute:: basis_vect - - list of strings - \ No newline at end of file +GroupedCoefficients +=================== + +.. automodule:: pyGroupedTransforms.GroupedCoefficients + :members: + :undoc-members: + :show-inheritance: + :member-order: alphabetical + :special-members: __getitem__, __setitem__, __add__, __sub__, __rmul__ diff --git a/docs/source/GroupedTransforms.rst b/docs/source/GroupedTransforms.rst index 7070387..892beb7 100644 --- a/docs/source/GroupedTransforms.rst +++ b/docs/source/GroupedTransforms.rst @@ -1,59 +1,16 @@ -GroupedTransform -======================= - -.. py:class:: GroupedTransform - - A class to describe a GroupedTransformation. - - .. rubric:: Attributes: - - .. py:attribute:: system - - A string - choice of "exp" or "cos" or "chui1" or "chui2" or "chui3" or "chui4" or "mixed". - - .. py:attribute:: settings - - List of setting objects - uniquely describes the setting such as the bandlimits. - - .. py:attribute:: X - - Numpy array of dtype float of dimension (M,d) - array of nodes. - - .. py:attribute:: transforms - - List of DeferredLinearOperator objects - holds the low-dimensional sub transformations. - - .. py:attribute:: basis_vect - - List of strings - holds for every dimension if a cosinus basis [true] or exponential basis [false] is used. - - - - .. rubric:: Constructor: - - .. py:method:: GroupedTransform( system, X, settings = settings, basis_vect = basis_vect) - - .. rubric:: Additional Constructor: - - .. py:method:: GroupedTransform( system, X, d=d, ds = ds, N =N basis_vect = basis_vect) - - .. py:method:: GroupedTransform( system, X, U = U, N = N basis_vect = basis_vect) - - - .. rubric:: Functions: - - .. py:method:: `*` - - If `F` is a GroupedTransfom object and `f` is a numpy array, - this overloads the * notation in order to achieve the adjoint transform `f = F*f`. - - If `F` is a GroupedTransform object and fhat is a GroupedCoefficient object, - this overloads the * notation in order to achieve `f = F*fhat`. - - .. py:method:: adjoint - - Overloads the `F'` notation and gives back the same GroupdTransform. GroupedTransform decides by the input if it is the normal trafo or the adjoint so this is only for convinience. - - .. py:method:: __getitem__ - - This function overloads `[]` of GroupedTransform such that you can do `F[(1,3)]` to obtain the transform of the corresponding ANOVA term defined by `u`. \ No newline at end of file +GroupedTransform +================ + +.. automodule:: pyGroupedTransforms.GroupedTransform + :no-members: + +.. autoclass:: pyGroupedTransforms.GroupedTransform.GroupedTransform + :members: + :undoc-members: + :show-inheritance: + :member-order: bysource + :special-members: __mul__, __getitem__ + +.. autofunction:: pyGroupedTransforms.GroupedTransform.get_superposition_set + +.. autofunction:: pyGroupedTransforms.GroupedTransform.get_setting diff --git a/docs/source/_static/custom.css b/docs/source/_static/custom.css new file mode 100644 index 0000000..c2c0000 --- /dev/null +++ b/docs/source/_static/custom.css @@ -0,0 +1,27 @@ +/* Custom overrides to widen the main content area for sphinx_rtd_theme */ + +/* Increase the maximum content width */ +.wy-nav-content, +.rst-content { + max-width: 100vw; + min-height: 100vh; + background-color: redsss; +} + +/* Slightly reduce left padding for large screens so content uses more space */ +@media (min-width: 1200px) { + .wy-nav-content { + padding-left: 20px; + padding-right: 20px; + } +} + +/* Ensure the main document content uses more horizontal space */ +.rst-content .section, .document { + margin-left: 0; + margin-right: 0; +} + +.rubric { + font-size: 20px; +} diff --git a/docs/source/conf.py b/docs/source/conf.py index ab16626..904a91e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -3,6 +3,13 @@ # For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html +import os +import sys + +# -- Path setup -------------------------------------------------------------- +# Macht den src-Ordner für Sphinx und autodoc auffindbar +sys.path.insert(0, os.path.abspath("../../src")) + # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information @@ -14,14 +21,48 @@ # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration -extensions = ["sphinx.ext.mathjax"] +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", # Unterstützt Google/NumPy-Style Docstrings + "sphinx.ext.mathjax", + "sphinx.ext.viewcode", + "sphinx.ext.intersphinx", + "myst_parser", +] + +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), + "numpy": ("https://numpy.org/doc/stable/", None), + "scipy": ("https://docs.scipy.org/doc/scipy/", None), +} + +# -- autodoc ----------------------------------------------------------------- +autodoc_default_options = { + "members": True, + "undoc-members": True, + "show-inheritance": True, + "member-order": "bysource", +} + +# GroupedTransform.py importiert torch und pykeops, die aber nur in den +# Methodenkoerpern benutzt werden. Beide sind sehr gross und werden fuer die +# Docstrings nicht gebraucht, deshalb werden sie hier nur simuliert. +autodoc_mock_imports = ["torch", "pykeops"] -templates_path = ["_templates"] +templates_path = [] exclude_patterns = [] # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_theme = "alabaster" +html_theme = "sphinx_rtd_theme" html_static_path = ["_static"] +html_theme_options = { + "collapse_navigation": False, + "sticky_navigation": True, + "navigation_depth": 5, +} +html_css_files = [ + 'custom.css', +] diff --git a/src/pyGroupedTransforms/GroupedCoefficients.py b/src/pyGroupedTransforms/GroupedCoefficients.py index 4bbf792..ac015fa 100644 --- a/src/pyGroupedTransforms/GroupedCoefficients.py +++ b/src/pyGroupedTransforms/GroupedCoefficients.py @@ -1,22 +1,54 @@ +"""Coefficients belonging to the indices of a grouped index set. + +:py:class:`GC` is the shared base class; use the :py:func:`GroupedCoefficients` +factory rather than instantiating :py:class:`GroupedCoefficientsComplex` or +:py:class:`GroupedCoefficientsReal` directly, since it picks the right one from +the mode of the given settings. + +A :py:class:`Setting` describes one ANOVA term — its dimensions, transform mode +and bandwidths — and a list of them uniquely fixes the layout of the +coefficient vector. +""" + import numpy as np import scipy from pyGroupedTransforms import CWWTtools, NFCTtools, NFFTtools, NFMTtools -class GC: # Superclass of GroupedCoefficientsComplex and GroupedCoefficientsReal. +class GC: + r"""Base class for grouped coefficient vectors. + + Superclass of :py:class:`GroupedCoefficientsComplex` and + :py:class:`GroupedCoefficientsReal`. Holds the coefficients of every ANOVA + term in one flat vector, together with the settings that describe how that + vector is partitioned. + + Attributes: + settings (list[Setting]): One entry per ANOVA term, uniquely + describing the layout such as the bandwidths. + data (numpy.ndarray): The flat vector of coefficients, real or complex + depending on the subclass. + """ def __getitem__(self, idx): - """ - (fhat::GroupedCoefficients[u::Vector{Int}]) + r"""Return coefficients by ANOVA term or by position. + + - a tuple selects a whole ANOVA term: ``fhat[(1, 3)]`` returns the + basis coefficients of the term defined by ``u`` + - an integer selects a single coefficient: ``fhat[1]`` - If idx is a tuple that contains integer, - this function overloads getitem of GC such that you can do `fhat[(1,3)]` to obtain the basis coefficients of the corresponding ANOVA term defined by `u`. + Args: + idx (tuple[int] or int): The ANOVA term, or the position in the + flat coefficient vector. - (fhat::GroupedCoefficients[idx::Int]) + Returns: + numpy.ndarray or numbers.Number: The coefficients of the term, or + the single coefficient. - If idx is an integer, - this function overloads getitem of GC such that you can do `fhat[1]` to obtain the basis coefficient determined by `idx`. + Raises: + ValueError: If the requested term is not part of the settings. + TypeError: If ``idx`` is neither an int nor a tuple of ints. """ if type(idx) == int: return self.data[idx] @@ -37,16 +69,21 @@ def __getitem__(self, idx): ) def __setitem__(self, idx, Number): - """ - (fhat::GroupedCoefficients[u::Vector{Int}] = fhatu::Union{Vector{ComplexF64},Vector{Float64}}) + r"""Set coefficients by ANOVA term or by position. - If idx is a tuple that contains integer, - this function overloads setitem of GC such that you can do `fhat[(1,3)] = [1 2 3]` to set the basis coefficients of the corresponding ANOVA term defined by `u`. + - a tuple sets a whole ANOVA term: ``fhat[(1, 3)] = np.array([1, 2, 3])`` + - an integer sets a single coefficient: ``fhat[1] = 3`` - (fhat::GroupedCoefficients[idx::Int] = z::Number) + Args: + idx (tuple[int] or int): The ANOVA term, or the position in the + flat coefficient vector. + Number (numpy.ndarray or numbers.Number): The value(s) to store. + Must match the real/complex type of the object. - If idx is an integer, - this function overloads setitem of GC such that you can do `fhat[1] = 3` to set the basis coefficient determined by `idx`. + Raises: + ValueError: If the requested term is not part of the settings. + TypeError: If ``idx`` has the wrong type, or if a real object is + given complex data or vice versa. """ if type(idx) == int: self.data[idx] = Number @@ -77,18 +114,21 @@ def __setitem__(self, idx, Number): raise TypeError("Index must be an int or a tuple that contains integer") def vec(self): - """ - (vec( fhat::GroupedCoefficients )::Vector{<:Number}) + r"""Return the flat vector of basis coefficients. - This function returns the vector of the basis coefficients of self. + Returns: + numpy.ndarray: The underlying coefficient vector. """ return self.data def __rmul__(self, alpha): - """ - (*( z::Number, fhat::GroupedCoefficients )::GroupedCoefficients) + r"""Multiply the coefficients by a scalar. - This function defines the multiplication of a number with a GC object. + Args: + alpha (numbers.Number): The scalar factor. + + Returns: + GC: A new object holding the scaled coefficients. """ if isinstance(alpha, (int, float, complex)): return GroupedCoefficients(self.settings, alpha * self.data) @@ -101,10 +141,16 @@ def __mul__(self, alpha): return NotImplemented def __add__(self, other): - """ - (+( z::Number, fhat::GroupedCoefficients )::GroupedCoefficients) + r"""Add two grouped coefficient objects. + + Args: + other (GC): The object to add. Must carry the same settings. - This function defines the addition of two GC objects. + Returns: + GC: A new object holding the summed coefficients. + + Raises: + ValueError: If the two objects have different settings. """ if not isinstance(other, GC): return NotImplemented @@ -115,20 +161,28 @@ def __add__(self, other): raise ValueError("Settings mismatch.") def __sub__(self, other): - """ - (-( z::Number, fhat::GroupedCoefficients )::GroupedCoefficients) + r"""Subtract two grouped coefficient objects. - This function defines the subtraction of two GC objects. + Args: + other (GC): The object to subtract. Must carry the same settings. + + Returns: + GC: A new object holding the difference. + + Raises: + ValueError: If the two objects have different settings. """ return self + (-1 * other) def set_data(self, data): - """ - (set_data!( - fhat::GroupedCoefficients, - data::Union{Vector{ComplexF64},Vector{Float64}},)) + r"""Overwrite the coefficient vector in place. - With this function one can set the data of a GC object. + Args: + data (numpy.ndarray): The new coefficients. Must match the + real/complex type of the object. + + Raises: + TypeError: If a real object is given complex data, or vice versa. """ if ( isinstance(self, GroupedCoefficientsComplex) @@ -142,16 +196,25 @@ def set_data(self, data): return None def norms(self, Dict=False, other=None, m=None): - """ - If other == None: - (norms(fhat::GroupedCoefficients; dict =false)) - - If other != None and m == None: - (norms(fhat::GroupedCoefficients, what::GroupedCoefficients)::Vector{Float64}) - - If other == None and m != None: - (function norms(fhat::GroupedCoefficients, m::Int ; dict =false)::Union{Vector{Float64},Dict{Vector{Int},Float64}}) - Inputs sind vertauscht leider... + r"""Compute the norm of each ANOVA term's coefficient block. + + These norms are what the variance and sensitivity analyses are built + on. For the Chui-Wang wavelet bases the basis functions are not + orthonormal, so passing the wavelet order ``m`` switches to a weighted + norm using the Gram matrix from :py:func:`variances`. + + Args: + Dict (bool, optional): Defaults to ``False``. If ``True``, return a dictionary indexed by ANOVA + term, skipping the empty term. Otherwise return a list in + settings order. + other (GC, optional): Compare against a second coefficient object + instead of taking plain norms. + m (int, optional): Wavelet order. Required to get correct norms for + the ``"chui*"`` bases; ignored for the orthonormal bases. + + Returns: + list or dict: One norm per ANOVA term, keyed by term if ``Dict`` + is set. """ if other == None: @@ -246,18 +309,30 @@ def norms(self, Dict=False, other=None, m=None): # Matrix of variances between two basis functions, needed for wavelet basis, since they are not orthonormal def variances(j, m): - """ - (variances(j::Int,m::Int)::Vector{Float64}) + r"""Return the scalar products between wavelets of one level. + + The Chui-Wang wavelets are not orthonormal, so computing norms of wavelet + coefficients needs the Gram matrix of the basis. This returns the first row + of that matrix for level ``j``, from which the full (circulant) matrix is + reconstructed by :py:meth:`GC.norms`. + + Formally the result is + + .. math:: + y = \left( \langle \psi_{j,0}, \psi_{j,k} \rangle + \right)_{k=0,\dots,2^j-1}, - INPUT - j ... level of wavelet - m ... order of wavelet - OUTPUT - y = ()for k = 0...2^j-1 - (psi_{j,k}) are the wavelets, output contains a vector of all scalar products of one level - for 2^j >2m*1 the same values, but more zeros for higher j. + where :math:`\psi_{j,k}` are the wavelets of level :math:`j`. For + :math:`2^j > 2m` the nonzero values repeat and only the number of trailing + zeros grows with :math:`j`. + Args: + j (int): Level of the wavelet. + m (int): Order of the wavelet, 2 to 4. + + Returns: + numpy.ndarray: The :math:`2^j` scalar products of level ``j``. """ if m == 2: if j == 0: @@ -350,6 +425,30 @@ def variances(j, m): class Setting: + r"""Describes one ANOVA term within a grouped index set. + + A list of these uniquely fixes the layout of a grouped coefficient vector + and of a :py:class:`GroupedTransform`. Two settings compare equal when + their mode, dimensions, bandwidths and bases all agree. + + Args: + u (tuple[int]): The coordinate subset this term covers. The empty + tuple denotes the constant term. + mode (module): The transform module handling this term — one of + :py:mod:`NFFTtools`, :py:mod:`NFCTtools`, :py:mod:`NFMTtools` or + :py:mod:`CWWTtools`. Defaults to :py:mod:`NFFTtools`. + bandwidths (numpy.ndarray): Bandwidths for this term, dtype + ``int32``, one entry per dimension in ``u``. + bases (list[str], optional): Per-dimension basis, used by the + ``"mixed"`` system. + + Attributes: + u (tuple[int]): The coordinate subset. + mode (module): The transform module. + bandwidths (numpy.ndarray): The bandwidths. + bases (list[str]): The per-dimension bases. + """ + def __init__(self, u=None, mode=NFFTtools, bandwidths=None, bases=None): self.u = u self.mode = mode @@ -385,20 +484,23 @@ def __eq__(self, other): return NotImplemented -# A class to hold complex coefficients belonging to indices in a grouped index set - -### Fields: -# * `setting::Vector{NamedTuple{(:u, :mode, :bandwidths, :bases),Tuple{Vector{Int},Module,Vector{Int},Vector{String}}}}` - uniquely describes the setting such as the bandlimits ``N_{\pmb u}``, see also [`get_setting(system::String,d::Int,ds::Int,N::Vector{Int},basis_vect::Vector{String})::Vector{NamedTuple{(:u, :mode, :bandwidths, :bases),Tuple{Vector{Int},Module,Vector{Int},Vector{String}}}}`](@ref) and #[`get_setting(system::String,U::Vector{Vector{Int}},N::Vector{Int},basis_vect::Vector{String})::Vector{NamedTuple{(:u, :mode, :bandwidths, :bases),Tuple{Vector{Int},Module,Vector{Int},Vector{String}}}}`](@ref) -# * `data::Union{Vector{ComplexF64},Nothing}` - the vector of coefficients +class GroupedCoefficientsComplex(GC): + r"""Complex coefficients belonging to a grouped index set. -## Constructor -# GroupedCoefficientsComplex( setting, data = nothing ) + Created by the :py:func:`GroupedCoefficients` factory for settings whose + mode is :py:mod:`NFFTtools`. -## Additional Constructor -# GroupedCoefficients( setting, data = nothing ) + Args: + settings (list[Setting]): One entry per ANOVA term, fixing the layout + of the coefficient vector. + data (numpy.ndarray, optional): The coefficients. If omitted, a zero + vector of the required length is allocated. + Raises: + ValueError: If ``data`` has the wrong length for ``settings``. + RuntimeError: If a setting's mode does not provide ``datalength``. + """ -class GroupedCoefficientsComplex(GC): def __init__(self, settings, data=None): try: @@ -417,20 +519,23 @@ def __init__(self, settings, data=None): self.data = data -# A class to hold real coefficients belonging to indices in a grouped index set - -### Fields -# * `setting::Vector{NamedTuple{(:u, :mode, :bandwidths, :bases),Tuple{Vector{Int},Module,Vector{Int},Vector{String}}}}` - uniquely describes the setting such as the bandlimits ``N_{\pmb u}``, see also [`get_setting(system::String,d::Int,ds::Int,N::Vector{Int},basis_vect::Vector{String})::Vector{NamedTuple{(:u, :mode, :bandwidths, :bases),Tuple{Vector{Int},Module,Vector{Int},Vector{String}}}}`](@ref) and [`get_setting(system::String,U::Vector{Vector{Int}},N::Vector{Int},basis_vect::Vector{String})::Vector{NamedTuple{(:u, :mode, :bandwidths, :bases),Tuple{Vector{Int},Module,Vector{Int},Vector{String}}}}`](@ref) -# * `data::Union{Vector{ComplexF64},Nothing}` - the vector of coefficients +class GroupedCoefficientsReal(GC): + r"""Real coefficients belonging to a grouped index set. -## Constructor -# GroupedCoefficientsComplex( setting, data = nothing ) + Created by the :py:func:`GroupedCoefficients` factory for settings whose + mode is :py:mod:`NFCTtools` or :py:mod:`CWWTtools`. -## Additional Constructor -# GroupedCoefficients( setting, data = nothing ) + Args: + settings (list[Setting]): One entry per ANOVA term, fixing the layout + of the coefficient vector. + data (numpy.ndarray, optional): The coefficients. If omitted, a zero + vector of the required length is allocated. + Raises: + ValueError: If ``data`` has the wrong length for ``settings``. + RuntimeError: If a setting's mode does not provide ``datalength``. + """ -class GroupedCoefficientsReal(GC): def __init__(self, settings, data=None): try: @@ -450,6 +555,21 @@ def __init__(self, settings, data=None): def GroupedCoefficients(settings, data=None): + r"""Create a grouped coefficient object matching the given settings. + + Picks :py:class:`GroupedCoefficientsComplex` or + :py:class:`GroupedCoefficientsReal` from the transform mode of the + settings. This is the intended way to construct coefficients. + + Args: + settings (list[Setting]): One entry per ANOVA term. + data (numpy.ndarray, optional): The coefficients. If omitted, a zero + vector of the required length is allocated. + + Returns: + GC: A complex object for :py:mod:`NFFTtools` settings, a real one for + :py:mod:`NFCTtools` and :py:mod:`CWWTtools`. + """ if settings[0].mode == NFFTtools: return GroupedCoefficientsComplex(settings, data) if settings[0].mode == NFCTtools or settings[0].mode == CWWTtools: diff --git a/src/pyGroupedTransforms/GroupedTransform.py b/src/pyGroupedTransforms/GroupedTransform.py index 2587a89..8149fd0 100644 --- a/src/pyGroupedTransforms/GroupedTransform.py +++ b/src/pyGroupedTransforms/GroupedTransform.py @@ -1,3 +1,15 @@ +"""Grouped transformations for ANOVA-style index sets. + +The central object is :py:class:`GroupedTransform`, which bundles the +low-dimensional sub-transforms belonging to each ANOVA term into a single +operator. Applying it to a :py:class:`GroupedCoefficients` object evaluates the +function; applying it to a vector of samples computes the adjoint. + +:py:func:`get_superposition_set` builds the index set for a given dimension and +interaction order, and :py:func:`get_setting` turns an index set and bandwidths +into the :py:class:`Setting` list a transform needs. +""" + import numpy as np import torch @@ -22,10 +34,21 @@ def get_superposition_set(d, ds): - """ - get_superposition_set( d::Int, ds::Int )::Vector{Vector{Int}} + r"""Return the superposition set of order ``ds`` in ``d`` dimensions. + + That is + + .. math:: + + U^{(d,ds)} = \{ \pmb u \subset \{1,2,\dots,d\} : |\pmb u| \leq ds \}. + + Args: + d (int): Number of dimensions. + ds (int): Maximum interaction order. - This function returns ``U^{(d,ds)} = \{ \pmb u \subset \{1,2,\dots,d\} : |\pmb u| \leq ds \}``. + Returns: + list[tuple[int]]: All subsets of ``{0, ..., d-1}`` of size at most + ``ds``, starting with the empty tuple. """ nset = [[j] for j in range(d)] returnset = [[]] + nset @@ -44,7 +67,28 @@ def get_superposition_set(d, ds): def get_setting( system, N, U=None, d=None, ds=None ): # I have to add bases and NFMT stuff here! - + r"""Build the :py:class:`Setting` list describing an ANOVA index set. + + The index set can be given either explicitly through ``U``, or implicitly + through ``d`` and ``ds``, in which case it is generated by + :py:func:`get_superposition_set`. Passing ``d`` takes precedence. + + Args: + system (str): One of ``"exp"``, ``"cos"``, ``"chui1"`` … ``"chui4"`` + or ``"mixed"``. + N (list[int]): Bandwidths. With ``d``/``ds`` there must be exactly + ``ds`` entries, one per interaction order. + U (list[tuple[int]], optional): Explicit index set. + d (int, optional): Number of dimensions, used with ``ds``. + ds (int, optional): Maximum interaction order, used with ``d``. + + Returns: + list[Setting]: One setting per ANOVA term. + + Raises: + ValueError: If ``system`` is unknown, or if ``N`` does not have ``ds`` + entries when ``d`` is given. + """ if system not in systems: raise ValueError("System not found.") @@ -164,32 +208,62 @@ def get_setting( ] -###GroupedTransform - -# A struct to describe a GroupedTransformation - -## Fields -# * `system::String` - choice of `"exp"` or `"cos"` or `"chui1"` or `"chui2"` or `"chui3"` or `"chui4"` or `"mixed"` -# * `setting::Vector{NamedTuple{(:u, :mode, :bandwidths, :bases),Tuple{Vector{Int},Module,Vector{Int},Vector{String}}}}` - vector of the dimensions, mode, bandwidths and bases for each term/group, see also [`get_setting(system::String,d::Int,ds::Int,N::Vector{Int},basis_vect::Vector{String})::Vector{NamedTuple{(:u, :mode, :bandwidths, :bases),Tuple{Vector{Int},Module,Vector{Int},Vector{String}}}}`](@ref) and [`get_setting(system::String,U::Vector{Vector{Int}},N::Vector{Int},basis_vect::Vector{String})::Vector{NamedTuple{(:u, :mode, :bandwidths, :bases),Tuple{Vector{Int},Module,Vector{Int},Vector{String}}}}`](@ref) -# * `X::Array{Float64}` - array of nodes -# * `transforms::Vector{LinearMap}` - holds the low-dimensional sub transformations -# * `basis_vect::Vector{String}` - holds for every dimension if a cosinus basis [true] or exponential basis [false] is used -# -## Constructor -# GroupedTransform( system, setting, X, basis_vect::Vector{String} = Vector{String}([]) ) -# -## Additional Constructor -# GroupedTransform( system, d, ds, N::Vector{Int}, X, basis_vect::Vector{String} = Vector{String}([]) ) -# GroupedTransform( system, U, N, X, basis_vect::Vector{String} = Vector{String}([]) ) - - class GroupedTransform: + r"""A grouped transformation over an ANOVA index set. + + Bundles the low-dimensional sub-transforms belonging to each ANOVA term + into one operator. The settings can be supplied in three ways: + + - directly, as a list of :py:class:`Setting` objects via ``settings`` + - by index set, passing ``U`` and ``N`` + - by dimension and order, passing ``d``, ``ds`` and ``N`` + + Args: + system (str): Which family of sub-transforms to use. One of ``"exp"`` + (exponentials, NFFT), ``"cos"`` (cosine, NFCT), ``"chui1"`` … + ``"chui4"`` (Chui-Wang wavelets), or ``"mixed"`` (per-dimension, + see ``basis_vect``). + X (numpy.ndarray): Nodes, ``M`` rows and ``d`` columns. Must lie in + ``[-0.5, 0.5)`` for ``"exp"`` and the ``"chui*"`` systems, and in + ``[0, 0.5]`` for ``"cos"``. + settings (list[Setting]): Pre-built settings. If empty, they are + derived from ``U``/``N`` or ``d``/``ds``/``N``. + algorithm (str, optional): Defaults to ``"nfft"``. ``"nfft"`` for the fast transforms, ``"keops"`` for implicit matrix multiplication (allow GPU or CPU computation), + or ``"direct"`` to build the matrix explicitly. + device: Device used by the ``"keops"`` algorithm, allows user to manually switch between GPU or CPU computation. Supported values are: ``"cuda"`` + for NVIDIA and CUDA-supporting devices, ``"mps"`` for Apple MPS-supporting devices, ``"cpu"`` for selecting CPU processing. + If ``None`` (default), the algorithm automatically selects the device based on availability, prioritizing GPU use. + parallel (bool): Run the sub-transforms threaded. Defaults to ``True``. + basis_vect (list[str]): Per-dimension basis, required for + ``system="mixed"``; must have one entry per dimension. + N (list): Bandwidths, one entry per ANOVA term or per interaction + order. + U (list[tuple[int]], optional): Index set defining the ANOVA terms. + d (int, optional): Number of dimensions, used with ``ds``. + ds (int, optional): Maximum interaction order, used with ``d``. + + Attributes: + system (str): The chosen family of sub-transforms. + settings (list[Setting]): One entry per ANOVA term, describing its + dimensions, mode and bandwidths. + X (numpy.ndarray): The nodes. + transforms (list): The low-dimensional sub-transformations. + basis_vect (list[str]): Per-dimension basis choice. + algorithm (str): ``"nfft"`` or ``"direct"``. + parallel (bool): Whether sub-transforms run threaded. + + Raises: + ValueError: If ``system`` is unknown, or if the nodes in ``X`` lie + outside the range the system requires. + """ + def __init__( self, system, X, settings=[], algorithm="nfft", + device=None, parallel=True, basis_vect=[], N=[], @@ -236,6 +310,7 @@ def __init__( algorithm = "nfft" self.algorithm = algorithm + self.device = device self.basis_vect = basis_vect self.system = system self.X = X @@ -274,13 +349,16 @@ def __init__( elif algorithm == "keops": self.matrix = np.empty((0,0), dtype=object) - if torch.cuda.is_available(): - device = "cuda" - elif torch.mps.is_available(): - device = "mps" - else: - device = "cpu" - + if self.device: + device = device + else: + if torch.cuda.is_available(): + device = "cuda" + elif torch.mps.is_available(): + device = "mps" + else: + device = "cpu" + self.transforms = [ DeferredLinearOperator() for _ in range(len(self.settings)) @@ -458,16 +536,22 @@ def adjoint(f): self.matrix = matrix def __mul__(self, other): - """ - (*( F::GroupedTransform, fhat::GroupedCoefficients )::Vector{<:Number}) + r"""Apply the transform, or its adjoint, with the ``*`` operator. + + The direction is chosen from the type of ``other``: - If other (= fhat) is an object of GC, this function - overloads the `*` notation in order to achieve `f = F*fhat`. + - a :py:class:`GroupedCoefficients` object evaluates the function, + ``f = F * fhat`` + - a :py:class:`numpy.ndarray` of samples applies the adjoint, + ``fhat = F * f`` - (*( F::GroupedTransform, f::Vector{<:Number} )::GroupedCoefficients) + Args: + other (GC or numpy.ndarray): Coefficients to evaluate, or samples + to transform back. - If other (= f) is an numpy.ndarray, this function - overloads the * notation in order to achieve the adjoint transform `f = F*f`. + Returns: + numpy.ndarray or GC: Function values for coefficient input, + grouped coefficients for sample input. """ if isinstance(other, np.ndarray): # `f = F*f` (f = other) if self.algorithm == "nfft": @@ -541,25 +625,41 @@ def __matmul__(self, other): return self.__mul__(other) def adjoint(self): - """ - adjoint( F::GroupedTransform )::GroupedTransform + r"""Return the transform itself, for symmetry with the ``F'`` notation. + + :py:meth:`__mul__` decides from its argument whether to apply the + forward or the adjoint transform, so no separate adjoint object is + needed. This exists purely for convenience. - Overloads the `F'` notation and gives back the same GroupdTransform. GroupedTransform decides by the input if it is the normal trafo or the adjoint so this is only for convinience. + Returns: + GroupedTransform: ``self``. """ return self @property def H(self): - """ - Overloads the `F'` notation and gives back the same GroupdTransform. GroupedTransform decides by the input if it is the normal trafo or the adjoint so this is only for convinience. + r"""Alias for :py:meth:`adjoint`, so ``F.H`` reads like the usual notation. + + Returns: + GroupedTransform: ``self``. """ return self def __getitem__(self, u): - """ - F::GroupedTransform[u::Vector{Int}]::LinearMap{<:Number} or SparseArray + r"""Return the sub-transform belonging to one ANOVA term. + + Lets you write ``F[(1, 3)]`` to obtain the transform of the ANOVA term + defined by ``u``. - This function overloads getindex of GroupedTransform such that you can do `F[[1,3]]` to obtain the transform of the corresponding ANOVA term defined by `u`. + Args: + u (tuple[int]): The coordinate subset identifying the term. + + Returns: + The sub-transform for ``u`` — a linear operator for the ``"nfft"`` + algorithm, otherwise the corresponding matrix block. + + Raises: + ValueError: If ``u`` is not one of the terms in this transform. """ idx = next( (i for i, s in enumerate(self.settings) if len(s.u) == len(u) and s.u == u), @@ -574,10 +674,14 @@ def __getitem__(self, u): return self.get_matrix() def get_matrix(self): - """ - get_matrix( F::GroupedTransform )::Matrix{<:Number} + r"""Assemble and return the transformation as an explicit matrix. + + Returns: + numpy.ndarray: The full transformation matrix. - This function returns the actual matrix of the transformation. This is not available for the wavelet basis. + Raises: + ValueError: For the wavelet systems ``"chui1"`` … ``"chui4"``, + which have no direct matrix representation. """ if self.system in ["chui1", "chui2", "chui3", "chui4"]: raise ValueError( From 11adaadd54016a672059a48760bd2ca1c17b0473 Mon Sep 17 00:00:00 2001 From: texlucas Date: Thu, 23 Jul 2026 14:13:52 +0000 Subject: [PATCH 2/2] Auto code format --- docs/source/conf.py | 4 +- src/pyGroupedTransforms/GroupedTransform.py | 78 +++++++++++---------- src/pyGroupedTransforms/NFFTtools.py | 8 +-- 3 files changed, 46 insertions(+), 44 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 904a91e..fd74ea4 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -26,7 +26,7 @@ "sphinx.ext.napoleon", # Unterstützt Google/NumPy-Style Docstrings "sphinx.ext.mathjax", "sphinx.ext.viewcode", - "sphinx.ext.intersphinx", + "sphinx.ext.intersphinx", "myst_parser", ] @@ -64,5 +64,5 @@ "navigation_depth": 5, } html_css_files = [ - 'custom.css', + "custom.css", ] diff --git a/src/pyGroupedTransforms/GroupedTransform.py b/src/pyGroupedTransforms/GroupedTransform.py index 8149fd0..fa943ba 100644 --- a/src/pyGroupedTransforms/GroupedTransform.py +++ b/src/pyGroupedTransforms/GroupedTransform.py @@ -11,14 +11,13 @@ """ import numpy as np +import pykeops import torch +from pykeops.torch import LazyTensor from pyGroupedTransforms import * -from .NFFTtools import index_set_without_zeros - -import pykeops -from pykeops.torch import LazyTensor +from .NFFTtools import index_set_without_zeros # All code that is linked to NFMTtools or to system = "mixed" is not tested yet.... @@ -229,8 +228,8 @@ class GroupedTransform: settings (list[Setting]): Pre-built settings. If empty, they are derived from ``U``/``N`` or ``d``/``ds``/``N``. algorithm (str, optional): Defaults to ``"nfft"``. ``"nfft"`` for the fast transforms, ``"keops"`` for implicit matrix multiplication (allow GPU or CPU computation), - or ``"direct"`` to build the matrix explicitly. - device: Device used by the ``"keops"`` algorithm, allows user to manually switch between GPU or CPU computation. Supported values are: ``"cuda"`` + or ``"direct"`` to build the matrix explicitly. + device: Device used by the ``"keops"`` algorithm, allows user to manually switch between GPU or CPU computation. Supported values are: ``"cuda"`` for NVIDIA and CUDA-supporting devices, ``"mps"`` for Apple MPS-supporting devices, ``"cpu"`` for selecting CPU processing. If ``None`` (default), the algorithm automatically selects the device based on availability, prioritizing GPU use. parallel (bool): Run the sub-transforms threaded. Defaults to ``True``. @@ -347,23 +346,22 @@ def __init__( bandwidths=s.bandwidths, X=np.copy(X[:, u], order="C") ) elif algorithm == "keops": - self.matrix = np.empty((0,0), dtype=object) - + self.matrix = np.empty((0, 0), dtype=object) + if self.device: device = device - else: + else: if torch.cuda.is_available(): device = "cuda" elif torch.mps.is_available(): device = "mps" else: device = "cpu" - + self.transforms = [ - DeferredLinearOperator() - for _ in range(len(self.settings)) + DeferredLinearOperator() for _ in range(len(self.settings)) ] - + D = X.shape[1] freq_list = [] @@ -372,8 +370,7 @@ def __init__( if len(s.bandwidths) == 0: full = np.zeros((1, D), dtype=np.int32) - - + local = np.atleast_2d( s.mode.index_set_without_zeros( np.array(s.bandwidths, dtype=np.int32) @@ -388,13 +385,9 @@ def __init__( freq_list.append(full) freq = np.vstack(freq_list) - + X_torch = torch.tensor(X, dtype=torch.float64, device=device) - I_torch = torch.tensor( - freq, - dtype=torch.float64, - device=device - ) + I_torch = torch.tensor(freq, dtype=torch.float64, device=device) def trafo(fhat): if self.system == "cos" or self.system == "cheb": @@ -403,8 +396,8 @@ def trafo(fhat): kernel = 1.0 for i in range(D): - Xi = LazyTensor(X_torch[:, None, i:i+1].contiguous()) - Ki = LazyTensor(I_torch[None, :, i:i+1].contiguous()) + Xi = LazyTensor(X_torch[:, None, i : i + 1].contiguous()) + Ki = LazyTensor(I_torch[None, :, i : i + 1].contiguous()) kernel = kernel * (2 * torch.pi * Xi * Ki).cos() fhat_torch = ( @@ -428,7 +421,9 @@ def trafo(fhat): two_pi_phase = -2 * torch.pi * phase_fwd kernel = two_pi_phase.cos() + 1j * two_pi_phase.sin() - fhat_torch = torch.tensor(fhat, dtype=torch.complex128, device=device) + fhat_torch = torch.tensor( + fhat, dtype=torch.complex128, device=device + ) fhat_j = LazyTensor(fhat_torch[None, :, None].contiguous()) try: @@ -443,15 +438,15 @@ def trafo(fhat): def adjoint(f): if self.system == "cos" or self.system == "cheb": mult = np.sqrt(2.0) ** np.count_nonzero(freq, axis=1) - + f_torch = torch.as_tensor( f, dtype=torch.float64, device=device ).contiguous() kernel = 1.0 for i in range(D): - Ki = LazyTensor(I_torch[:, None, i:i+1].contiguous()) - Xi = LazyTensor(X_torch[None, :, i:i+1].contiguous()) + Ki = LazyTensor(I_torch[:, None, i : i + 1].contiguous()) + Xi = LazyTensor(X_torch[None, :, i : i + 1].contiguous()) kernel = kernel * (2 * torch.pi * Xi * Ki).cos() f_i = LazyTensor(f_torch[None, :, None].contiguous()) @@ -465,7 +460,9 @@ def adjoint(f): print("Error in KeOps adjoint:", e) return None else: - f_torch = torch.tensor(f, dtype=torch.complex128, device=device).contiguous() + f_torch = torch.tensor( + f, dtype=torch.complex128, device=device + ).contiguous() X_i = LazyTensor(X_torch[None, :, :].contiguous()) K_j = LazyTensor(I_torch[:, None, :].contiguous()) @@ -487,10 +484,15 @@ def adjoint(f): return None keops_dtype = np.float64 if self.system == "cos" else np.complex128 - self.transforms = [DeferredLinearOperator( - dtype=keops_dtype, shape=(X.shape[0], len(freq)), mfunc=trafo, rmfunc=adjoint - )] - + self.transforms = [ + DeferredLinearOperator( + dtype=keops_dtype, + shape=(X.shape[0], len(freq)), + mfunc=trafo, + rmfunc=adjoint, + ) + ] + else: self.transforms = [] s1 = self.settings[0] @@ -576,15 +578,15 @@ def adjoint_worker(i): adjoint_worker(i) return fhat - + elif self.algorithm == "keops": - return GroupedCoefficients(self.settings, self.transforms[0].H @ other) - + return GroupedCoefficients(self.settings, self.transforms[0].H @ other) + elif self.algorithm == "direct": return GroupedCoefficients( self.settings, (self.matrix.conj()).T @ other ) - + elif isinstance(other, GC): # `f = F*fhat` (fhat = other) if self.settings != other.settings: raise ValueError( @@ -612,10 +614,10 @@ def worker(i): worker(i) return sum(results) - + elif self.algorithm == "keops": return self.transforms[0] @ other.data - + elif self.algorithm == "direct": return self.matrix @ other.data else: diff --git a/src/pyGroupedTransforms/NFFTtools.py b/src/pyGroupedTransforms/NFFTtools.py index d0b02c6..432ac47 100644 --- a/src/pyGroupedTransforms/NFFTtools.py +++ b/src/pyGroupedTransforms/NFFTtools.py @@ -1,11 +1,10 @@ import numpy as np - -from pyGroupedTransforms import * - -import torch import pykeops +import torch from pykeops.torch import LazyTensor +from pyGroupedTransforms import * + def datalength( bandwidths: np.ndarray, @@ -173,6 +172,7 @@ def adjoint(f): # function adjoint(f::Vector{ComplexF64})::Vector{ComplexF64} dtype=np.complex128, shape=(M, N), mfunc=trafo, rmfunc=adjoint ) + def get_matrix( bandwidths, X ): # get_matrix(bandwidths::Vector{Int}, X::Array{Float64})::Array{ComplexF64}