From b3890d62620dce62586057ba81d2f6f20fbc0f20 Mon Sep 17 00:00:00 2001 From: Rastislav Turanyi Date: Fri, 5 Sep 2025 15:45:46 +0100 Subject: [PATCH 1/7] Add HODLR theory section --- docs/source/index.rst | 1 + docs/source/theory.rst | 12 + docs/source/theory/hodlr_theory.rst | 122 ++++++++++ docs/source/theory/img/conversion2.svg | 302 +++++++++++++++++++++++++ docs/source/theory/img/convert.svg | 168 ++++++++++++++ docs/source/theory/img/dense.svg | 60 +++++ docs/source/theory/img/partition.svg | 256 +++++++++++++++++++++ docs/source/theory/img/svd.svg | 197 ++++++++++++++++ docs/source/theory/img/svd2.svg | 213 +++++++++++++++++ docs/source/theory/img/svd3.svg | 217 ++++++++++++++++++ 10 files changed, 1548 insertions(+) create mode 100644 docs/source/theory.rst create mode 100644 docs/source/theory/hodlr_theory.rst create mode 100644 docs/source/theory/img/conversion2.svg create mode 100644 docs/source/theory/img/convert.svg create mode 100644 docs/source/theory/img/dense.svg create mode 100644 docs/source/theory/img/partition.svg create mode 100644 docs/source/theory/img/svd.svg create mode 100644 docs/source/theory/img/svd2.svg create mode 100644 docs/source/theory/img/svd3.svg diff --git a/docs/source/index.rst b/docs/source/index.rst index 9b040e2..14df94f 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -10,6 +10,7 @@ matrices, with a focus on HODLR-HODLR matrix-matrix multiplication. :caption: Contents: installation + theory api dev diff --git a/docs/source/theory.rst b/docs/source/theory.rst new file mode 100644 index 0000000..f824cc7 --- /dev/null +++ b/docs/source/theory.rst @@ -0,0 +1,12 @@ +Theory +====== + +These pages explain various background aspects of ``hmat_lib``. + + +.. toctree:: + :maxdepth: 1 + :glob: + + theory/* + diff --git a/docs/source/theory/hodlr_theory.rst b/docs/source/theory/hodlr_theory.rst new file mode 100644 index 0000000..96b9d60 --- /dev/null +++ b/docs/source/theory/hodlr_theory.rst @@ -0,0 +1,122 @@ +What is HODLR? +============== + +Hierarchical Off-Diagonal Low-Rank (HODLR) matrix is a matrix representation +in which a matrix is recursively partitioned into quarters, with the +off-diagonal blocks stored as low-rank matrices. + +Off-Diagonal +------------ + +In other words, given a square dense matrix :math:`D`, we construct a HODLR +matrix :math:`H` by first dividing :math:`D` into 4 blocks: + +.. image:: img/convert.svg + +The two blocks on the diagonal are then stored as dense matrices while the two +*off-diagonal* blocks are compressed and stored as *low-rank* matrices: + +.. math:: + + H= + \left[ {\begin{array}{cc} + {}^{0,0}D & {}^{0,1}U {}^{0,1}V^T \\ + {}^{1,0}U {}^{1,0}V^T & {}^{1,1}D \\ + \end{array} } \right] + +where :math:`{}^{i,i}D` is a dense block and :math:`{}^{i,j}U {}^{i,j}V^T` is +a low-rank block. Visually: + +.. image:: img/partition.svg + + +Low-Rank +-------- + +The dense blocks are simple copies of the blocks of the dense matrix +:math:`D`, but the low-rank blocks are obtained by compressing the blocks of +:math:`D` using `Singular Value Decomposition`_ (SVD). Using SVD, any real +matrix can be decomposed into: + +.. math:: + + D = U \Sigma V^T + + +where :math:`U` and :math:`V^T` are square orthogonal matrices and +:math:`\Sigma` is a rectangular diagonal matrix containing the singular +values. The singular values (:math:`\sigma_k = \Sigma_{k,k}`) are real +non-negative numbers and, whem obtained computationally, they usually come +sorted in descending order (:math:`\Sigma_{k,k} < \Sigma_{k+1,k+1}`). +Therefore, when performing an SVD of an off-diagonal (``i!=j``) block: + +.. math:: + + {}^{i,j}D = {}^{i,j}U {}^{i,j}\Sigma {}^{i,j}V^T + +where :math:`{}^{i,j}D` is a dense diagonal block of size ``m×n``, +:math:`{}^{i,j}U` is the ``m×m`` left singular matrix, :math:`{}^{i,j}\Sigma` +is the ``m×n`` matrix of singular values, and :math:`{}^{i,j}V^T` is the +``n×n`` right-singular matrix. Visually: + +.. image:: img/svd.svg + +if :math:`D` is indeed structured correctly and suitable for conversion +to HODLR, it will be the case that the singular values, of this off-diagonal +block will decay rapidly (:math:`{}^{i,j}\Sigma_{r,r} \approx 0` for a +:math:`r << \min(m, n)`). In that case, the decomposition can be written as: + +.. math:: + + {}^{i,j}D = \sum_{k=0}^{k + + + diff --git a/docs/source/theory/img/convert.svg b/docs/source/theory/img/convert.svg new file mode 100644 index 0000000..1844608 --- /dev/null +++ b/docs/source/theory/img/convert.svg @@ -0,0 +1,168 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/source/theory/img/dense.svg b/docs/source/theory/img/dense.svg new file mode 100644 index 0000000..fd40119 --- /dev/null +++ b/docs/source/theory/img/dense.svg @@ -0,0 +1,60 @@ + + + + + + + + 2025-09-05T09:38:06.757510 + image/svg+xml + + + Matplotlib v3.10.0, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/source/theory/img/partition.svg b/docs/source/theory/img/partition.svg new file mode 100644 index 0000000..64e94b4 --- /dev/null +++ b/docs/source/theory/img/partition.svg @@ -0,0 +1,256 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Dense + Dense + Low-Rank + Low-Rank + + diff --git a/docs/source/theory/img/svd.svg b/docs/source/theory/img/svd.svg new file mode 100644 index 0000000..50878c3 --- /dev/null +++ b/docs/source/theory/img/svd.svg @@ -0,0 +1,197 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + = + + + D + U + VT + Σ + + diff --git a/docs/source/theory/img/svd2.svg b/docs/source/theory/img/svd2.svg new file mode 100644 index 0000000..a215712 --- /dev/null +++ b/docs/source/theory/img/svd2.svg @@ -0,0 +1,213 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + = + + + D + U + VT + Σ + + + + + + diff --git a/docs/source/theory/img/svd3.svg b/docs/source/theory/img/svd3.svg new file mode 100644 index 0000000..9160c9c --- /dev/null +++ b/docs/source/theory/img/svd3.svg @@ -0,0 +1,217 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + = + + + D + U + VT + Σ + + + + + + + From 20540eea8f1a521c56bb6639fdfa82f6406663dc Mon Sep 17 00:00:00 2001 From: Rastislav Turanyi Date: Wed, 10 Sep 2025 18:37:22 +0100 Subject: [PATCH 2/7] Add glossary and explanations of HODLR data structure --- docs/source/glossary.rst | 93 +++ docs/source/index.rst | 1 + docs/source/theory/hmat_lib.rst | 95 +++ docs/source/theory/hodlr_theory.rst | 10 + docs/source/theory/img/tree1+hodlr1_1.svg | 299 ++++++++ docs/source/theory/img/tree1+hodlr1_root2.svg | 191 +++++ docs/source/theory/img/tree2+hodlr2_0.svg | 501 +++++++++++++ docs/source/theory/img/tree2+hodlr2_1.svg | 625 ++++++++++++++++ docs/source/theory/img/tree2+hodlr2_2.svg | 684 ++++++++++++++++++ docs/source/theory/library/hodlr.rst | 197 +++++ docs/source/theory/why_hodlr.rst | 4 + 11 files changed, 2700 insertions(+) create mode 100644 docs/source/glossary.rst create mode 100644 docs/source/theory/hmat_lib.rst create mode 100644 docs/source/theory/img/tree1+hodlr1_1.svg create mode 100644 docs/source/theory/img/tree1+hodlr1_root2.svg create mode 100644 docs/source/theory/img/tree2+hodlr2_0.svg create mode 100644 docs/source/theory/img/tree2+hodlr2_1.svg create mode 100644 docs/source/theory/img/tree2+hodlr2_2.svg create mode 100644 docs/source/theory/library/hodlr.rst create mode 100644 docs/source/theory/why_hodlr.rst diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst new file mode 100644 index 0000000..7a3668d --- /dev/null +++ b/docs/source/glossary.rst @@ -0,0 +1,93 @@ +Glossary +======== + +.. glossary:: + + HODLR + Hierarchical Off-Diagonal Low-Rank matrix. See + :doc:`theory/hodlr_theory` for more information. + + low-rank matrix + low-rank format + Matrix representation obtained by approximating a matrix using a + truncated singular value decomposition. For more information, see + :ref:`low-rank-explanation`. + + tree + Tree-shaped structure used to represent the hierarchical nature of a + :term:`HODLR` matrix. Composed of :term:`nodes` connected in a + tree-like shape. For more information, see + :doc:`theory/library/hodlr`. + + node + nodes + Basic unit of a :term:`tree` data structure - multiple nodes connected + together in a tree shape make up a :term:`tree`. Depending on its + type, a node may either hold data or connect to children nodes. + + height + The number of edges on the longest path from the :term:`root node` to + the bottommost :term:`leaf node`. Also, the number of :term:`levels` + composing a :term:`tree`. E.g., a tree with one :term:`root node` + and four :term:`children` will have a height of 1. + + level + levels + The number of edges on the longest path from the :term:`root node` to + a particular :term:`node`. Nodes that are the same number of edges + away from the :term:`root` are on the same level. E.g., the + :term:`root node` is always at ``level==0``, its children are at + ``level==1``, etc. + + root + root node + The topmost :term:`node` of a :term:`tree`, i.e. its beginning. Has no + :term:`parent`. In ``hmat_lib``, a root node is always an + :term:`internal node`. + + leaf + leaves + leaf node + leaf nodes + A terminal :term:`node`, i.e. its end. Has no :term:`children`. + + internal node + internal nodes + A :term:`node` that has one or more :term:`children`. In ``hmat_lib``, + an internal node does not store any data. For more information, see + :ref:`HODLR structure explanation` + + diagonal node + diagonal nodes + diagonal leaf node + diagonal leaf nodes + A :term:`leaf node` which represents a dense block on the diagonal + of the :term:`HODLR` matrix. For more information, see + :ref:`HODLR structure explanation` + + off-diagonal node + off-diagonal nodes + off-diagonal leaf node + off-diagonal leaf nodes + A :term:`leaf node` which represents a low-rank block off the diagonal + of the :term:`HODLR` matrix. For more information, see + :ref:`HODLR structure explanation` + + parent + parent node + A :term:`node` that is the ancestor of another :term:`node` (its + :term:`child`). In ``hmat_lib``, a parent node is always an + :term:`internal node`. + + child + children + child node + child nodes + A :term:`node` that descends from another :term:`node` (its + :term:`parent`) + + subtree + A :term:`tree` formed by a :term:`node` and all its descendants. + + + diff --git a/docs/source/index.rst b/docs/source/index.rst index 14df94f..ba1834b 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -11,6 +11,7 @@ matrices, with a focus on HODLR-HODLR matrix-matrix multiplication. installation theory + glossary api dev diff --git a/docs/source/theory/hmat_lib.rst b/docs/source/theory/hmat_lib.rst new file mode 100644 index 0000000..915392c --- /dev/null +++ b/docs/source/theory/hmat_lib.rst @@ -0,0 +1,95 @@ +hmat_lib Library +================ + +How does the library work? +-------------------------- + +The below pages explain *how* ``hmat_lib`` works, describing the data +structures and how they relate to :term:`HODLR`. For, instead, *why* the +library works the way it works, read on in the +:ref:`next section`. + +.. toctree:: + :maxdepth: 1 + :glob: + + library/* + + +.. _design-decisions: + +Design decisions +---------------- + +``hmat_lib`` was designed with HODLR-HODLR matrix-matrix multiplication in +mind. In the process of its implementation, several design choices have been +made that influenced the process. These and their reasons are detailed here: + +HODLR as a perfectly balanced tree +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The HODLR matrix in ``hmat_lib`` is represented via a perfectly balanced +:term:`tree`. For more details on the data structure and how it relates to +HODLR matrices, see :doc:`library/hodlr` - here we discuss the reasons for +and consequences of this decision. In this case, the reasons for this +decision are quite straightforward: + +1. Simplicity + + * It is simpler to conceptualise and work with a :term:`tree` that is + perfectly balanced (+ fewer things to unit test!) - this way there are no + extraneous cases and it is simpler and more efficient to + :ref:`iterate over`. + +2. Practicality + + * In many :doc:`applications` when a matrix is converted into + the HODLR format, the HODLR ends up fairly balanced - the ranks on all + off-diagonal nodes, both between different levels and within a level, + tend to be similar. A perfectly balanced :term:`tree` represents such + arrangement well. + +The consequence of this decision is that only uniformly deep :term:`HODLR` +matrices can be represented in ``hmat_lib``. Fortunately, this is the default, +and if a more complex arrangement ever becomes of interest, it should be +possible to extend the current framework. + + +.. _hodlr-always-square: + +HODLR matrix is always square +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The :c:struct:`TreeHODLR` data structure has been designed to always represent +a square :term:`HODLR` matrix - not only are there no routines for converting +a rectangular matrix into the :term:`HODLR` format, with the current +``struct``\ s, it is impossible to generate one at all. This is because, +again, there is limited interest in such an arrangement and would require +:ref:`additional work`. + + +.. _diagonal-always-square: + +Diagonal blocks are square matrices +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The :c:struct:`TreeHODLR` data structure has also been designed so that all +the :term:`diagonal leaf nodes` store *squre* blocks. This way, the +:term:`HODLR` always captures the diagonal - which is typically the densest +region of the kind of matrix well represented by a :term:`HODLR` - using dense +data. As a consequence, however, a rectangular :term:`HODLR` may be difficult +to represent, though there are currently +:ref:`no plans to do so`. + + +.. _tree-iteration: + +HODLR tree traversal uses iteration +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +For all operations on :c:struct:`TreeHODLR`, it has been decided to iterate +over the :term:`tree` rather than use recursion. This was mostly done because +of concerns for how well recursion would be handled (i.e. potential to run +into stack overflows etc.), but no rigorous tests on the topic were preformed +and so is more of a preference choice. + diff --git a/docs/source/theory/hodlr_theory.rst b/docs/source/theory/hodlr_theory.rst index 96b9d60..68d1a94 100644 --- a/docs/source/theory/hodlr_theory.rst +++ b/docs/source/theory/hodlr_theory.rst @@ -12,6 +12,10 @@ In other words, given a square dense matrix :math:`D`, we construct a HODLR matrix :math:`H` by first dividing :math:`D` into 4 blocks: .. image:: img/convert.svg + :alt: Visual representation of the conversion of a dense matrix into the + HODLR format. Shows a dense matrix as a filled square being turned + into HODLR matrix, a square with the top left and bottom right + quarters filled. The two blocks on the diagonal are then stored as dense matrices while the two *off-diagonal* blocks are compressed and stored as *low-rank* matrices: @@ -30,6 +34,12 @@ a low-rank block. Visually: .. image:: img/partition.svg +.. _low-rank-explanation: + :alt: Visual representation of a HODLR matrix. Shows a square with the top + left and bottom right quarters filled, both of which are labelled as + dense. + + Low-Rank -------- diff --git a/docs/source/theory/img/tree1+hodlr1_1.svg b/docs/source/theory/img/tree1+hodlr1_1.svg new file mode 100644 index 0000000..28c0239 --- /dev/null +++ b/docs/source/theory/img/tree1+hodlr1_1.svg @@ -0,0 +1,299 @@ + + + +1234 diff --git a/docs/source/theory/img/tree1+hodlr1_root2.svg b/docs/source/theory/img/tree1+hodlr1_root2.svg new file mode 100644 index 0000000..86b90f0 --- /dev/null +++ b/docs/source/theory/img/tree1+hodlr1_root2.svg @@ -0,0 +1,191 @@ + + + + diff --git a/docs/source/theory/img/tree2+hodlr2_0.svg b/docs/source/theory/img/tree2+hodlr2_0.svg new file mode 100644 index 0000000..d73f70a --- /dev/null +++ b/docs/source/theory/img/tree2+hodlr2_0.svg @@ -0,0 +1,501 @@ + + + + diff --git a/docs/source/theory/img/tree2+hodlr2_1.svg b/docs/source/theory/img/tree2+hodlr2_1.svg new file mode 100644 index 0000000..b28215e --- /dev/null +++ b/docs/source/theory/img/tree2+hodlr2_1.svg @@ -0,0 +1,625 @@ + + + +1234 diff --git a/docs/source/theory/img/tree2+hodlr2_2.svg b/docs/source/theory/img/tree2+hodlr2_2.svg new file mode 100644 index 0000000..9a19f61 --- /dev/null +++ b/docs/source/theory/img/tree2+hodlr2_2.svg @@ -0,0 +1,684 @@ + + + +1234ABCD diff --git a/docs/source/theory/library/hodlr.rst b/docs/source/theory/library/hodlr.rst new file mode 100644 index 0000000..ca1208c --- /dev/null +++ b/docs/source/theory/library/hodlr.rst @@ -0,0 +1,197 @@ +HODLR +***** + +In ``hmat_lib``, the :term:`HODLR` matrix is represented using a data +structure akin to a perfectly balanced binary :term:`tree`, all wrapped in a +single ``struct``, :c:struct:`TreeHODLR`, that also stores important metadata. +In practice, when using ``hmat_lib``, only this top-level struct should be +interacted with, but this page explains how the entire structure works. + +TreeHODLR struct +================ + +As mentioned, :c:struct:`TreeHODLR` is a wrapper ``struct`` holding the actual +:term:`tree` structure. It has 4 purposes: + +1. Wrapping the :term:`tree` by: + + a. Holding a pointer to the :term:`root node`, :c:member:`TreeHODLR.root` + b. Holding an array of pointers to the :term:`diagonal leaf nodes`, + :c:member:`innermost_leaves`. + +2. Storing metadata such as the :term:`height` of the tree + (:c:member:`TreeHODLR.height`) + +3. Holding workspace arrays used in most operations on the tree, such as + :c:member:`TreeHODLR.work_queue` + +4. Storing information internal to the library, primarily used for memory + management (e.g. :c:member:`TreeHODLR.memory_leaf_ptr`). + + +Nodes +===== + +Conceptually, there are three types of nodes that make up a :term:`HODLR` +:term:`tree` (unlike a binary tree): + +.. _internal-node-explanation: + +1. :term:`internal node` is a node that represents a recursive :term:`HODLR` + component (:math:`{}^{i,i}H`). It has :term:`children` (always four of + them, again unline a binary tree) and forms the backbone of the + :term:`tree`, connecting all the nodes, but holds no data. + +.. _diagonal-node-explanation: + +2. :term:`diagonal leaf node` is a node that represents a diagonal dense block + of the :term:`HODLR` (:math:`{}^{i,i}D`). It has no :term:`children` + (it is a terminal node) but stores a dense matrix. + +.. _offdiagonal-node-explanation: + +3. :term:`off-diagonal leaf node` is a node that represents an off-diagonal + low-rank block of the :term:`HODLR` (:math:`{}^{i,j}U {}^{i,j}V^T`). It has + no :term:`children` (it is a terminal node) but stores a low-rank matrix. + +In a table format: + +============================== ============================= ======== ======== ======================================================= +Node Block Children Data Struct(s) +============================== ============================= ======== ======== ======================================================= +:term:`internal node` :math:`{}^{i,i}H` 4 none :c:struct:`HODLRInternalNode` +:term:`diagonal leaf node` :math:`{}^{i,i}D` 0 dense :c:struct:`HODLRLeafNode` & :c:struct:`NodeDiagonal` +:term:`off-diagonal leaf node` :math:`{}^{i,j}U {}^{i,j}V^T` 0 low-rank :c:struct:`HODLRLeafNode` & :c:struct:`NodeOffDiagonal` +============================== ============================= ======== ======== ======================================================= + + +HODLR tree +========== + +These :term:`nodes` are assembled in the following way to form the +:term:`tree` data structure: + +* The :term:`root node` is always an :term:`internal node` + (:c:struct:`HODLRInternalNode`) + +* Each :term:`internal node` stores three types of information: + + * The size of the matrix block represented by the internal node, + :c:member:`HODLRInternalNode.m` + + * Pointer to the node's :term:`parent`. This is ``NULL`` for the + :term:`root node`. + + * The four :term:`children` that each internal node has: + + 1. The first child represents the top left diagonal block and is always + either: + + a. A :term:`diagonal leaf node`, storing a dense matrix, if the + :term:`children` are on the highest (last) :term:`level`. + + b. Another :term:`internal node`, storing a :term:`HODLR` + :term:`subtree`, otherwise. + + 2. The second child is always an :term:`off-diagonal leaf node`, storing + the :term:`low-rank matrix` representation of the top right + off-diagonal block. + + 3. The third child is always an :term:`off-diagonal leaf node`, storing + the :term:`low-rank matrix` representation of the bottom left + off-diagonal block. + + 4. The fourth child represents the bottom right diagonal block and is + always either: + + a. A :term:`diagonal leaf node`, storing a dense matrix, if the + :term:`children` are on the highest (last) :term:`level`. + + b. Another :term:`internal node`, storing a :term:`HODLR` + :term:`subtree`, otherwise. + +In short, the :term:`tree` consists of a series of :term:`internal nodes` +representing the recursive structure of the :term:`HODLR` matrix, each of +which also has two :term:`children` :term:`off-diagonal leaf nodes`. The +penultimate level :term:`internal nodes` have two :term:`diagonal leaf node` +:term:`children` instead of the :term:`internal nodes`, terminating the +:term:`tree`. + +Examples +-------- + +Hieght 1 tree +^^^^^^^^^^^^^ + +A :term:`tree` of :term:`height` equal to ``1`` consists of one :term:`root` +:term:`internal node`: + +.. image:: ../img/tree1+hodlr1_root2.svg + +which has four children: + +1. A :term:`diagonal leaf node` storing the top left block in a dense format. +2. An :term:`off-diagonal leaf node` storing the top right block in a low-rank + format. +3. An :term:`off-diagonal leaf node` storing the bottom left block in a + low-rank format. +4. A :term:`diagonal leaf node` storing the bottom right block in a dense + format. + +.. image:: ../img/tree1+hodlr1_1.svg + +In this case, the first and fourth :term:`children` are +:term:`diagonal leaf nodes`, since the first level is the last one. Therefore, +they store the respective blocks of the matrix as dense matrices. + + +Height 2 tree +^^^^^^^^^^^^^ + +A :term:`tree` of :term:`height` equal to ``2`` also starts with one +:term:`root` :term:`internal node`: + +.. image:: ../img/tree2+hodlr2_0.svg + +which has four children: + +1. An :term:`internal node` storing the top left block in a :term:`HODLR` + format. +2. An :term:`off-diagonal leaf node` storing the top right block in a low-rank + format. +3. An :term:`off-diagonal leaf node` storing the bottom left block in a + low-rank format. +4. An :term:`internal node` storing the bottom right block in a :term:`HODLR` + format. + +.. image:: ../img/tree2+hodlr2_1.svg + +In this case, the first and fourth :term:`children` are :term:`internal nodes` +and therefore, instead of storing the respective blocks as dense matrices, +they store them as :term:`HODLR` matrices. As such, each one has four +:term:`children` of its own, for :term:`node` 1 this is: + +1. A :term:`diagonal leaf node` storing the top left block of the top left + block in a dense format. +2. An :term:`off-diagonal leaf node` storing the top right block of the top + left block in a low-rank format. +3. An :term:`off-diagonal leaf node` storing the bottom left block of the top + left block in a low-rank format. +4. A :term:`diagonal leaf node` storing the bottom right block of the top left + block in a dense format. + +and for :term:`node` 2: + +A. A :term:`diagonal leaf node` storing the top left block of the bottom right + block in a dense format. +B. An :term:`off-diagonal leaf node` storing the top right block of the bottom + right block in a low-rank format. +C. An :term:`off-diagonal leaf node` storing the bottom left block of the + bottom right block in a low-rank format +D. A :term:`diagonal leaf node` storing the bottom right block of the bottom + right block in a dense format. + +.. image:: ../img/tree2+hodlr2_2.svg + + + diff --git a/docs/source/theory/why_hodlr.rst b/docs/source/theory/why_hodlr.rst new file mode 100644 index 0000000..f000357 --- /dev/null +++ b/docs/source/theory/why_hodlr.rst @@ -0,0 +1,4 @@ +Why Use HODLR Matrix? +===================== + + From 92b6794c55ba67afff3fb3447cc3c4348955c52a Mon Sep 17 00:00:00 2001 From: Rastislav Turanyi Date: Fri, 12 Sep 2025 09:57:41 +0100 Subject: [PATCH 3/7] Add alt text to images --- docs/source/theory/hodlr_theory.rst | 17 ++++++++++++++++- docs/source/theory/library/hodlr.rst | 15 +++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/docs/source/theory/hodlr_theory.rst b/docs/source/theory/hodlr_theory.rst index 68d1a94..9a82e38 100644 --- a/docs/source/theory/hodlr_theory.rst +++ b/docs/source/theory/hodlr_theory.rst @@ -37,7 +37,8 @@ a low-rank block. Visually: .. _low-rank-explanation: :alt: Visual representation of a HODLR matrix. Shows a square with the top left and bottom right quarters filled, both of which are labelled as - dense. + dense. The top right and bottom left quarters are labelled as + off-diagonal. Low-Rank @@ -70,6 +71,8 @@ is the ``m×n`` matrix of singular values, and :math:`{}^{i,j}V^T` is the ``n×n`` right-singular matrix. Visually: .. image:: img/svd.svg + :alt: Diagram illustrating SVD using a filled square for D, U, and V + transpose matrices and an empty square with a diagonal for sigma. if :math:`D` is indeed structured correctly and suitable for conversion to HODLR, it will be the case that the singular values, of this off-diagonal @@ -87,6 +90,11 @@ representing a column of the :math:`{}^{i,j}U` and :math:`{}^{i,j}V^T` matrix: .. image:: img/svd2.svg + :alt: Diagram illustrating truncated SVD using the previous diagram, but + with the diagonal of the sigma square going only halfway from the + top right corner. A vertical line halfway through U and a horizontal + line halfway through V transpose also indicate that only half the + matrices will be kept. Furthermore, it is expected that, due to this decay, the singular values quickly become small enough in comparison to the first @@ -94,6 +102,10 @@ singular value that they can be considered insignificant ( :math:`{}^{i,j}\Sigma_{r',r'} << {}^{i,j}\Sigma_{0,0}` for :math:`r' < r`): .. image:: img/svd3.svg + :alt: Diagram illustrating an even more truncated SVD. Uses the previous + diagram, but the diagonal of sigma only extends a tenth of the way, + after which it appears as a very faint line. The lines through the + U and V transpose are now placed at a tenth of the matrices. Then, we can discard all the insignificant singular values and truncate the :math:`{}^{i,j}U` and :math:`{}^{i,j}V^T` matrices, keeping only a small @@ -116,6 +128,8 @@ Hierarchical Lastly, the above procedure can be repeated for the two diagonal blocks: .. image:: img/conversion2.svg + :alt: Diagram showing the conversion from a height 1 HODLR to a height 2 + HODLR. splitting each block into quarters and compressing the two off-diagonal sub-blocks. This can be repeated any number of times, yielding a HODLR matrix: @@ -130,3 +144,4 @@ sub-blocks. This can be repeated any number of times, yielding a HODLR matrix: .. _Singular Value Decomposition: https://en.wikipedia.org/wiki/Singular_value_decomposition + diff --git a/docs/source/theory/library/hodlr.rst b/docs/source/theory/library/hodlr.rst index ca1208c..bd9eaa7 100644 --- a/docs/source/theory/library/hodlr.rst +++ b/docs/source/theory/library/hodlr.rst @@ -127,6 +127,9 @@ A :term:`tree` of :term:`height` equal to ``1`` consists of one :term:`root` :term:`internal node`: .. image:: ../img/tree1+hodlr1_root2.svg + :alt: Diagram showing a height 1 HODLR, above which is a tree diagram + showing one parent node above four children nodes. The parent node + and the outline of the HODLR are highlighted. which has four children: @@ -139,6 +142,10 @@ which has four children: format. .. image:: ../img/tree1+hodlr1_1.svg + :alt: Previous diagram, but with the first child node highlighted with the + top left block, the second child node highlighted with the top right + block, the third with the bottom left block, and the fourth with the + bottom right block. In this case, the first and fourth :term:`children` are :term:`diagonal leaf nodes`, since the first level is the last one. Therefore, @@ -152,6 +159,10 @@ A :term:`tree` of :term:`height` equal to ``2`` also starts with one :term:`root` :term:`internal node`: .. image:: ../img/tree2+hodlr2_0.svg + :alt: Diagram showing a height 2 HODLR, above which is a height 2 tree + diagram, in which the root node has four children, the first and + fourth of which have four children of their own. The root node and + the HODLR outline are highlighted. which has four children: @@ -165,6 +176,8 @@ which has four children: format. .. image:: ../img/tree2+hodlr2_1.svg + :alt: Previous diagram but with the children of the root node and the + corresponding HODLR blocks highlighted. In this case, the first and fourth :term:`children` are :term:`internal nodes` and therefore, instead of storing the respective blocks as dense matrices, @@ -192,6 +205,8 @@ D. A :term:`diagonal leaf node` storing the bottom right block of the bottom right block in a dense format. .. image:: ../img/tree2+hodlr2_2.svg + :alt: Previous diagram but with the last-level nodes and the corresponding + HODLR blocks highlighted. From e589c00c36545eecce3e8dcb1b71cda9cd3f3890 Mon Sep 17 00:00:00 2001 From: Rastislav Turanyi Date: Sun, 14 Sep 2025 10:16:53 +0100 Subject: [PATCH 4/7] Add looping documentation --- docs/source/theory/library/looping.rst | 180 +++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 docs/source/theory/library/looping.rst diff --git a/docs/source/theory/library/looping.rst b/docs/source/theory/library/looping.rst new file mode 100644 index 0000000..62079fb --- /dev/null +++ b/docs/source/theory/library/looping.rst @@ -0,0 +1,180 @@ +Iterating Over HODLR Tree +========================= + +A :term:`HODLR` :term:`tree` can be iterated through in two directions: + +.. contents:: + :backlinks: entry + :depth: 2 + :local: + + +Top-down interation +------------------- + +The first direction is to start at the :term:`root node` (``level==0``) and +then iterate through the :term:`tree` until the end (``level==height-1``). +However, in practice, this can be implemented in two ways: + +Using multiple arrays +^^^^^^^^^^^^^^^^^^^^^ + +.. note:: + + This approach has been phased out in ``hmat_lib``. + +The conceptually simpler approach is to use one array for storing pointers +to :term:`internal nodes` on one level, and a second array for storing +pointers for the next level. These can then be swapped between levels: + +.. code:: C + + int iter1(struct TreeHODLR *hodlr, struct HODLRInternalNode **q2) { + struct HODLRInternalNode **q1 = hodlr->work_queue, **temp_ptr = NULL; + q1[0] = hodlr->root; + int len_queue = 1; + + for (int level = 0; level < hodlr->height; level++) { + for (int parent = 0; parent < len_queue; parent++) { + // Populate new-level array + q2[2 * parent] = q1[parent]->children[0].internal; + q2[2 * parent + 1] = q1[parent]->children[3].internal; + } + len_queue *= 2; + + // Swap pointers + temp_ptr = q1; + q1 = q2; + q2 = temp_ptr; + } + } + +In this approach, the first N elements in the ``q1`` array always hold the N +nodes from the lower :term:`level` and the first 2*N elements in the ``q2`` +array always hold the 2*N nodes from the higher level. After a level is +iterated over, the ``q2`` pointer is swapped over to ``q1`` to be used as a +new source, and the original ``q1`` array is reused in ``q2`` as the new +destination and will be overwritten. At each iteration, either the ``q1`` or +``q2`` nodes can be used for computations etc. At the end, the ``q1`` array +stores the highest-level :term:`internal nodes`, which can be utilised in an +additional loop after the above one, if neccessary. + +**Pros** + +* Simple to understand +* Consecutive entries in arrays being used + +**Cons** + +* Requires two arrays (twice the memory, though storage is not a significant + concern) + + +Using one array +^^^^^^^^^^^^^^^ + +.. note:: + + This is the preferred top-down iteration approach in ``hmat_lib``, though + bottom-up looping is preferred when possible. + +The more complex approach is to use only one array and use clever indexing +to avoid premature overwriting: + +.. code:: C + + int iter2(struct TreeHODLR *hodlr) { + struct HODLRInternalNode **queue = hodlr->work_queue; + queue[0] = hodlr->root; + + int len_queue = 1; + int q_next_node_density = hodlr->len_work_queue; + int q_current_node_density = q_next_node_density; + + for (int level = 0; level < hodlr->height; level++) { + // The next level has twice as many nodes + q_next_node_density /= 2; + + for (int parent = 0; parent < len_queue; parent++) { + const int idx = parent * q_current_node_density; + + // Populate new-level array + // Place child 4 halfway between occupied indices + queue[(2 * parent + 1) * q_next_node_density] = + queue[idx]->children[3].internal; + + // Replace parent with child 1 + queue[idx] = queue[parent]->children[0].internal; + } + len_queue *= 2; + q_current_node_density = q_next_node_density; + } + } + +In this approach, the :term:`nodes` at each :term:`level` are placed +strategically at indices of ``queue`` such that there is always enough space +between two occupied indices to fit all the descendants of that node. As an +example. the :term:`root node` is placed at index ``0``, its first child then +replaces it at index ``0`` while its fourth child is placed halfway through +``queue``. The first child of the first child then replaces the first child +at index ``0`` while its fourth child is placed halfway between index ``0`` +and the halfway point of ``queue``, etc. + +At each iteration, the nodes from ``queue`` can be used either before or after +being updated. At the end, the ``queue`` array stores the highest-level +:term:`internal nodes`, which can be utilised in an additional loop after the +above one, if neccessary. + +**Pros** + +* Only requires one array + +**Cons** + +* More difficult to understand +* Non-consecutive elements of the array are being used (until the last + iteration) + + +Bottom-up iteration +------------------- + +The other direction utilises the :c:member:`TreeHODLR.innermost_leaves` array +(``level==height``) to access the highest-level :term:`internal nodes` +(``level==height-1``) and then iterates up the :term:`tree` until the +:term:`root node` is reached: + +.. code:: C + + int iter3(struct TreeHODLR *hodlr) { + struct HODLRInternalNode **queue = hodlr->work_queue; + int n_parent_nodes = hodlr->len_queue; + + for (int parent = 0; parent < n_parent_nodes; parent++) { + queue[parent] = hodlr->innermost_leaves[2 * parent]->parent; + } + + for (int level = hodlr->height - 1; level > 0; level--) { + n_parent_nodes /= 2; + + for (int parent = 0; parent < n_parent_nodes; parent++) { + queue[parent] = queue[2 * parent]->parent; + } + } + } + +In this approach, the work array, ``queue``, is first fully populated with +with the highest-level :term:`internal nodes`, after which as we iterate over +the :term:`tree`, the nodes on each level take up half as many indices as the +previous, until the :term:`root node` takes up only the first index. + +**Pros** + +* Only requires one array +* Simple to understand +* Consecutive elements of the array are used + +**Cons** + +* Requires an additional loop at the beginning, if ``queue`` is not already + populated From 939d039009e764955e877c5479ac8b4ef3dcc7a1 Mon Sep 17 00:00:00 2001 From: Rastislav Turanyi Date: Tue, 23 Sep 2025 16:16:07 +0100 Subject: [PATCH 5/7] Fix typos --- docs/source/theory/hmat_lib.rst | 2 +- docs/source/theory/hodlr_theory.rst | 2 +- docs/source/theory/library/hodlr.rst | 4 ++-- docs/source/theory/library/looping.rst | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/source/theory/hmat_lib.rst b/docs/source/theory/hmat_lib.rst index 915392c..ab588d5 100644 --- a/docs/source/theory/hmat_lib.rst +++ b/docs/source/theory/hmat_lib.rst @@ -74,7 +74,7 @@ Diagonal blocks are square matrices ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The :c:struct:`TreeHODLR` data structure has also been designed so that all -the :term:`diagonal leaf nodes` store *squre* blocks. This way, the +the :term:`diagonal leaf nodes` store *square* blocks. This way, the :term:`HODLR` always captures the diagonal - which is typically the densest region of the kind of matrix well represented by a :term:`HODLR` - using dense data. As a consequence, however, a rectangular :term:`HODLR` may be difficult diff --git a/docs/source/theory/hodlr_theory.rst b/docs/source/theory/hodlr_theory.rst index 9a82e38..97cfb46 100644 --- a/docs/source/theory/hodlr_theory.rst +++ b/docs/source/theory/hodlr_theory.rst @@ -57,7 +57,7 @@ matrix can be decomposed into: where :math:`U` and :math:`V^T` are square orthogonal matrices and :math:`\Sigma` is a rectangular diagonal matrix containing the singular values. The singular values (:math:`\sigma_k = \Sigma_{k,k}`) are real -non-negative numbers and, whem obtained computationally, they usually come +non-negative numbers and, when obtained computationally, they usually come sorted in descending order (:math:`\Sigma_{k,k} < \Sigma_{k+1,k+1}`). Therefore, when performing an SVD of an off-diagonal (``i!=j``) block: diff --git a/docs/source/theory/library/hodlr.rst b/docs/source/theory/library/hodlr.rst index bd9eaa7..5e6e118 100644 --- a/docs/source/theory/library/hodlr.rst +++ b/docs/source/theory/library/hodlr.rst @@ -39,7 +39,7 @@ Conceptually, there are three types of nodes that make up a :term:`HODLR` 1. :term:`internal node` is a node that represents a recursive :term:`HODLR` component (:math:`{}^{i,i}H`). It has :term:`children` (always four of - them, again unline a binary tree) and forms the backbone of the + them, again unlike a binary tree) and forms the backbone of the :term:`tree`, connecting all the nodes, but holds no data. .. _diagonal-node-explanation: @@ -120,7 +120,7 @@ penultimate level :term:`internal nodes` have two :term:`diagonal leaf node` Examples -------- -Hieght 1 tree +Height 1 tree ^^^^^^^^^^^^^ A :term:`tree` of :term:`height` equal to ``1`` consists of one :term:`root` diff --git a/docs/source/theory/library/looping.rst b/docs/source/theory/library/looping.rst index 62079fb..6bb632a 100644 --- a/docs/source/theory/library/looping.rst +++ b/docs/source/theory/library/looping.rst @@ -9,7 +9,7 @@ A :term:`HODLR` :term:`tree` can be iterated through in two directions: :local: -Top-down interation +Top-down iteration ------------------- The first direction is to start at the :term:`root node` (``level==0``) and @@ -57,7 +57,7 @@ new source, and the original ``q1`` array is reused in ``q2`` as the new destination and will be overwritten. At each iteration, either the ``q1`` or ``q2`` nodes can be used for computations etc. At the end, the ``q1`` array stores the highest-level :term:`internal nodes`, which can be utilised in an -additional loop after the above one, if neccessary. +additional loop after the above one, if necessary. **Pros** @@ -123,7 +123,7 @@ and the halfway point of ``queue``, etc. At each iteration, the nodes from ``queue`` can be used either before or after being updated. At the end, the ``queue`` array stores the highest-level :term:`internal nodes`, which can be utilised in an additional loop after the -above one, if neccessary. +above one, if necessary. **Pros** From 6d62162b063ed2fb2c6b54648cf464fbf7110903 Mon Sep 17 00:00:00 2001 From: Rastislav Turanyi Date: Wed, 24 Sep 2025 11:38:43 +0100 Subject: [PATCH 6/7] Improve explanation of the off-diagonal nodes --- docs/source/theory/hodlr_theory.rst | 44 +++- docs/source/theory/img/hodlr2_diag.svg | 196 +++++++++++++++ docs/source/theory/img/hodlr2_internal.svg | 193 ++++++++++++++ docs/source/theory/img/hodlr2_offdiag.svg | 277 +++++++++++++++++++++ docs/source/theory/img/svd3.svg | 29 ++- docs/source/theory/img/svd4.svg | 170 +++++++++++++ docs/source/theory/library/hodlr.rst | 195 ++++++++++++--- 7 files changed, 1068 insertions(+), 36 deletions(-) create mode 100644 docs/source/theory/img/hodlr2_diag.svg create mode 100644 docs/source/theory/img/hodlr2_internal.svg create mode 100644 docs/source/theory/img/hodlr2_offdiag.svg create mode 100644 docs/source/theory/img/svd4.svg diff --git a/docs/source/theory/hodlr_theory.rst b/docs/source/theory/hodlr_theory.rst index 97cfb46..f4af159 100644 --- a/docs/source/theory/hodlr_theory.rst +++ b/docs/source/theory/hodlr_theory.rst @@ -74,7 +74,11 @@ is the ``m×n`` matrix of singular values, and :math:`{}^{i,j}V^T` is the :alt: Diagram illustrating SVD using a filled square for D, U, and V transpose matrices and an empty square with a diagonal for sigma. -if :math:`D` is indeed structured correctly and suitable for conversion + +Truncating zeroes +^^^^^^^^^^^^^^^^^ + +If :math:`D` is indeed structured correctly and suitable for conversion to HODLR, it will be the case that the singular values, of this off-diagonal block will decay rapidly (:math:`{}^{i,j}\Sigma_{r,r} \approx 0` for a :math:`r << \min(m, n)`). In that case, the decomposition can be written as: @@ -96,6 +100,10 @@ representing a column of the :math:`{}^{i,j}U` and line halfway through V transpose also indicate that only half the matrices will be kept. + +Truncating insignificant singular values +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + Furthermore, it is expected that, due to this decay, the singular values quickly become small enough in comparison to the first singular value that they can be considered insignificant ( @@ -120,7 +128,39 @@ or .. math:: - {}^{i,j}D = \sum_{k=0}^{k`. + Hierarchical ------------ diff --git a/docs/source/theory/img/hodlr2_diag.svg b/docs/source/theory/img/hodlr2_diag.svg new file mode 100644 index 0000000..2bce200 --- /dev/null +++ b/docs/source/theory/img/hodlr2_diag.svg @@ -0,0 +1,196 @@ + + + + diff --git a/docs/source/theory/img/hodlr2_internal.svg b/docs/source/theory/img/hodlr2_internal.svg new file mode 100644 index 0000000..8f35c4b --- /dev/null +++ b/docs/source/theory/img/hodlr2_internal.svg @@ -0,0 +1,193 @@ + + + + diff --git a/docs/source/theory/img/hodlr2_offdiag.svg b/docs/source/theory/img/hodlr2_offdiag.svg new file mode 100644 index 0000000..98408c1 --- /dev/null +++ b/docs/source/theory/img/hodlr2_offdiag.svg @@ -0,0 +1,277 @@ + + + + diff --git a/docs/source/theory/img/svd3.svg b/docs/source/theory/img/svd3.svg index 9160c9c..16c28b3 100644 --- a/docs/source/theory/img/svd3.svg +++ b/docs/source/theory/img/svd3.svg @@ -7,9 +7,32 @@ viewBox="0 0 232.00002 67.000008" version="1.1" id="svg1" + sodipodi:docname="svd3.svg" + inkscape:version="1.4 (86a8ad7, 2024-10-11)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> + = + sodipodi:role="line" + id="tspan1">≈ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + D + U' + VT + + diff --git a/docs/source/theory/library/hodlr.rst b/docs/source/theory/library/hodlr.rst index 5e6e118..899893b 100644 --- a/docs/source/theory/library/hodlr.rst +++ b/docs/source/theory/library/hodlr.rst @@ -65,50 +65,184 @@ Node Block Children Data S ============================== ============================= ======== ======== ======================================================= -HODLR tree -========== +Internal node +------------- -These :term:`nodes` are assembled in the following way to form the -:term:`tree` data structure: +An :term:`internal node` represents a :term:`HODLR` (sub)matrix, i.e. if a +HODLR matrix is defined as: + +.. math:: + + H= + \left[ {\begin{array}{cc} + {}^{0,0}H & {}^{0,1}U {}^{0,1}V^T \\ + {}^{1,0}U {}^{1,0}V^T & {}^{1,1}H \\ + \end{array} } \right] + +then :math:`{}^{0,0}H` and :math:`{}^{1,1}H` would be represented using +internal nodes: + +.. image:: ../img/hodlr2_internal.svg + :alt: Diagram of a height 2 HODLR matrix with the entire HODLR matrix and + the two HODLR submatrices highlighted. + +In ``hmat_lib``, it is represented via the :c:struct:`HODLRInternalNode` +``struct`` which does not store any data, only pointers to its four children: + +1. Top-left diagonal block +2. Top-right off-diagonal block +3. Bottom-left off-diagonal block +4. Bottom-right diagonal block + +.. _internal-node-children-middle: + +In the above case, where the internal node is in the middle of the tree +(``level < height=1``), this would be: + +1. Another internal node storing a HODLR subtree (:math:`{}^{0,0}H`) +2. An off-diagonal leaf node (:math:`{}^{0,1}U {}^{0,1}V^T`) +3. An off-diagonal leaf node (:math:`{}^{1,0}U {}^{1,0}V^T`) +4. Another internal node storing a HODLR subtree (:math:`{}^{1,1}H`) + +However, the last internal nodes (second-to-last level, which is the last +level with internal nodes, ``level == height - 1``), which take the form: + +.. math:: + + H= + \left[ {\begin{array}{cc} + {}^{0,0}D & {}^{0,1}U {}^{0,1}V^T \\ + {}^{1,0}U {}^{1,0}V^T & {}^{1,1}D \\ + \end{array} } \right] + +.. _internal-node-children-bottom: + +the four children would instead be: + +1. A diagonal leaf node (:math:`{}^{0,0}D`) +2. An off-diagonal leaf node (:math:`{}^{0,1}U {}^{0,1}V^T`) +3. An off-diagonal leaf node (:math:`{}^{1,0}U {}^{1,0}V^T`) +4. A diagonal leaf node (:math:`{}^{1,1}D`) + +Information stored on the node +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* Pointer to its parent (``NULL``) if the node is the :term:`root node`. +* Size of the HODLR submatrix +* Pointers to its four children (as specified above). + + +Diagonal leaf node +------------------ + +A :term:`diagonal leaf node` represents a dense block, i.e. if a :term:`HODLR` +(sub)matrix is defined as: -* The :term:`root node` is always an :term:`internal node` - (:c:struct:`HODLRInternalNode`) +.. math:: -* Each :term:`internal node` stores three types of information: + {}^{i,i}H= + \left[ {\begin{array}{cc} + {}^{0,0}D & {}^{0,1}U {}^{0,1}V^T \\ + {}^{1,0}U {}^{1,0}V^T & {}^{1,1}D \\ + \end{array} } \right] - * The size of the matrix block represented by the internal node, - :c:member:`HODLRInternalNode.m` +then :math:`{}^{0,0}D` and :math:`{}^{1,1}D` would be represented using +diagonal leaf nodes: - * Pointer to the node's :term:`parent`. This is ``NULL`` for the - :term:`root node`. +.. image:: ../img/hodlr2_diag.svg + :alt: Diagram of a height 2 HODLR matrix with all the diagonal dense blocks + highlighted. - * The four :term:`children` that each internal node has: +In ``hmat_lib``, it is represented via the :c:struct:`HODLRLeafNode` struct +with its :c:member:`HODLRLeafNode.data` field being the +:c:struct:`NodeDiagonal`. It stores the diagonal block of the HODLR matrix as +a single contiguous, column-major array. - 1. The first child represents the top left diagonal block and is always - either: - a. A :term:`diagonal leaf node`, storing a dense matrix, if the - :term:`children` are on the highest (last) :term:`level`. +Information stored on the node +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - b. Another :term:`internal node`, storing a :term:`HODLR` - :term:`subtree`, otherwise. +* Pointer to its parent (always an internal node). +* Size of the block it stores. +* The dense matrix. - 2. The second child is always an :term:`off-diagonal leaf node`, storing - the :term:`low-rank matrix` representation of the top right - off-diagonal block. - 3. The third child is always an :term:`off-diagonal leaf node`, storing - the :term:`low-rank matrix` representation of the bottom left - off-diagonal block. +Off-diagonal leaf node +---------------------- - 4. The fourth child represents the bottom right diagonal block and is - always either: +An :term:`off-diagonal leaf node` represents a low-rank off-diagonal +block, i.e. if a :term:`HODLR` (sub)matrix is defined as: - a. A :term:`diagonal leaf node`, storing a dense matrix, if the - :term:`children` are on the highest (last) :term:`level`. +.. math:: - b. Another :term:`internal node`, storing a :term:`HODLR` - :term:`subtree`, otherwise. + {}^{i,i}H= + \left[ {\begin{array}{cc} + {}^{0,0}H & {}^{0,1}U {}^{0,1}V^T \\ + {}^{1,0}U {}^{1,0}V^T & {}^{1,1}H \\ + \end{array} } \right] + +then :math:`{}^{0,1}U {}^{0,1}V^T` and :math:`{}^{0,1}U {}^{0,1}V^T` would be +represented using off-diagonal leaf nodes: + +.. image:: ../img/hodlr2_offdiag.svg + :alt: Diagram of a height 2 HODLR matrix with all the off-diagona blocks + highlighted. + +In ``hmat_lib``, it is represented via the :c:struct:`HODLRLeafNode` struct +with its :c:member:`HODLRLeafNode.data` field being the +:c:struct:`NodeOffDiagonal` struct. It stores the off-diagonal block in the +:term:`low-rank format` by storing the :math:`U` and :math:`V^T` matrices: + +* The :math:`U` matrix is stored + :ref:`scaled by the singular values` (i.e. :math:`U' = U \Sigma`) + as a single contiguous, column-major array. + + * This saves memory since the :math:`\Sigma` array does not need to be + stored and also avoids recomputing the scaling every time the node is used + for computation. + +* The :math:`V^T` matrix is stored transposed (i.e. :math:`V`) as a single + contiguous, column-major array. + + * There is not a strong reason for this approach - either way works fine + since in some operations :math:`V` is used and in some :math:`V^T` is. + +Information stored on the node +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* Pointer to its parent (always an internal node). +* The dimensions of the matrix it stores. +* The rank of the matrix. +* The low-rank matrix. + + +HODLR tree +========== + +The above :term:`nodes` are assembled in the following way to form the +:term:`tree` data structure: + +1. The tree starts with a :term:`root node`, which is always an + :term:`internal node`. + +2. The :term:`internal node` has four children: + + a. If the current level is not the + :ref:`second-to-last one` + (``level < height-1``): + + * Nodes 1 and 4 are internal nodes (go back to 2.). + * Nodes 2 and 3 are off-diagonal leaf nodes, formed by compressing the + off-diagonal blocks. + + b. If the current level *is* the + :ref:`second-to-last one` + (``level == height-1``): + + * Nodes 1 and 4 are diagonal leaf nodes, formed by copying the dense + blocks. + * Nodes 2 and 3 are off-diagonal leaf nodes, formed by compressing the + off-diagonal blocks. In short, the :term:`tree` consists of a series of :term:`internal nodes` representing the recursive structure of the :term:`HODLR` matrix, each of @@ -117,6 +251,7 @@ penultimate level :term:`internal nodes` have two :term:`diagonal leaf node` :term:`children` instead of the :term:`internal nodes`, terminating the :term:`tree`. + Examples -------- From da163508a31c8336f073f78119b57382cefe1209 Mon Sep 17 00:00:00 2001 From: Rastislav Turanyi Date: Wed, 24 Sep 2025 16:20:05 +0100 Subject: [PATCH 7/7] Improve nodes table --- docs/source/theory/library/hodlr.rst | 70 +++++++++++++++++++++------- 1 file changed, 52 insertions(+), 18 deletions(-) diff --git a/docs/source/theory/library/hodlr.rst b/docs/source/theory/library/hodlr.rst index 899893b..eb8a087 100644 --- a/docs/source/theory/library/hodlr.rst +++ b/docs/source/theory/library/hodlr.rst @@ -37,33 +37,63 @@ Conceptually, there are three types of nodes that make up a :term:`HODLR` .. _internal-node-explanation: -1. :term:`internal node` is a node that represents a recursive :term:`HODLR` - component (:math:`{}^{i,i}H`). It has :term:`children` (always four of - them, again unlike a binary tree) and forms the backbone of the - :term:`tree`, connecting all the nodes, but holds no data. +1. :ref:`internal node` is a node that represents + a recursive :term:`HODLR` component (:math:`{}^{i,i}H`). It has + :term:`children` (always four of them, again unlike a binary tree) and + forms the backbone of the :term:`tree`, connecting all the nodes, but holds + no data. .. _diagonal-node-explanation: -2. :term:`diagonal leaf node` is a node that represents a diagonal dense block - of the :term:`HODLR` (:math:`{}^{i,i}D`). It has no :term:`children` - (it is a terminal node) but stores a dense matrix. +2. :ref:`diagonal leaf node` is a node that + represents a diagonal dense block of the :term:`HODLR` (:math:`{}^{i,i}D`). + It has no :term:`children` (it is a terminal node) but stores a dense + matrix. .. _offdiagonal-node-explanation: -3. :term:`off-diagonal leaf node` is a node that represents an off-diagonal - low-rank block of the :term:`HODLR` (:math:`{}^{i,j}U {}^{i,j}V^T`). It has - no :term:`children` (it is a terminal node) but stores a low-rank matrix. +3. :ref:`off-diagonal leaf node` is a node that + represents an off-diagonal low-rank block of the :term:`HODLR` + (:math:`{}^{i,j}U {}^{i,j}V^T`). It has no :term:`children` (it is a + terminal node) but stores a low-rank matrix. In a table format: -============================== ============================= ======== ======== ======================================================= -Node Block Children Data Struct(s) -============================== ============================= ======== ======== ======================================================= -:term:`internal node` :math:`{}^{i,i}H` 4 none :c:struct:`HODLRInternalNode` -:term:`diagonal leaf node` :math:`{}^{i,i}D` 0 dense :c:struct:`HODLRLeafNode` & :c:struct:`NodeDiagonal` -:term:`off-diagonal leaf node` :math:`{}^{i,j}U {}^{i,j}V^T` 0 low-rank :c:struct:`HODLRLeafNode` & :c:struct:`NodeOffDiagonal` -============================== ============================= ======== ======== ======================================================= - +.. list-table:: + :header-rows: 1 + :stub-columns: 1 + + * - Node + - :term:`internal node` + - :term:`diagonal leaf node` + - :term:`off-diagonal leaf node` + * - Block + - :math:`{}^{i,i}H` + - :math:`{}^{i,i}D` + - :math:`{}^{i,j}U {}^{i,j}V^T` + * - Children + - 4 + - 0 + - 0 + * - Is terminal + - No + - Yes + - Yes + * - Data + - none + - dense + - low-rank + * - Possible positions in tree + - ``level < height`` + - ``level <= height`` + - ``level == height`` + * - Struct(s) + - :c:struct:`HODLRInternalNode` + - :c:struct:`HODLRLeafNode` & :c:struct:`NodeDiagonal` + - :c:struct:`HODLRLeafNode` & :c:struct:`NodeOffDiagonal` + + +.. _internal-node-explanation2: Internal node ------------- @@ -132,6 +162,8 @@ Information stored on the node * Pointers to its four children (as specified above). +.. _diagonal-node-explanation2: + Diagonal leaf node ------------------ @@ -167,6 +199,8 @@ Information stored on the node * The dense matrix. +.. _offdiagonal-node-explanation2: + Off-diagonal leaf node ----------------------