Repository: PTB-MR/mrpro
Pull request #771: Allow matrix of operators in CG and add preconditioning
Author: login: fzimmermann89
Created: 2025-04-07T10:54:30Z | Merged: 2025-04-17T19:50:01Z

As discussed with @ckolbPTB and @koflera , this breaks backward compatibility (@Stef-Martin and maybe @guastinimara):
CG now always returns a tuple of tensors (same as operators and the other optimizers)

if no x0 is given, it now starts with a zero tensor

- Enables use of LinearOperatorMatrix in cg.
- Adds preconditioning to cg
- Adds cuda test.

closes #544 

Linked issue #544: preconditioning for CG
our CG could support preconditioning by another linear operator, see for the algorithm:

https://en.wikipedia.org/wiki/Conjugate_gradient_method#The_preconditioned_conjugate_gradient_method

and use case in MR:
https://github.com/mikgroup/kspace_precond/blob/master/Sense%20Reconstruction.ipynb

Diff:
diff --git a/examples/notebooks/iterative_sense_reconstruction_radial2D.ipynb b/examples/notebooks/iterative_sense_reconstruction_radial2D.ipynb
index 02c9f1781..7278a8edb 100644
--- a/examples/notebooks/iterative_sense_reconstruction_radial2D.ipynb
+++ b/examples/notebooks/iterative_sense_reconstruction_radial2D.ipynb
@@ -362,7 +362,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "img_manual = mrpro.algorithms.optimizers.cg(\n",
+    "(img_manual,) = mrpro.algorithms.optimizers.cg(\n",
     "    operator, right_hand_side, initial_value=right_hand_side, max_iterations=4, tolerance=0.0\n",
     ")"
    ]
diff --git a/examples/notebooks/iterative_sense_reconstruction_with_regularization.ipynb b/examples/notebooks/iterative_sense_reconstruction_with_regularization.ipynb
index af84172af..10ea9386f 100644
--- a/examples/notebooks/iterative_sense_reconstruction_with_regularization.ipynb
+++ b/examples/notebooks/iterative_sense_reconstruction_with_regularization.ipynb
@@ -369,7 +369,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "img_manual = mrpro.algorithms.optimizers.cg(\n",
+    "(img_manual,) = mrpro.algorithms.optimizers.cg(\n",
     "    operator, right_hand_side, initial_value=right_hand_side, max_iterations=8, tolerance=0.0\n",
     ")"
    ]
diff --git a/examples/scripts/iterative_sense_reconstruction_radial2D.py b/examples/scripts/iterative_sense_reconstruction_radial2D.py
index 0cf658e7d..c5deffe43 100644
--- a/examples/scripts/iterative_sense_reconstruction_radial2D.py
+++ b/examples/scripts/iterative_sense_reconstruction_radial2D.py
@@ -170,7 +170,7 @@
 # to stop the iterations when the residual is below a certain threshold.
 
 # %%
-img_manual = mrpro.algorithms.optimizers.cg(
+(img_manual,) = mrpro.algorithms.optimizers.cg(
     operator, right_hand_side, initial_value=right_hand_side, max_iterations=4, tolerance=0.0
 )
 
diff --git a/examples/scripts/iterative_sense_reconstruction_with_regularization.py b/examples/scripts/iterative_sense_reconstruction_with_regularization.py
index 0491c940b..3a3b8b2c3 100644
--- a/examples/scripts/iterative_sense_reconstruction_with_regularization.py
+++ b/examples/scripts/iterative_sense_reconstruction_with_regularization.py
@@ -196,7 +196,7 @@ def show_images(*images: torch.Tensor, titles: list[str] | None = None) -> None:
 # the residual is small enough.
 
 # %%
-img_manual = mrpro.algorithms.optimizers.cg(
+(img_manual,) = mrpro.algorithms.optimizers.cg(
     operator, right_hand_side, initial_value=right_hand_side, max_iterations=8, tolerance=0.0
 )
 
diff --git a/src/mrpro/algorithms/optimizers/cg.py b/src/mrpro/algorithms/optimizers/cg.py
index 19ead3b30..e9ca5840b 100644
--- a/src/mrpro/algorithms/optimizers/cg.py
+++ b/src/mrpro/algorithms/optimizers/cg.py
@@ -1,34 +1,80 @@
-"""Conjugate Gradient for linear systems with self-adjoint linear operator."""
+"""Conjugate Gradient for linear systems with self-adjoint positive semidefinite linear operator."""
 
-from collections.abc import Callable
+from collections.abc import Callable, Sequence
 
 import torch
+from typing_extensions import Unpack, overload
 
 from mrpro.algorithms.optimizers.OptimizerStatus import OptimizerStatus
 from mrpro.operators.LinearOperator import LinearOperator
+from mrpro.operators.LinearOperatorMatrix import LinearOperatorMatrix
+
+
+def vdot(a: Sequence[torch.Tensor], b: Sequence[torch.Tensor]) -> torch.Tensor:
+    """Vector dot product."""
+    return sum(
+        (torch.vdot(a_i.flatten(), b_i.flatten()) for a_i, b_i in zip(a, b, strict=True)), start=torch.tensor(0.0)
+    )
 
 
 class CGStatus(OptimizerStatus):
     """Status of the conjugate gradient algorithm."""
 
-    residual: torch.Tensor
+    residual: tuple[torch.Tensor, ...]
     """Residual of the current estimate."""
 
+    preconditioned_residual: tuple[torch.Tensor, ...]
+    """Preconditioned residual of the current estimate."""
+
+
+OperatorMatrixLikeCallable = Callable[[Unpack[tuple[torch.Tensor, ...]]], tuple[torch.Tensor, ...]]
+OperatorLikeCallable = Callable[[torch.Tensor], tuple[torch.Tensor]]
 
+
+@overload
 def cg(
-    operator: LinearOperator,
-    right_hand_side: torch.Tensor,
-    initial_value: torch.Tensor | None = None,
+    operator: OperatorLikeCallable,
+    right_hand_side: tuple[torch.Tensor] | torch.Tensor,
     *,
+    initial_value: tuple[torch.Tensor] | torch.Tensor | None = None,
+    preconditioner_inverse: OperatorLikeCallable | None = None,
     max_iterations: int = 128,
     tolerance: float = 1e-4,
     callback: Callable[[CGStatus], bool | None] | None = None,
-) -> torch.Tensor:
-    r"""CG for solving a linear system :math:`Hx=b`.
+) -> tuple[torch.Tensor]: ...
 
-    This algorithm solves systems of the form :math:`H x = b`, where :math:`H` is a self-adjoint linear operator
-    and :math:`b` is the right-hand side. The method can solve a batch of :math:`N` systems jointly, thereby taking
-    :math:`H` as a block-diagonal with blocks :math:`H_i` and :math:`b = [b_1, ..., b_N] ^T`.
+
+@overload
+def cg(
+    operator: OperatorMatrixLikeCallable,
+    right_hand_side: Sequence[torch.Tensor],
+    *,
+    initial_value: Sequence[torch.Tensor] | None = None,
+    preconditioner_inverse: OperatorMatrixLikeCallable | None = None,
+    max_iterations: int = 128,
+    tolerance: float = 1e-4,
+    callback: Callable[[CGStatus], bool | None] | None = None,
+) -> tuple[torch.Tensor, ...]: ...
+
+
+def cg(
+    operator: LinearOperator | LinearOperatorMatrix | OperatorMatrixLikeCallable | OperatorLikeCallable,
+    right_hand_side: Sequence[torch.Tensor] | torch.Tensor,
+    *,
+    initial_value: Sequence[torch.Tensor] | torch.Tensor | None = None,
+    preconditioner_inverse: LinearOperator
+    | LinearOperatorMatrix
+    | OperatorMatrixLikeCallable
+    | OperatorLikeCallable
+    | None = None,
+    max_iterations: int = 128,
+    tolerance: float = 1e-4,
+    callback: Callable[[CGStatus], bool | None] | None = None,
+) -> tuple[torch.Tensor, ...] | tuple[torch.Tensor]:
+    r"""(Preconditioned) Conjugate Gradient for solving :math:`Hx=b`.
+
+    This algorithm solves systems of the form :math:`H x = b`, where :math:`H` is a self-adjoint positive semidefinite
+    linear operator and :math:`b` is the right-hand side.
 
      The method performs the following steps:
 
@@ -42,27 +88,35 @@ def cg(
         - Update the search direction: :math:`\beta_k = \frac{r_{k+1}^T r_{k+1}}{r_k^T r_k}`,
             then :math:`p_{k+1} = r_{k+1} + \beta_k p_k`.
 
-    This implementation assumes that :math:`H` is self-adjoint and does not verify this condition.
+    The operator can be either a `LinearOperator` or a `LinearOperatorMatrix`.
+    In both cases, this implementation does not verify if the operator is self-adjoint and positive semidefinite.
+    It will silently return the wrong result if the assumptions are not met.
+    It can solve a batch of :math:`N` systems jointly if `right_hand_side` has a batch dimension
+    and the operator interprets the batch dimension as :math:`H` being block-diagonal with blocks :math:`H_i`
+    resulting in :math:`b = [b_1, ..., b_N] ^T`.
+
+    If `preconditioner_inverse` is provided, it solves :math:`M^{-1}Hx = M^{-1}b`
+    implicitly, where `preconditioner_inverse(r)` computes :math:`M^{-1}r`.
 
     See [Hestenes1952]_, [Nocedal2006]_, and [WikipediaCG]_ for more information.
 
+
     Parameters
     ----------
     operator
-        self-adjoint operator (named :math:`H` above)
+        Self-adjoint operator :math:`H`
     right_hand_side
-        right-hand-side of the system (named :math:`b` above)
+        Right-hand-side :math:`b`.
     initial_value
-        initial value of the algorithm; if `None`, it will be set to `right_hand_side`
+        Initial guess :math:`x_0`. If `None`, the initial guess is set to the zero vector.
+    preconditioner_inverse
+        Preconditioner :math:`M^{-1}`. If None, no preconditioning is applied.
     max_iterations
-        maximal number of iterations. Can be used for early stopping.
+        Maximum number of iterations.
     tolerance
-        tolerance for the residual; if set to zero, the maximal number of iterations
-        is the only stopping criterion used to stop the cg.
-        If the condition number of :math:`H` is large, a small residual may not imply a highly accurate solution.
+        Tolerance for the L2 norm of the residual :math:`\|r_k\|_2`.
     callback
-        Function to be called at each iteration. This can be used to monitor the progress of the algorithm.
-        If it returns `False`, the algorithm stops at that iteration.
+        Function called at each iteration. See `CGStatus`.
 
     Returns
     -------
@@ -75,54 +129,55 @@ def cg(
     .. [Nocedal2006] Nocedal, J. (2006). *Numerical Optimization* (2nd ed.). Springer.
     .. [WikipediaCG] Wikipedia: Conjugate Gradient https://en.wikipedia.org/wiki/Conjugate_gradient
     """
-    if initial_value is not None and (initial_value.shape != right_hand_side.shape):
-        raise ValueError(
-            f'Shapes of starting_value and right_hand_side must match,got {initial_value.shape, right_hand_side.shape}'
-        )
-
-    # initial residual
-    residual = right_hand_side - operator(initial_value)[0] if initial_value is not None else right_hand_side.clone()
-
-    # initialize conjugate vector
-    conjugate_vector = residual.clone()
-
-    # assign starting value to the solution
-    solution = initial_value.clone() if initial_value is not None else right_hand_side.clone()
-
-    # for the case where the residual is exactly zero
-    if torch.vdot(residual.flatten(), residual.flatten()) == 0:
-        return solution
+    right_hand_side_ = (right_hand_side,) if isinstance(right_hand_side, torch.Tensor) else tuple(right_hand_side)
+    if initial_value is None:  # start with zero initial value
+        solution = tuple(torch.zeros_like(r) for r in right_hand_side_)
+        residual = right_hand_side_
+    else:  # start with provided initial value
+        solution = (initial_value,) if isinstance(initial_value, torch.Tensor) else tuple(initial_value)
+        if len(solution) != len(right_hand_side_):
+            raise ValueError('Length mismatch in initial_value and right_hand_side')
+        if any(s.shape != r.shape for s, r in zip(solution, right_hand_side_, strict=True)):
+            raise ValueError('Shape mismatch in initial_value and right_hand_side')
+        residual = tuple(rhs - op_sol for rhs, op_sol in zip(right_hand_side_, operator(*solution), strict=True))
+
+    if preconditioner_inverse is not None:
+        conjugate = preconditioner_inverse(*residual)
+    else:
+        conjugate = residual
 
     # dummy value. new value will be set in loop before first usage
-    residual_norm_squared_previous: torch.Tensor | None = None
+    direction_dot_residual_old: torch.Tensor | None = None
 
     for iteration in range(max_iterations):
-        # calculate the square norm of the residual
-        residual_flat = residual.flatten()
-        residual_norm_squared = torch.vdot(residual_flat, residual_flat).real
-
-        # check if the solution is already accurate enough
-        if tolerance != 0 and (residual_norm_squared < tolerance**2):
-            return solution
-
-        if residual_norm_squared_previous is not None:  # not first iteration
-            beta = residual_norm_squared / residual_norm_squared_previous
-            conjugate_vector = residual + beta * conjugate_vector
-
-        # update estimates of the solution and the residual
-        (operator_conjugate_vector,) = operator(conjugate_vector)
-        alpha = residual_norm_squared / (torch.vdot(conjugate_vector.flatten(), operator_conjugate_vector.flatten()))
-        solution = solution + alpha * conjugate_vector
-        residual = residual - alpha * operator_conjugate_vector
-
-        residual_norm_squared_previous = residual_norm_squared
+        residual_dot_residual = vdot(residual, residual).real
+
+        if residual_dot_residual < tolerance**2:  # are we done?
+            break
+
+        if preconditioner_inverse is not None:
+            direction = preconditioner_inverse(*residual)
+            direction_dot_residual = vdot(residual, direction).real
+        else:
+            direction = residual
+            direction_dot_residual = residual_dot_residual
+
+        if direction_dot_residual_old is not None:  # not first iteration
+            beta = direction_dot_residual / direction_dot_residual_old
+            conjugate = tuple(d + beta * con for d, con in zip(direction, conjugate, strict=True))
+        operator_conjugate = operator(*conjugate)
+        alpha = direction_dot_residual / vdot(conjugate, operator_conjugate).real
+        solution = tuple(sol + alpha * con for sol, con in zip(solution, conjugate, strict=True))
+        residual = tuple(res - alpha * op_con for res, op_con in zip(residual, operator_conjugate, strict=True))
+        direction_dot_residual_old = direction_dot_residual
 
         if callback is not None:
             continue_iterations = callback(
                 {
-                    'solution': (solution,),
+                    'solution': solution,
                     'iteration_number': iteration,
                     'residual': residual,
+                    'preconditioned_residual': direction,
                 }
             )
             if continue_iterations is False:
diff --git a/src/mrpro/algorithms/optimizers/pdhg.py b/src/mrpro/algorithms/optimizers/pdhg.py
index 638344426..6efb89973 100644
--- a/src/mrpro/algorithms/optimizers/pdhg.py
+++ b/src/mrpro/algorithms/optimizers/pdhg.py
@@ -168,10 +168,10 @@ def pdhg(
         operator_norm = operator_matrix.operator_norm(*[torch.randn_like(v) for v in initial_values]).amax()
         if primal_stepsize is None and dual_stepsize is None:
             primal_stepsize_ = dual_stepsize_ = 1.0 / operator_norm
-        elif primal_stepsize is None:
+        elif primal_stepsize is None and dual_stepsize is not None:
             primal_stepsize_ = 1 / (operator_norm * dual_stepsize)
             dual_stepsize_ = dual_stepsize
-        elif dual_stepsize is None:
+        elif dual_stepsize is None and primal_stepsize is not None:
             dual_stepsize_ = 1 / (operator_norm * primal_stepsize)
             primal_stepsize_ = primal_stepsize
     else:
diff --git a/src/mrpro/algorithms/reconstruction/RegularizedIterativeSENSEReconstruction.py b/src/mrpro/algorithms/reconstruction/RegularizedIterativeSENSEReconstruction.py
index 6cc8b3fb4..5af74d9de 100644
--- a/src/mrpro/algorithms/reconstruction/RegularizedIterativeSENSEReconstruction.py
+++ b/src/mrpro/algorithms/reconstruction/RegularizedIterativeSENSEReconstruction.py
@@ -129,7 +129,7 @@ def forward(self, kdata: KData) -> IData:
             operator = operator + self.regularization_weight * self.regularization_op.H @ self.regularization_op
             right_hand_side += self.regularization_weight * self.regularization_op.H(self.regularization_data)[0]
 
-        img_tensor = cg(
+        (img_tensor,) = cg(
             operator,
             right_hand_side,
             initial_value=right_hand_side,
diff --git a/src/mrpro/operators/LinearOperatorMatrix.py b/src/mrpro/operators/LinearOperatorMatrix.py
index f11496c3c..375753177 100644
--- a/src/mrpro/operators/LinearOperatorMatrix.py
+++ b/src/mrpro/operators/LinearOperatorMatrix.py
@@ -240,7 +240,7 @@ def adjoint(self, *x: torch.Tensor) -> tuple[torch.Tensor, ...]:
         return self.H(*x)
 
     @classmethod
-    def from_diagonal(cls, *operators: LinearOperator):
+    def from_diagonal(cls, *operators: LinearOperator) -> Self:
         """Create a diagonal `LinearOperatorMatrix`.
 
         Construct a square `LinearOperatorMatrix` with the given linear operators on the diagonal,
diff --git a/tests/algorithms/test_cg.py b/tests/algorithms/test_cg.py
index 3d8e3f652..48954e53a 100644
--- a/tests/algorithms/test_cg.py
+++ b/tests/algorithms/test_cg.py
@@ -1,41 +1,35 @@
 """Tests for the conjugate gradient method."""
 
 import pytest
-import scipy
+import scipy.linalg
+import scipy.sparse
 import torch
 from mrpro.algorithms.optimizers import cg
 from mrpro.algorithms.optimizers.cg import CGStatus
-from mrpro.operators import EinsumOp
+from mrpro.operators import EinsumOp, LinearOperatorMatrix
 from mrpro.utils import RandomGenerator
-from scipy.sparse.linalg import cg as cg_scp
 
 
 @pytest.fixture(
-    params=[
-        (1, 32, True),  # (batch-size, vector-size, flag for complex-valued system or not)
-        (1, 32, False),
-        (4, 32, True),
-        (4, 32, False),
+    params=[  # (batch-size, vector-size, complex-valued system, separate initial_value)
+        ((), 32, False, False),
+        ((4,), 32, True, True),
+    ],
+    ids=[
+        'real_single_noinit',
+        'complex_batch',
     ],
-    ids=['complex_single', 'real_single', 'complex_batch', 'real_batch'],
 )
 def system(request):
-    """Generate data for creating a system Hx=b with linear and self-adjoint
-    H."""
-    rng = RandomGenerator(seed=0)
-
-    # get parameters for the test
-    batchsize, vectorsize, complex_valued = request.param
-
-    # if batchsize=1, it corresponds to one linear system; for batchsize>1, multiple systems
-    # are considered simultaneously
-    matrix_shape: tuple[int, int, int] = (batchsize, vectorsize, vectorsize)
-    vector_shape: tuple[int, int] = (batchsize, vectorsize)
-
+    """Generate system Hx=b with linear and self-adjoint H."""
+    rng = RandomGenerator(seed=123)
+    batchsize, vectorsize, complex_valued, separate_initial_value = request.param
+    matrix_shape: tuple[int, int, int] = (*batchsize, vectorsize, vectorsize)
+    vector_shape: tuple[int, int] = (*batchsize, vectorsize)
     if complex_valued:
         matrix = rng.complex64_tensor(size=matrix_shape, high=1.0)
     else:
-        matrix = rng.float32_tensor(size=matrix_shape, low=-1.0, high=1.0)
+        matrix = rng.float64_tensor(size=matrix_shape, low=-1.0, high=1.0)
 
     # make sure H is self-adjoint
     self_adjoint_matrix = matrix.mH @ matrix
@@ -47,23 +41,68 @@ def system(request):
     if complex_valued:
         vector = rng.complex64_tensor(size=vector_shape, high=1.0)
     else:
-        vector = rng.float32_tensor(size=vector_shape, low=-1.0, high=1.0)
+        vector = rng.float64_tensor(size=vector_shape, low=-1.0, high=1.0)
 
     (right_hand_side,) = operator(vector)
+    if separate_initial_value:
+        initial_value = rng.rand_like(vector)
+    else:
+        initial_value = None
+    return operator, right_hand_side, vector, initial_value
 
-    return operator, right_hand_side, vector
 
+@pytest.fixture(
+    params=[  # (batch-size, vector-size, complex-valued system, separate initial_value)
+        (2, 10, (False, True, False), True),
+        (3, 10, (False,), False),
+    ],
+    ids=['3x3-operator-matrix', '1x1-operator-matrix'],
+)
+def matrixsystem(
+    request,
+) -> tuple[LinearOperatorMatrix, tuple[torch.Tensor, ...], tuple[torch.Tensor, ...], tuple[torch.Tensor, ...] | None]:
+    """system Hx=b with linear and self-adjoint H as LinearOperatorMatrix."""
+    rng = RandomGenerator(seed=456)
+    batchsize, vectorsize, complex_valued_system, separate_initial_value = request.param
+    matrix_shape: tuple[int, int, int] = (batchsize, vectorsize, vectorsize)
+    vector_shape: tuple[int, int] = (batchsize, vectorsize)
+    operators = []
+    vectors = []
+    for complex_operator in complex_valued_system:
+        if complex_operator:
+            matrix = rng.complex64_tensor(size=matrix_shape, high=1.0)
+            vector = rng.complex64_tensor(size=vector_shape, high=1.0)
+        else:
+            matrix = rng.float32_tensor(size=matrix_shape, low=-1.0, high=1.0)
+            vector = rng.float32_tensor(size=vector_shape, low=-1.0, high=1.0)
+        vectors.append(vector)
+        operators.append(EinsumOp(matrix.mH @ matrix))  # make sure H is self-adjoint
+    operator_matrix = LinearOperatorMatrix.from_diagonal(*operators)
+    right_hand_side = operator_matrix(*vectors)
+    if separate_initial_value:
+        initial_value = tuple(rng.rand_like(vector) for vector in vectors)
+    else:
+        initial_value = None
+    return (operator_matrix, right_hand_side, tuple(vectors), initial_value)
 
-def test_cg_convergence(system) -> None:
-    """Test if CG delivers accurate solution."""
 
-    # create operator, right-hand side and ground-truth data
-    operator, right_hand_side, solution = system
+def test_cg_solution(system) -> None:
+    """Test if CG delivers accurate solution."""
+    operator, right_hand_side, solution, initial_value = system
+    (cg_solution,) = cg(operator, right_hand_side, initial_value=initial_value, max_iterations=256)
+    torch.testing.assert_close(cg_solution, solution, rtol=5e-3, atol=5e-3)
 
-    initial_value = torch.ones_like(solution)
-    cg_solution = cg(operator, right_hand_side, initial_value=initial_value, max_iterations=256)
 
-    # test if solution is accurate
+def test_cg_solution_operatormatrix(matrixsystem) -> None:
+    """Test if CG delivers accurate solution for a LinearOperatorMatrix."""
+    operator, right_hand_side, solution, initial_value = matrixsystem
+    cg_solution = cg(
+        operator,
+        right_hand_side,
+        initial_value=initial_value,
+        max_iterations=1000,
+        tolerance=1e-6,
+    )
     torch.testing.assert_close(cg_solution, solution, rtol=5e-3, atol=5e-3)
 
 
@@ -71,85 +110,90 @@ def test_cg_stopping_after_one_iteration(system) -> None:
     """Test if cg stops after one iteration if the ground-truth is the initial
     guess."""
     # create operator, right-hand side and ground-truth data
-    operator, right_hand_side, solution = system
+    operator, right_hand_side, solution, _ = system
 
     # callback function; should not be called since cg should exit for loop
     def callback(solution):
         pytest.fail('CG did not exit before performing any iterations')
 
     # the test should fail if we reach the callback
-    xcg_one_iteration = cg(
+    (cg_solution,) = cg(
         operator, right_hand_side, initial_value=solution, max_iterations=10, tolerance=1e-4, callback=callback
     )
-    assert (xcg_one_iteration == solution).all()
+    assert (cg_solution == solution).all()
 
 
-def test_compare_cg_to_scipy(system) -> None:
+@pytest.mark.parametrize('max_iterations', [1, 2, 3, 5])
+@pytest.mark.parametrize('use_preconditioner', [True, False], ids=['with preconditioner', 'without preconditioner'])
+def test_compare_cg_to_scipy(system, max_iterations: int, use_preconditioner: bool) -> None:
     """Test if our implementation is close to the one of scipy."""
-    # create operator, right-hand side and ground-truth data
-    operator, right_hand_side, _ = system
-
-    # generate initial value
-    initial_value = torch.zeros_like(right_hand_side)
-
-    # if batchsize>1, construct H = diag(H1,...,H_batchsize)
-    # and b=[b1,...,b_batchsize]^T, otherwise just take the matrix
-    matrix_np = scipy.linalg.block_diag(*operator.matrix.numpy())
-
-    # choose zero tolerance to avoid exiting the for loop in the cg
-    tolerance = 0.0
-
-    # test for different maximal number of iterations
-    for max_iterations in [1, 2, 4, 8]:
-        # run our cg and scipy's cg and compare the results
-        (xcg_scp, _) = cg_scp(
-            matrix_np,
-            right_hand_side.flatten().numpy(),
-            x0=initial_value.flatten().numpy(),
-            maxiter=max_iterations,
-            atol=tolerance,
-        )
-        cg_solution_scipy = xcg_scp.reshape(right_hand_side.shape)
-        cg_solution_torch = cg(
-            operator,
-            right_hand_side,
-            initial_value=initial_value,
-            max_iterations=max_iterations,
-            tolerance=tolerance,
+    operator, right_hand_side, solution, initial_value = system
+
+    if operator.matrix.ndim == 2:
+        operator_sp = operator.matrix.numpy()
+    else:
+        operator_sp = scipy.linalg.block_diag(*operator.matrix.numpy())
+    if use_preconditioner:
+        ilu = scipy.sparse.linalg.spilu(scipy.sparse.csc_matrix(operator_sp), drop_tol=0.05)
+        preconditioner_sp = scipy.sparse.linalg.LinearOperator(
+            operator_sp.shape, lambda x: ilu.solve(x), dtype=operator_sp.dtype
         )
-        torch.testing.assert_close(cg_solution_torch, torch.tensor(cg_solution_scipy), atol=1e-5, rtol=1e-5)
+        preconditioner = lambda x: (torch.as_tensor(ilu.solve(x.flatten().numpy())).reshape(x.shape),)  #  # noqa: E731
+    else:
+        preconditioner_sp = preconditioner = None
+
+    (scipy_solution, _) = scipy.sparse.linalg.cg(
+        operator_sp,
+        right_hand_side.flatten().numpy(),
+        x0=None if initial_value is None else initial_value.flatten().numpy(),
+        maxiter=max_iterations,
+        atol=0,
+        M=preconditioner_sp,
+    )
+    cg_solution_scipy = scipy_solution.reshape(right_hand_side.shape)
+    (cg_solution,) = cg(
+        operator,
+        right_hand_side,
+        initial_value=initial_value,
+        max_iterations=max_iterations,
+        tolerance=0,
+        preconditioner_inverse=preconditioner,
+    )
+
+    tol = 1e-6 if cg_solution.dtype.to_real() == torch.float64 else 5e-3
+
+    torch.testing.assert_close(cg_solution, torch.tensor(cg_solution_scipy), atol=tol, rtol=tol)
 
 
 def test_invalid_shapes(system) -> None:
     """Test if CG throws error in case of shape-mismatch."""
     # create operator, right-hand side and ground-truth data
-    h_operator, right_hand_side, _ = system
+    operator, right_hand_side, *_ = system
 
-    # generate invalid initial point due to shape mismatch of
-    # right_hand_side and input
-    initial_value = torch.zeros(
-        h_operator.matrix.shape[-1] + 1,
+    # invalid initial value with mismatched shape
+    bad_initial_value = torch.zeros(
+        operator.matrix.shape[-1] + 1,
     )
     with pytest.raises(ValueError, match='match'):
-        cg(h_operator, right_hand_side, initial_value=initial_value, max_iterations=10)
+        cg(operator, right_hand_side, initial_value=bad_initial_value, max_iterations=10)
 
 
 def test_callback(system) -> None:
     """Test if the callback function is called if a callback function is set."""
     # create operator, right-hand side
-    h_operator, right_hand_side, _ = system
+    operator, right_hand_side, _, initial_value = system
 
     # callback function; if the function is called during the iterations, the
     # test is successful
     def callback(cg_status: CGStatus) -> None:
-        _, _, _ = cg_status['iteration_number'], cg_status['solution'][0], cg_status['residual'].norm()
+        _, _, _ = cg_status['iteration_number'], cg_status['solution'][0], cg_status['residual'][0].norm()
         assert True
 
-    cg(h_operator, right_hand_side, callback=callback)
+    cg(operator, right_hand_side, callback=callback)
 
 
 def test_callback_early_stop(system) -> None:
-    h_operator, right_hand_side, _ = system
+    operator, right_hand_side, _, initial_value = system
     """Check that when the callback function returns False the optimizer is stopped."""
     callback_check = 0
 
@@ -159,15 +203,28 @@ def callback(solution):
         callback_check += 1
         return False
 
-    cg(h_operator, right_hand_side, max_iterations=100, callback=callback)
+    cg(operator, right_hand_side, initial_value=initial_value, max_iterations=100, callback=callback)
     assert callback_check == 1
 
 
-def test_autograd(system) -> None:
+def test_cg_autograd(system) -> None:
     """Test autograd through cg"""
-    h_operator, right_hand_side, _ = system
+    operator, right_hand_side, _, initial_value = system
     right_hand_side.requires_grad_(True)
     with torch.autograd.detect_anomaly():
-        result = cg(h_operator, right_hand_side, tolerance=0, max_iterations=5)
+        (result,) = cg(operator, right_hand_side, initial_value=initial_value, tolerance=0, max_iterations=5)
         result.abs().sum().backward()
     assert right_hand_side.grad is not None
+
+
+@pytest.mark.cuda
+def test_cg_cuda(matrixsystem) -> None:
+    """Test if CG works on CUDA."""
+    operator, right_hand_side, solution, initial_value = matrixsystem
+    right_hand_side = tuple(x.to('cuda') for x in right_hand_side)
+    operator = operator.to('cuda')
+    initial_value = tuple(x.to('cuda') for x in initial_value) if initial_value is not None else None
+    solution = tuple(x.to('cuda') for x in solution)
+    (result,) = cg(operator, right_hand_side, initial_value=initial_value, tolerance=1e-6, max_iterations=1000)
+    assert all(x.is_cuda for x in result)
+    torch.testing.assert_close(result, solution, rtol=5e-3, atol=5e-3)


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.


<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**tests/algorithms/test_cg.py:80**
* Overriding the fixture-provided initial_value may lead to inconsistent test behavior, especially when separate initial_value is supplied. Consider using the fixture's initial_value directly or document why it is being replaced.
```
initial_value = torch.ones_like(solution)
```
**src/mrpro/algorithms/optimizers/cg.py:135**
* [nitpick] The error message for mismatched lengths of initial_value and right_hand_side could be more explicit. Consider clearly formatting the expected and provided lengths to improve clarity.
```
raise ValueError(
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-04-07T11:33:17Z
user: login: ckolbPTB
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T13:33:06Z
user: login: fzimmermann89
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T15:01:05Z
user: login: fzimmermann89
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T15:01:29Z
user: login: ckolbPTB
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-14T15:07:06Z
user: login: fzimmermann89
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T15:55:53Z
user: login: fzimmermann89
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T18:24:01Z
user: login: koflera
body: looks good to me 
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-16T13:57:32Z
user: login: fzimmermann89
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T08:36:40Z
user: login: koflera
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-17T09:29:42Z
user: login: koflera
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-17T09:33:35Z
user: login: fzimmermann89
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T11:51:01Z

Review comments:
diff_hunk: @@ -43,25 +91,28 @@ def cg(
 
     This implementation assumes that :math:`H` is self-adjoint and does not verify this condition.
 
+    If `preconditioner_inverse` is provided, it solves :math:`M^{-1}Hx = M^{-1}b`
+    implicitly, where `preconditioner_inverse(r)` computes :math:`M^{-1}r`.
+
     See [Hestenes1952]_, [Nocedal2006]_, and [WikipediaCG]_ for more information.
 
+
     Parameters
     ----------
     operator
-        self-adjoint operator (named :math:`H` above)
+        Self-adjoint operator :math:`H`.
     right_hand_side
-        right-hand-side of the system (named :math:`b` above)
+        Right-hand-side :math:`b`.
     initial_value
-        initial value of the algorithm; if `None`, it will be set to `right_hand_side`
+        Initial guess :math:`x_0`.
path: src/mrpro/algorithms/optimizers/cg.py
user: login: ckolbPTB
body: I would leave the comment about what None means.
created_at: 2025-04-14T13:18:46Z
author_association: COLLABORATOR
diff_hunk: @@ -74,54 +125,57 @@ def cg(
     .. [Nocedal2006] Nocedal, J. (2006). *Numerical Optimization* (2nd ed.). Springer.
     .. [WikipediaCG] Wikipedia: Conjugate Gradient https://en.wikipedia.org/wiki/Conjugate_gradient
     """
-    if initial_value is not None and (initial_value.shape != right_hand_side.shape):
-        raise ValueError(
-            f'Shapes of starting_value and right_hand_side must match,got {initial_value.shape, right_hand_side.shape}'
-        )
-
-    # initial residual
-    residual = right_hand_side - operator(initial_value)[0] if initial_value is not None else right_hand_side.clone()
-
-    # initialize conjugate vector
-    conjugate_vector = residual.clone()
-
-    # assign starting value to the solution
-    solution = initial_value.clone() if initial_value is not None else right_hand_side.clone()
-
-    # for the case where the residual is exactly zero
-    if torch.vdot(residual.flatten(), residual.flatten()) == 0:
-        return solution
+    right_hand_side_ = (right_hand_side,) if isinstance(right_hand_side, torch.Tensor) else tuple(right_hand_side)
+    if initial_value is None:
+        solution = right_hand_side_
+    elif isinstance(initial_value, torch.Tensor):
+        solution = (initial_value,)
+    elif len(initial_value) != len(right_hand_side_):
+        raise ValueError('Length mismatch in initial_value and right_hand_side')
path: src/mrpro/algorithms/optimizers/cg.py
user: login: ckolbPTB
body: This check should also be run if initial_value is a Tensor, because right_hand_side could be a Sequence of tensors
created_at: 2025-04-14T13:21:54Z
author_association: COLLABORATOR
diff_hunk: @@ -74,54 +125,57 @@ def cg(
     .. [Nocedal2006] Nocedal, J. (2006). *Numerical Optimization* (2nd ed.). Springer.
     .. [WikipediaCG] Wikipedia: Conjugate Gradient https://en.wikipedia.org/wiki/Conjugate_gradient
     """
-    if initial_value is not None and (initial_value.shape != right_hand_side.shape):
-        raise ValueError(
-            f'Shapes of starting_value and right_hand_side must match,got {initial_value.shape, right_hand_side.shape}'
-        )
-
-    # initial residual
-    residual = right_hand_side - operator(initial_value)[0] if initial_value is not None else right_hand_side.clone()
-
-    # initialize conjugate vector
-    conjugate_vector = residual.clone()
-
-    # assign starting value to the solution
-    solution = initial_value.clone() if initial_value is not None else right_hand_side.clone()
-
-    # for the case where the residual is exactly zero
-    if torch.vdot(residual.flatten(), residual.flatten()) == 0:
-        return solution
+    right_hand_side_ = (right_hand_side,) if isinstance(right_hand_side, torch.Tensor) else tuple(right_hand_side)
+    if initial_value is None:
+        solution = right_hand_side_
+    elif isinstance(initial_value, torch.Tensor):
+        solution = (initial_value,)
+    elif len(initial_value) != len(right_hand_side_):
+        raise ValueError('Length mismatch in initial_value and right_hand_side')
+    else:
+        solution = tuple(initial_value)
+    if any(s.shape != r.shape for s, r in zip(solution, right_hand_side_, strict=True)):
+        raise ValueError('Shape mismatch in initial_value and right_hand_side')
+
+    residual = tuple(rhs - op_sol for rhs, op_sol in zip(right_hand_side_, operator(*solution), strict=True))
+
+    if preconditioner_inverse is not None:
+        conjugate = preconditioner_inverse(*residual)
+    else:
+        conjugate = residual
 
     # dummy value. new value will be set in loop before first usage
-    residual_norm_squared_previous: torch.Tensor | None = None
+    direction_dot_residual_prev: torch.Tensor | None = None
path: src/mrpro/algorithms/optimizers/cg.py
user: login: ckolbPTB
body: previous?
created_at: 2025-04-14T13:23:06Z
author_association: COLLABORATOR
diff_hunk: @@ -74,54 +125,57 @@ def cg(
     .. [Nocedal2006] Nocedal, J. (2006). *Numerical Optimization* (2nd ed.). Springer.
     .. [WikipediaCG] Wikipedia: Conjugate Gradient https://en.wikipedia.org/wiki/Conjugate_gradient
     """
-    if initial_value is not None and (initial_value.shape != right_hand_side.shape):
-        raise ValueError(
-            f'Shapes of starting_value and right_hand_side must match,got {initial_value.shape, right_hand_side.shape}'
-        )
-
-    # initial residual
-    residual = right_hand_side - operator(initial_value)[0] if initial_value is not None else right_hand_side.clone()
-
-    # initialize conjugate vector
-    conjugate_vector = residual.clone()
-
-    # assign starting value to the solution
-    solution = initial_value.clone() if initial_value is not None else right_hand_side.clone()
-
-    # for the case where the residual is exactly zero
-    if torch.vdot(residual.flatten(), residual.flatten()) == 0:
-        return solution
+    right_hand_side_ = (right_hand_side,) if isinstance(right_hand_side, torch.Tensor) else tuple(right_hand_side)
+    if initial_value is None:
+        solution = right_hand_side_
+    elif isinstance(initial_value, torch.Tensor):
+        solution = (initial_value,)
+    elif len(initial_value) != len(right_hand_side_):
+        raise ValueError('Length mismatch in initial_value and right_hand_side')
+    else:
+        solution = tuple(initial_value)
+    if any(s.shape != r.shape for s, r in zip(solution, right_hand_side_, strict=True)):
+        raise ValueError('Shape mismatch in initial_value and right_hand_side')
+
+    residual = tuple(rhs - op_sol for rhs, op_sol in zip(right_hand_side_, operator(*solution), strict=True))
+
+    if preconditioner_inverse is not None:
+        conjugate = preconditioner_inverse(*residual)
+    else:
+        conjugate = residual
 
     # dummy value. new value will be set in loop before first usage
-    residual_norm_squared_previous: torch.Tensor | None = None
+    direction_dot_residual_prev: torch.Tensor | None = None
 
     for iteration in range(max_iterations):
-        # calculate the square norm of the residual
-        residual_flat = residual.flatten()
-        residual_norm_squared = torch.vdot(residual_flat, residual_flat).real
-
-        # check if the solution is already accurate enough
-        if tolerance != 0 and (residual_norm_squared < tolerance**2):
-            return solution
-
-        if residual_norm_squared_previous is not None:  # not first iteration
-            beta = residual_norm_squared / residual_norm_squared_previous
-            conjugate_vector = residual + beta * conjugate_vector
-
-        # update estimates of the solution and the residual
-        (operator_conjugate_vector,) = operator(conjugate_vector)
-        alpha = residual_norm_squared / (torch.vdot(conjugate_vector.flatten(), operator_conjugate_vector.flatten()))
-        solution = solution + alpha * conjugate_vector
-        residual = residual - alpha * operator_conjugate_vector
-
-        residual_norm_squared_previous = residual_norm_squared
+        residual_dot_residual = vdot(residual, residual).real
+
+        if residual_dot_residual < tolerance**2:  # are we done?
+            break
+
+        if preconditioner_inverse is not None:
+            direction = preconditioner_inverse(*residual)
+            direction_dot_residual = vdot(residual, direction).real
+        else:
+            direction = residual
+            direction_dot_residual = residual_dot_residual
+
+        if direction_dot_residual_prev is not None:  # not first iteration
+            beta = direction_dot_residual / direction_dot_residual_prev
+            conjugate = tuple(res + beta * con for res, con in zip(direction, conjugate, strict=True))
path: src/mrpro/algorithms/optimizers/cg.py
user: login: ckolbPTB
body: dir, con in zip(direction, conjugate)
created_at: 2025-04-14T13:24:16Z
author_association: COLLABORATOR
diff_hunk: @@ -74,54 +125,57 @@ def cg(
     .. [Nocedal2006] Nocedal, J. (2006). *Numerical Optimization* (2nd ed.). Springer.
     .. [WikipediaCG] Wikipedia: Conjugate Gradient https://en.wikipedia.org/wiki/Conjugate_gradient
     """
-    if initial_value is not None and (initial_value.shape != right_hand_side.shape):
-        raise ValueError(
-            f'Shapes of starting_value and right_hand_side must match,got {initial_value.shape, right_hand_side.shape}'
-        )
-
-    # initial residual
-    residual = right_hand_side - operator(initial_value)[0] if initial_value is not None else right_hand_side.clone()
-
-    # initialize conjugate vector
-    conjugate_vector = residual.clone()
-
-    # assign starting value to the solution
-    solution = initial_value.clone() if initial_value is not None else right_hand_side.clone()
-
-    # for the case where the residual is exactly zero
-    if torch.vdot(residual.flatten(), residual.flatten()) == 0:
-        return solution
+    right_hand_side_ = (right_hand_side,) if isinstance(right_hand_side, torch.Tensor) else tuple(right_hand_side)
+    if initial_value is None:
+        solution = right_hand_side_
+    elif isinstance(initial_value, torch.Tensor):
+        solution = (initial_value,)
+    elif len(initial_value) != len(right_hand_side_):
+        raise ValueError('Length mismatch in initial_value and right_hand_side')
+    else:
+        solution = tuple(initial_value)
+    if any(s.shape != r.shape for s, r in zip(solution, right_hand_side_, strict=True)):
+        raise ValueError('Shape mismatch in initial_value and right_hand_side')
+
+    residual = tuple(rhs - op_sol for rhs, op_sol in zip(right_hand_side_, operator(*solution), strict=True))
+
+    if preconditioner_inverse is not None:
+        conjugate = preconditioner_inverse(*residual)
+    else:
+        conjugate = residual
 
     # dummy value. new value will be set in loop before first usage
-    residual_norm_squared_previous: torch.Tensor | None = None
+    direction_dot_residual_prev: torch.Tensor | None = None
 
     for iteration in range(max_iterations):
-        # calculate the square norm of the residual
-        residual_flat = residual.flatten()
-        residual_norm_squared = torch.vdot(residual_flat, residual_flat).real
-
-        # check if the solution is already accurate enough
-        if tolerance != 0 and (residual_norm_squared < tolerance**2):
-            return solution
-
-        if residual_norm_squared_previous is not None:  # not first iteration
-            beta = residual_norm_squared / residual_norm_squared_previous
-            conjugate_vector = residual + beta * conjugate_vector
-
-        # update estimates of the solution and the residual
-        (operator_conjugate_vector,) = operator(conjugate_vector)
-        alpha = residual_norm_squared / (torch.vdot(conjugate_vector.flatten(), operator_conjugate_vector.flatten()))
-        solution = solution + alpha * conjugate_vector
-        residual = residual - alpha * operator_conjugate_vector
-
-        residual_norm_squared_previous = residual_norm_squared
+        residual_dot_residual = vdot(residual, residual).real
+
+        if residual_dot_residual < tolerance**2:  # are we done?
+            break
+
+        if preconditioner_inverse is not None:
+            direction = preconditioner_inverse(*residual)
+            direction_dot_residual = vdot(residual, direction).real
+        else:
+            direction = residual
+            direction_dot_residual = residual_dot_residual
+
+        if direction_dot_residual_prev is not None:  # not first iteration
+            beta = direction_dot_residual / direction_dot_residual_prev
+            conjugate = tuple(res + beta * con for res, con in zip(direction, conjugate, strict=True))
path: src/mrpro/algorithms/optimizers/cg.py
user: login: fzimmermann89
body: dir would shadow dir(), so d

created_at: 2025-04-14T15:01:03Z
author_association: MEMBER
diff_hunk: @@ -74,54 +125,57 @@ def cg(
     .. [Nocedal2006] Nocedal, J. (2006). *Numerical Optimization* (2nd ed.). Springer.
     .. [WikipediaCG] Wikipedia: Conjugate Gradient https://en.wikipedia.org/wiki/Conjugate_gradient
     """
-    if initial_value is not None and (initial_value.shape != right_hand_side.shape):
-        raise ValueError(
-            f'Shapes of starting_value and right_hand_side must match,got {initial_value.shape, right_hand_side.shape}'
-        )
-
-    # initial residual
-    residual = right_hand_side - operator(initial_value)[0] if initial_value is not None else right_hand_side.clone()
-
-    # initialize conjugate vector
-    conjugate_vector = residual.clone()
-
-    # assign starting value to the solution
-    solution = initial_value.clone() if initial_value is not None else right_hand_side.clone()
-
-    # for the case where the residual is exactly zero
-    if torch.vdot(residual.flatten(), residual.flatten()) == 0:
-        return solution
+    right_hand_side_ = (right_hand_side,) if isinstance(right_hand_side, torch.Tensor) else tuple(right_hand_side)
+    if initial_value is None:
+        solution = right_hand_side_
+    elif isinstance(initial_value, torch.Tensor):
+        solution = (initial_value,)
+    elif len(initial_value) != len(right_hand_side_):
+        raise ValueError('Length mismatch in initial_value and right_hand_side')
+    else:
+        solution = tuple(initial_value)
+    if any(s.shape != r.shape for s, r in zip(solution, right_hand_side_, strict=True)):
+        raise ValueError('Shape mismatch in initial_value and right_hand_side')
+
+    residual = tuple(rhs - op_sol for rhs, op_sol in zip(right_hand_side_, operator(*solution), strict=True))
+
+    if preconditioner_inverse is not None:
+        conjugate = preconditioner_inverse(*residual)
+    else:
+        conjugate = residual
 
     # dummy value. new value will be set in loop before first usage
-    residual_norm_squared_previous: torch.Tensor | None = None
+    direction_dot_residual_prev: torch.Tensor | None = None
path: src/mrpro/algorithms/optimizers/cg.py
user: login: fzimmermann89
body: old?
created_at: 2025-04-14T15:01:28Z
author_association: MEMBER
diff_hunk: @@ -74,54 +125,57 @@ def cg(
     .. [Nocedal2006] Nocedal, J. (2006). *Numerical Optimization* (2nd ed.). Springer.
     .. [WikipediaCG] Wikipedia: Conjugate Gradient https://en.wikipedia.org/wiki/Conjugate_gradient
     """
-    if initial_value is not None and (initial_value.shape != right_hand_side.shape):
-        raise ValueError(
-            f'Shapes of starting_value and right_hand_side must match,got {initial_value.shape, right_hand_side.shape}'
-        )
-
-    # initial residual
-    residual = right_hand_side - operator(initial_value)[0] if initial_value is not None else right_hand_side.clone()
-
-    # initialize conjugate vector
-    conjugate_vector = residual.clone()
-
-    # assign starting value to the solution
-    solution = initial_value.clone() if initial_value is not None else right_hand_side.clone()
-
-    # for the case where the residual is exactly zero
-    if torch.vdot(residual.flatten(), residual.flatten()) == 0:
-        return solution
+    right_hand_side_ = (right_hand_side,) if isinstance(right_hand_side, torch.Tensor) else tuple(right_hand_side)
+    if initial_value is None:
+        solution = right_hand_side_
+    elif isinstance(initial_value, torch.Tensor):
+        solution = (initial_value,)
+    elif len(initial_value) != len(right_hand_side_):
+        raise ValueError('Length mismatch in initial_value and right_hand_side')
+    else:
+        solution = tuple(initial_value)
+    if any(s.shape != r.shape for s, r in zip(solution, right_hand_side_, strict=True)):
+        raise ValueError('Shape mismatch in initial_value and right_hand_side')
+
+    residual = tuple(rhs - op_sol for rhs, op_sol in zip(right_hand_side_, operator(*solution), strict=True))
+
+    if preconditioner_inverse is not None:
+        conjugate = preconditioner_inverse(*residual)
+    else:
+        conjugate = residual
 
     # dummy value. new value will be set in loop before first usage
-    residual_norm_squared_previous: torch.Tensor | None = None
+    direction_dot_residual_prev: torch.Tensor | None = None
path: src/mrpro/algorithms/optimizers/cg.py
user: login: ckolbPTB
body: sorry, I meant `direction_dot_residual_previous`

created_at: 2025-04-14T15:07:06Z
author_association: COLLABORATOR
diff_hunk: @@ -74,54 +125,57 @@ def cg(
     .. [Nocedal2006] Nocedal, J. (2006). *Numerical Optimization* (2nd ed.). Springer.
     .. [WikipediaCG] Wikipedia: Conjugate Gradient https://en.wikipedia.org/wiki/Conjugate_gradient
     """
-    if initial_value is not None and (initial_value.shape != right_hand_side.shape):
-        raise ValueError(
-            f'Shapes of starting_value and right_hand_side must match,got {initial_value.shape, right_hand_side.shape}'
-        )
-
-    # initial residual
-    residual = right_hand_side - operator(initial_value)[0] if initial_value is not None else right_hand_side.clone()
-
-    # initialize conjugate vector
-    conjugate_vector = residual.clone()
-
-    # assign starting value to the solution
-    solution = initial_value.clone() if initial_value is not None else right_hand_side.clone()
-
-    # for the case where the residual is exactly zero
-    if torch.vdot(residual.flatten(), residual.flatten()) == 0:
-        return solution
+    right_hand_side_ = (right_hand_side,) if isinstance(right_hand_side, torch.Tensor) else tuple(right_hand_side)
+    if initial_value is None:
+        solution = right_hand_side_
+    elif isinstance(initial_value, torch.Tensor):
+        solution = (initial_value,)
+    elif len(initial_value) != len(right_hand_side_):
+        raise ValueError('Length mismatch in initial_value and right_hand_side')
+    else:
+        solution = tuple(initial_value)
+    if any(s.shape != r.shape for s, r in zip(solution, right_hand_side_, strict=True)):
+        raise ValueError('Shape mismatch in initial_value and right_hand_side')
+
+    residual = tuple(rhs - op_sol for rhs, op_sol in zip(right_hand_side_, operator(*solution), strict=True))
+
+    if preconditioner_inverse is not None:
+        conjugate = preconditioner_inverse(*residual)
+    else:
+        conjugate = residual
 
     # dummy value. new value will be set in loop before first usage
-    residual_norm_squared_previous: torch.Tensor | None = None
+    direction_dot_residual_prev: torch.Tensor | None = None
path: src/mrpro/algorithms/optimizers/cg.py
user: login: fzimmermann89
body: And I meant direction_dot_residual_old
created_at: 2025-04-14T15:55:53Z
author_association: MEMBER
diff_hunk: @@ -43,25 +91,28 @@ def cg(
 
     This implementation assumes that :math:`H` is self-adjoint and does not verify this condition.
 
+    If `preconditioner_inverse` is provided, it solves :math:`M^{-1}Hx = M^{-1}b`
+    implicitly, where `preconditioner_inverse(r)` computes :math:`M^{-1}r`.
+
     See [Hestenes1952]_, [Nocedal2006]_, and [WikipediaCG]_ for more information.
 
+
     Parameters
     ----------
     operator
-        self-adjoint operator (named :math:`H` above)
+        Self-adjoint operator :math:`H`.
     right_hand_side
-        right-hand-side of the system (named :math:`b` above)
+        Right-hand-side :math:`b`.
     initial_value
-        initial value of the algorithm; if `None`, it will be set to `right_hand_side`
+        Initial guess :math:`x_0`.
path: src/mrpro/algorithms/optimizers/cg.py
user: login: fzimmermann89
body: yes, i also changed it to match scipy and jax
created_at: 2025-04-14T18:24:01Z
author_association: MEMBER
diff_hunk: @@ -1,32 +1,79 @@
 """Conjugate Gradient for linear systems with self-adjoint linear operator."""
 
-from collections.abc import Callable
+from collections.abc import Callable, Sequence
 
 import torch
+from typing_extensions import Unpack, overload
 
 from mrpro.algorithms.optimizers.OptimizerStatus import OptimizerStatus
 from mrpro.operators.LinearOperator import LinearOperator
+from mrpro.operators.LinearOperatorMatrix import LinearOperatorMatrix
+
+
+def vdot(a: Sequence[torch.Tensor], b: Sequence[torch.Tensor]) -> torch.Tensor:
+    """Vector dot product."""
+    return sum(
+        (torch.vdot(a_i.flatten(), b_i.flatten()) for a_i, b_i in zip(a, b, strict=True)), start=torch.tensor(0.0)
+    )
 
 
 class CGStatus(OptimizerStatus):
     """Status of the conjugate gradient algorithm."""
 
-    residual: torch.Tensor
+    residual: tuple[torch.Tensor, ...]
     """Residual of the current estimate."""
 
+    preconditioned_residual: tuple[torch.Tensor, ...]
+    """Preconditioned residual of the current estimate."""
+
+
+OperatorMatrixLikeCallable = Callable[[Unpack[tuple[torch.Tensor, ...]]], tuple[torch.Tensor, ...]]
+OperatorLikeCallable = Callable[[torch.Tensor], tuple[torch.Tensor]]
 
+
+@overload
 def cg(
-    operator: LinearOperator,
-    right_hand_side: torch.Tensor,
-    initial_value: torch.Tensor | None = None,
+    operator: OperatorLikeCallable,
+    right_hand_side: tuple[torch.Tensor] | torch.Tensor,
     *,
+    initial_value: tuple[torch.Tensor] | torch.Tensor | None = None,
+    preconditioner_inverse: OperatorLikeCallable | None = None,
     max_iterations: int = 128,
     tolerance: float = 1e-4,
     callback: Callable[[CGStatus], bool | None] | None = None,
-) -> torch.Tensor:
-    r"""CG for solving a linear system :math:`Hx=b`.
+) -> tuple[torch.Tensor]: ...
+
 
-    This algorithm solves systems of the form :math:`H x = b`, where :math:`H` is a self-adjoint linear operator
+@overload
+def cg(
+    operator: OperatorMatrixLikeCallable,
+    right_hand_side: Sequence[torch.Tensor],
+    *,
+    initial_value: Sequence[torch.Tensor] | None = None,
+    preconditioner_inverse: OperatorMatrixLikeCallable | None = None,
+    max_iterations: int = 128,
+    tolerance: float = 1e-4,
+    callback: Callable[[CGStatus], bool | None] | None = None,
+) -> tuple[torch.Tensor, ...]: ...
+
+
+def cg(
+    operator: LinearOperator | LinearOperatorMatrix | OperatorMatrixLikeCallable | OperatorLikeCallable,
+    right_hand_side: Sequence[torch.Tensor] | torch.Tensor,
+    *,
+    initial_value: Sequence[torch.Tensor] | torch.Tensor | None = None,
+    preconditioner_inverse: LinearOperator
+    | LinearOperatorMatrix
+    | OperatorMatrixLikeCallable
+    | OperatorLikeCallable
+    | None = None,
+    max_iterations: int = 128,
path: src/mrpro/algorithms/optimizers/cg.py
user: login: koflera
body: that sounds like many iterations, did we always use to have this many? I think probably 16 or 32 would be enough already in most cases to be used as the default?
created_at: 2025-04-16T13:37:57Z
author_association: COLLABORATOR
diff_hunk: @@ -1,32 +1,79 @@
 """Conjugate Gradient for linear systems with self-adjoint linear operator."""
 
-from collections.abc import Callable
+from collections.abc import Callable, Sequence
 
 import torch
+from typing_extensions import Unpack, overload
 
 from mrpro.algorithms.optimizers.OptimizerStatus import OptimizerStatus
 from mrpro.operators.LinearOperator import LinearOperator
+from mrpro.operators.LinearOperatorMatrix import LinearOperatorMatrix
+
+
+def vdot(a: Sequence[torch.Tensor], b: Sequence[torch.Tensor]) -> torch.Tensor:
+    """Vector dot product."""
+    return sum(
+        (torch.vdot(a_i.flatten(), b_i.flatten()) for a_i, b_i in zip(a, b, strict=True)), start=torch.tensor(0.0)
+    )
 
 
 class CGStatus(OptimizerStatus):
     """Status of the conjugate gradient algorithm."""
 
-    residual: torch.Tensor
+    residual: tuple[torch.Tensor, ...]
     """Residual of the current estimate."""
 
+    preconditioned_residual: tuple[torch.Tensor, ...]
+    """Preconditioned residual of the current estimate."""
+
+
+OperatorMatrixLikeCallable = Callable[[Unpack[tuple[torch.Tensor, ...]]], tuple[torch.Tensor, ...]]
+OperatorLikeCallable = Callable[[torch.Tensor], tuple[torch.Tensor]]
 
+
+@overload
 def cg(
-    operator: LinearOperator,
-    right_hand_side: torch.Tensor,
-    initial_value: torch.Tensor | None = None,
+    operator: OperatorLikeCallable,
+    right_hand_side: tuple[torch.Tensor] | torch.Tensor,
     *,
+    initial_value: tuple[torch.Tensor] | torch.Tensor | None = None,
+    preconditioner_inverse: OperatorLikeCallable | None = None,
     max_iterations: int = 128,
     tolerance: float = 1e-4,
     callback: Callable[[CGStatus], bool | None] | None = None,
-) -> torch.Tensor:
-    r"""CG for solving a linear system :math:`Hx=b`.
+) -> tuple[torch.Tensor]: ...
+
 
-    This algorithm solves systems of the form :math:`H x = b`, where :math:`H` is a self-adjoint linear operator
+@overload
+def cg(
+    operator: OperatorMatrixLikeCallable,
+    right_hand_side: Sequence[torch.Tensor],
+    *,
+    initial_value: Sequence[torch.Tensor] | None = None,
+    preconditioner_inverse: OperatorMatrixLikeCallable | None = None,
+    max_iterations: int = 128,
+    tolerance: float = 1e-4,
+    callback: Callable[[CGStatus], bool | None] | None = None,
+) -> tuple[torch.Tensor, ...]: ...
+
+
+def cg(
+    operator: LinearOperator | LinearOperatorMatrix | OperatorMatrixLikeCallable | OperatorLikeCallable,
+    right_hand_side: Sequence[torch.Tensor] | torch.Tensor,
+    *,
+    initial_value: Sequence[torch.Tensor] | torch.Tensor | None = None,
+    preconditioner_inverse: LinearOperator
+    | LinearOperatorMatrix
+    | OperatorMatrixLikeCallable
+    | OperatorLikeCallable
+    | None = None,
+    max_iterations: int = 128,
+    tolerance: float = 1e-4,
+    callback: Callable[[CGStatus], bool | None] | None = None,
+) -> tuple[torch.Tensor, ...] | tuple[torch.Tensor]:
+    r"""(Preconditioned) Conjugate Gradient for solving :math:`Hx=b`.
+
+     This algorithm solves systems of the form :math:`H x = b`, where :math:`H` is a self-adjoint linear operator
path: src/mrpro/algorithms/optimizers/cg.py
user: login: koflera
body: maybe worth mentioning that this implementation also works for matrices containing linear operators and what exactly is meant by that, i.e. the interpretation is to have an operator H = diag(H_1, ..., H_N) with H_j self adjoint. This is what is intended to be done here, right? :D

(Also, maybe highlight the difference between working with a matrix of opeartors and the other case that we already had described in the docstrings?)
created_at: 2025-04-16T13:45:59Z
author_association: COLLABORATOR
diff_hunk: @@ -1,32 +1,79 @@
 """Conjugate Gradient for linear systems with self-adjoint linear operator."""
 
-from collections.abc import Callable
+from collections.abc import Callable, Sequence
 
 import torch
+from typing_extensions import Unpack, overload
 
 from mrpro.algorithms.optimizers.OptimizerStatus import OptimizerStatus
 from mrpro.operators.LinearOperator import LinearOperator
+from mrpro.operators.LinearOperatorMatrix import LinearOperatorMatrix
+
+
+def vdot(a: Sequence[torch.Tensor], b: Sequence[torch.Tensor]) -> torch.Tensor:
+    """Vector dot product."""
+    return sum(
+        (torch.vdot(a_i.flatten(), b_i.flatten()) for a_i, b_i in zip(a, b, strict=True)), start=torch.tensor(0.0)
+    )
 
 
 class CGStatus(OptimizerStatus):
     """Status of the conjugate gradient algorithm."""
 
-    residual: torch.Tensor
+    residual: tuple[torch.Tensor, ...]
     """Residual of the current estimate."""
 
+    preconditioned_residual: tuple[torch.Tensor, ...]
+    """Preconditioned residual of the current estimate."""
+
+
+OperatorMatrixLikeCallable = Callable[[Unpack[tuple[torch.Tensor, ...]]], tuple[torch.Tensor, ...]]
+OperatorLikeCallable = Callable[[torch.Tensor], tuple[torch.Tensor]]
 
+
+@overload
 def cg(
-    operator: LinearOperator,
-    right_hand_side: torch.Tensor,
-    initial_value: torch.Tensor | None = None,
+    operator: OperatorLikeCallable,
+    right_hand_side: tuple[torch.Tensor] | torch.Tensor,
     *,
+    initial_value: tuple[torch.Tensor] | torch.Tensor | None = None,
+    preconditioner_inverse: OperatorLikeCallable | None = None,
     max_iterations: int = 128,
     tolerance: float = 1e-4,
     callback: Callable[[CGStatus], bool | None] | None = None,
-) -> torch.Tensor:
-    r"""CG for solving a linear system :math:`Hx=b`.
+) -> tuple[torch.Tensor]: ...
+
 
-    This algorithm solves systems of the form :math:`H x = b`, where :math:`H` is a self-adjoint linear operator
+@overload
+def cg(
+    operator: OperatorMatrixLikeCallable,
+    right_hand_side: Sequence[torch.Tensor],
+    *,
+    initial_value: Sequence[torch.Tensor] | None = None,
+    preconditioner_inverse: OperatorMatrixLikeCallable | None = None,
+    max_iterations: int = 128,
+    tolerance: float = 1e-4,
+    callback: Callable[[CGStatus], bool | None] | None = None,
+) -> tuple[torch.Tensor, ...]: ...
+
+
+def cg(
+    operator: LinearOperator | LinearOperatorMatrix | OperatorMatrixLikeCallable | OperatorLikeCallable,
+    right_hand_side: Sequence[torch.Tensor] | torch.Tensor,
+    *,
+    initial_value: Sequence[torch.Tensor] | torch.Tensor | None = None,
+    preconditioner_inverse: LinearOperator
+    | LinearOperatorMatrix
+    | OperatorMatrixLikeCallable
+    | OperatorLikeCallable
+    | None = None,
+    max_iterations: int = 128,
+    tolerance: float = 1e-4,
+    callback: Callable[[CGStatus], bool | None] | None = None,
+) -> tuple[torch.Tensor, ...] | tuple[torch.Tensor]:
+    r"""(Preconditioned) Conjugate Gradient for solving :math:`Hx=b`.
+
+     This algorithm solves systems of the form :math:`H x = b`, where :math:`H` is a self-adjoint linear operator
path: src/mrpro/algorithms/optimizers/cg.py
user: login: fzimmermann89
body: A linearoperatormatrix is mathematicaly speaking  just a linear operator H, correct?
We make no diagonal assumpion anywhere. And how exactly a linear operator matrix is applied is doumented in the linearoperator matrix..
I dont really see the need to add anything here? Maybe for the parameter docstring, we could call out and reference the MatrixOepratr class ? 
created_at: 2025-04-17T08:36:39Z
author_association: MEMBER
diff_hunk: @@ -1,32 +1,79 @@
 """Conjugate Gradient for linear systems with self-adjoint linear operator."""
 
-from collections.abc import Callable
+from collections.abc import Callable, Sequence
 
 import torch
+from typing_extensions import Unpack, overload
 
 from mrpro.algorithms.optimizers.OptimizerStatus import OptimizerStatus
 from mrpro.operators.LinearOperator import LinearOperator
+from mrpro.operators.LinearOperatorMatrix import LinearOperatorMatrix
+
+
+def vdot(a: Sequence[torch.Tensor], b: Sequence[torch.Tensor]) -> torch.Tensor:
+    """Vector dot product."""
+    return sum(
+        (torch.vdot(a_i.flatten(), b_i.flatten()) for a_i, b_i in zip(a, b, strict=True)), start=torch.tensor(0.0)
+    )
 
 
 class CGStatus(OptimizerStatus):
     """Status of the conjugate gradient algorithm."""
 
-    residual: torch.Tensor
+    residual: tuple[torch.Tensor, ...]
     """Residual of the current estimate."""
 
+    preconditioned_residual: tuple[torch.Tensor, ...]
+    """Preconditioned residual of the current estimate."""
+
+
+OperatorMatrixLikeCallable = Callable[[Unpack[tuple[torch.Tensor, ...]]], tuple[torch.Tensor, ...]]
+OperatorLikeCallable = Callable[[torch.Tensor], tuple[torch.Tensor]]
 
+
+@overload
 def cg(
-    operator: LinearOperator,
-    right_hand_side: torch.Tensor,
-    initial_value: torch.Tensor | None = None,
+    operator: OperatorLikeCallable,
+    right_hand_side: tuple[torch.Tensor] | torch.Tensor,
     *,
+    initial_value: tuple[torch.Tensor] | torch.Tensor | None = None,
+    preconditioner_inverse: OperatorLikeCallable | None = None,
     max_iterations: int = 128,
     tolerance: float = 1e-4,
     callback: Callable[[CGStatus], bool | None] | None = None,
-) -> torch.Tensor:
-    r"""CG for solving a linear system :math:`Hx=b`.
+) -> tuple[torch.Tensor]: ...
+
 
-    This algorithm solves systems of the form :math:`H x = b`, where :math:`H` is a self-adjoint linear operator
+@overload
+def cg(
+    operator: OperatorMatrixLikeCallable,
+    right_hand_side: Sequence[torch.Tensor],
+    *,
+    initial_value: Sequence[torch.Tensor] | None = None,
+    preconditioner_inverse: OperatorMatrixLikeCallable | None = None,
+    max_iterations: int = 128,
+    tolerance: float = 1e-4,
+    callback: Callable[[CGStatus], bool | None] | None = None,
+) -> tuple[torch.Tensor, ...]: ...
+
+
+def cg(
+    operator: LinearOperator | LinearOperatorMatrix | OperatorMatrixLikeCallable | OperatorLikeCallable,
+    right_hand_side: Sequence[torch.Tensor] | torch.Tensor,
+    *,
+    initial_value: Sequence[torch.Tensor] | torch.Tensor | None = None,
+    preconditioner_inverse: LinearOperator
+    | LinearOperatorMatrix
+    | OperatorMatrixLikeCallable
+    | OperatorLikeCallable
+    | None = None,
+    max_iterations: int = 128,
+    tolerance: float = 1e-4,
+    callback: Callable[[CGStatus], bool | None] | None = None,
+) -> tuple[torch.Tensor, ...] | tuple[torch.Tensor]:
+    r"""(Preconditioned) Conjugate Gradient for solving :math:`Hx=b`.
+
+     This algorithm solves systems of the form :math:`H x = b`, where :math:`H` is a self-adjoint linear operator
path: src/mrpro/algorithms/optimizers/cg.py
user: login: koflera
body: Yeah, sorry, the comment was not quite clear.... I think I was just wondering for which matrices of linear operators one could meaningfully use the CG, given that the CG is supposed to be used for self-adjoint operators and the only example I could think of at first sight was block-diagonal matrices of containing self-adjoint operators in the diagonal.
But giving it a little bit more thought, there are also non-block-diagonal examples, i.e. when all the linear operators in the matrix are self-adjoint and the matrix of linear operators is block-symmetric. But I think these are all the matrices of linear operators that fulfill the self-adjoint property, i.e. "M is self-adjoint iff ... ".
Maybe we could include this in the docstring and point out that, as for the case with only the linear operator, it is up to the user to make sure that the matrix of linear operators fulfills the condition and that this is not checked by the CG.

created_at: 2025-04-17T09:29:42Z
author_association: COLLABORATOR
diff_hunk: @@ -1,32 +1,79 @@
 """Conjugate Gradient for linear systems with self-adjoint linear operator."""
 
-from collections.abc import Callable
+from collections.abc import Callable, Sequence
 
 import torch
+from typing_extensions import Unpack, overload
 
 from mrpro.algorithms.optimizers.OptimizerStatus import OptimizerStatus
 from mrpro.operators.LinearOperator import LinearOperator
+from mrpro.operators.LinearOperatorMatrix import LinearOperatorMatrix
+
+
+def vdot(a: Sequence[torch.Tensor], b: Sequence[torch.Tensor]) -> torch.Tensor:
+    """Vector dot product."""
+    return sum(
+        (torch.vdot(a_i.flatten(), b_i.flatten()) for a_i, b_i in zip(a, b, strict=True)), start=torch.tensor(0.0)
+    )
 
 
 class CGStatus(OptimizerStatus):
     """Status of the conjugate gradient algorithm."""
 
-    residual: torch.Tensor
+    residual: tuple[torch.Tensor, ...]
     """Residual of the current estimate."""
 
+    preconditioned_residual: tuple[torch.Tensor, ...]
+    """Preconditioned residual of the current estimate."""
+
+
+OperatorMatrixLikeCallable = Callable[[Unpack[tuple[torch.Tensor, ...]]], tuple[torch.Tensor, ...]]
+OperatorLikeCallable = Callable[[torch.Tensor], tuple[torch.Tensor]]
 
+
+@overload
 def cg(
-    operator: LinearOperator,
-    right_hand_side: torch.Tensor,
-    initial_value: torch.Tensor | None = None,
+    operator: OperatorLikeCallable,
+    right_hand_side: tuple[torch.Tensor] | torch.Tensor,
     *,
+    initial_value: tuple[torch.Tensor] | torch.Tensor | None = None,
+    preconditioner_inverse: OperatorLikeCallable | None = None,
     max_iterations: int = 128,
     tolerance: float = 1e-4,
     callback: Callable[[CGStatus], bool | None] | None = None,
-) -> torch.Tensor:
-    r"""CG for solving a linear system :math:`Hx=b`.
+) -> tuple[torch.Tensor]: ...
+
 
-    This algorithm solves systems of the form :math:`H x = b`, where :math:`H` is a self-adjoint linear operator
+@overload
+def cg(
+    operator: OperatorMatrixLikeCallable,
+    right_hand_side: Sequence[torch.Tensor],
+    *,
+    initial_value: Sequence[torch.Tensor] | None = None,
+    preconditioner_inverse: OperatorMatrixLikeCallable | None = None,
+    max_iterations: int = 128,
+    tolerance: float = 1e-4,
+    callback: Callable[[CGStatus], bool | None] | None = None,
+) -> tuple[torch.Tensor, ...]: ...
+
+
+def cg(
+    operator: LinearOperator | LinearOperatorMatrix | OperatorMatrixLikeCallable | OperatorLikeCallable,
+    right_hand_side: Sequence[torch.Tensor] | torch.Tensor,
+    *,
+    initial_value: Sequence[torch.Tensor] | torch.Tensor | None = None,
+    preconditioner_inverse: LinearOperator
+    | LinearOperatorMatrix
+    | OperatorMatrixLikeCallable
+    | OperatorLikeCallable
+    | None = None,
+    max_iterations: int = 128,
+    tolerance: float = 1e-4,
+    callback: Callable[[CGStatus], bool | None] | None = None,
+) -> tuple[torch.Tensor, ...] | tuple[torch.Tensor]:
+    r"""(Preconditioned) Conjugate Gradient for solving :math:`Hx=b`.
+
+     This algorithm solves systems of the form :math:`H x = b`, where :math:`H` is a self-adjoint linear operator
path: src/mrpro/algorithms/optimizers/cg.py
user: login: koflera
body: which also means that we could maybe at least raise a warning or a value error if the block-symmetry is not fulfilled? is it possible to check whether two linear-operators are the same?
created_at: 2025-04-17T09:33:35Z
author_association: COLLABORATOR
diff_hunk: @@ -1,32 +1,79 @@
 """Conjugate Gradient for linear systems with self-adjoint linear operator."""
 
-from collections.abc import Callable
+from collections.abc import Callable, Sequence
 
 import torch
+from typing_extensions import Unpack, overload
 
 from mrpro.algorithms.optimizers.OptimizerStatus import OptimizerStatus
 from mrpro.operators.LinearOperator import LinearOperator
+from mrpro.operators.LinearOperatorMatrix import LinearOperatorMatrix
+
+
+def vdot(a: Sequence[torch.Tensor], b: Sequence[torch.Tensor]) -> torch.Tensor:
+    """Vector dot product."""
+    return sum(
+        (torch.vdot(a_i.flatten(), b_i.flatten()) for a_i, b_i in zip(a, b, strict=True)), start=torch.tensor(0.0)
+    )
 
 
 class CGStatus(OptimizerStatus):
     """Status of the conjugate gradient algorithm."""
 
-    residual: torch.Tensor
+    residual: tuple[torch.Tensor, ...]
     """Residual of the current estimate."""
 
+    preconditioned_residual: tuple[torch.Tensor, ...]
+    """Preconditioned residual of the current estimate."""
+
+
+OperatorMatrixLikeCallable = Callable[[Unpack[tuple[torch.Tensor, ...]]], tuple[torch.Tensor, ...]]
+OperatorLikeCallable = Callable[[torch.Tensor], tuple[torch.Tensor]]
 
+
+@overload
 def cg(
-    operator: LinearOperator,
-    right_hand_side: torch.Tensor,
-    initial_value: torch.Tensor | None = None,
+    operator: OperatorLikeCallable,
+    right_hand_side: tuple[torch.Tensor] | torch.Tensor,
     *,
+    initial_value: tuple[torch.Tensor] | torch.Tensor | None = None,
+    preconditioner_inverse: OperatorLikeCallable | None = None,
     max_iterations: int = 128,
     tolerance: float = 1e-4,
     callback: Callable[[CGStatus], bool | None] | None = None,
-) -> torch.Tensor:
-    r"""CG for solving a linear system :math:`Hx=b`.
+) -> tuple[torch.Tensor]: ...
+
 
-    This algorithm solves systems of the form :math:`H x = b`, where :math:`H` is a self-adjoint linear operator
+@overload
+def cg(
+    operator: OperatorMatrixLikeCallable,
+    right_hand_side: Sequence[torch.Tensor],
+    *,
+    initial_value: Sequence[torch.Tensor] | None = None,
+    preconditioner_inverse: OperatorMatrixLikeCallable | None = None,
+    max_iterations: int = 128,
+    tolerance: float = 1e-4,
+    callback: Callable[[CGStatus], bool | None] | None = None,
+) -> tuple[torch.Tensor, ...]: ...
+
+
+def cg(
+    operator: LinearOperator | LinearOperatorMatrix | OperatorMatrixLikeCallable | OperatorLikeCallable,
+    right_hand_side: Sequence[torch.Tensor] | torch.Tensor,
+    *,
+    initial_value: Sequence[torch.Tensor] | torch.Tensor | None = None,
+    preconditioner_inverse: LinearOperator
+    | LinearOperatorMatrix
+    | OperatorMatrixLikeCallable
+    | OperatorLikeCallable
+    | None = None,
+    max_iterations: int = 128,
path: src/mrpro/algorithms/optimizers/cg.py
user: login: fzimmermann89
body: we always had it at 128.... 
created_at: 2025-04-17T11:51:00Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-07T10:57:50Z
body: <!-- Pytest Coverage Comment: coverage_report_pr -->
<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/README.md"><img alt="Coverage" src="https://img.shields.io/badge/Coverage-92%25-brightgreen.svg" /></a><details><summary>Coverage Report </summary><table><tr><th>File</th><th>Stmts</th><th>Miss</th><th>Cover</th><th>Missing</th></tr><tbody><tr><td colspan="5"><b>src/mrpro</b></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/_version.py">_version.py</a></td><td>6</td><td>2</td><td>67%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/_version.py#L7-L8">7&ndash;8</a></td></tr><tr><td colspan="5"><b>src/mrpro/algorithms/csm</b></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/algorithms/csm/inati.py">inati.py</a></td><td>24</td><td>1</td><td>96%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/algorithms/csm/inati.py#L42">42</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/algorithms/csm/walsh.py">walsh.py</a></td><td>16</td><td>1</td><td>94%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/algorithms/csm/walsh.py#L53">53</a></td></tr><tr><td colspan="5"><b>src/mrpro/algorithms/dcf</b></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/algorithms/dcf/dcf_voronoi.py">dcf_voronoi.py</a></td><td>55</td><td>4</td><td>93%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/algorithms/dcf/dcf_voronoi.py#L15">15</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/algorithms/dcf/dcf_voronoi.py#L55-L56">55&ndash;56</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/algorithms/dcf/dcf_voronoi.py#L89">89</a></td></tr><tr><td colspan="5"><b>src/mrpro/algorithms/optimizers</b></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/algorithms/optimizers/adam.py">adam.py</a></td><td>30</td><td>6</td><td>80%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/algorithms/optimizers/adam.py#L108">108</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/algorithms/optimizers/adam.py#L125-L129">125&ndash;129</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/algorithms/optimizers/cg.py">cg.py</a></td><td>56</td><td>1</td><td>98%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/algorithms/optimizers/cg.py#L139">139</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/algorithms/optimizers/pdhg.py">pdhg.py</a></td><td>81</td><td>3</td><td>96%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/algorithms/optimizers/pdhg.py#L178-L179">178&ndash;179</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/algorithms/optimizers/pdhg.py#L185">185</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/algorithms/optimizers/pgd.py">pgd.py</a></td><td>53</td><td>4</td><td>92%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/algorithms/optimizers/pgd.py#L107">107</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/algorithms/optimizers/pgd.py#L152-L155">152&ndash;155</a></td></tr><tr><td colspan="5"><b>src/mrpro/algorithms/reconstruction</b></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/algorithms/reconstruction/DirectReconstruction.py">DirectReconstruction.py</a></td><td>28</td><td>6</td><td>79%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/algorithms/reconstruction/DirectReconstruction.py#L62">62</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/algorithms/reconstruction/DirectReconstruction.py#L65">65</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/algorithms/reconstruction/DirectReconstruction.py#L70">70</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/algorithms/reconstruction/DirectReconstruction.py#L77-L79">77&ndash;79</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/algorithms/reconstruction/IterativeSENSEReconstruction.py">IterativeSENSEReconstruction.py</a></td><td>13</td><td>1</td><td>92%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/algorithms/reconstruction/IterativeSENSEReconstruction.py#L79">79</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/algorithms/reconstruction/Reconstruction.py">Reconstruction.py</a></td><td>50</td><td>13</td><td>74%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/algorithms/reconstruction/Reconstruction.py#L54-L56">54&ndash;56</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/algorithms/reconstruction/Reconstruction.py#L80-L87">80&ndash;87</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/algorithms/reconstruction/Reconstruction.py#L109">109</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/algorithms/reconstruction/Reconstruction.py#L112">112</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/algorithms/reconstruction/RegularizedIterativeSENSEReconstruction.py">RegularizedIterativeSENSEReconstruction.py</a></td><td>41</td><td>17</td><td>59%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/algorithms/reconstruction/RegularizedIterativeSENSEReconstruction.py#L97-L101">97&ndash;101</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/algorithms/reconstruction/RegularizedIterativeSENSEReconstruction.py#L115-L140">115&ndash;140</a></td></tr><tr><td colspan="5"><b>src/mrpro/data</b></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/AcqInfo.py">AcqInfo.py</a></td><td>141</td><td>5</td><td>96%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/AcqInfo.py#L42">42</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/AcqInfo.py#L120-L121">120&ndash;121</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/AcqInfo.py#L123">123</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/AcqInfo.py#L229">229</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/CsmData.py">CsmData.py</a></td><td>40</td><td>5</td><td>88%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/CsmData.py#L16-L18">16&ndash;18</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/CsmData.py#L165-L167">165&ndash;167</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Dataclass.py">Dataclass.py</a></td><td>196</td><td>18</td><td>91%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Dataclass.py#L31">31</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Dataclass.py#L250">250</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Dataclass.py#L266">266</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Dataclass.py#L332">332</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Dataclass.py#L390-L392">390&ndash;392</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Dataclass.py#L405">405</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Dataclass.py#L487">487</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Dataclass.py#L507-L508">507&ndash;508</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Dataclass.py#L510">510</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Dataclass.py#L525-L526">525&ndash;526</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Dataclass.py#L528">528</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Dataclass.py#L562">562</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Dataclass.py#L570-L571">570&ndash;571</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/DcfData.py">DcfData.py</a></td><td>35</td><td>2</td><td>94%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/DcfData.py#L15">15</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/DcfData.py#L62">62</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/EncodingLimits.py">EncodingLimits.py</a></td><td>81</td><td>3</td><td>96%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/EncodingLimits.py#L33">33</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/EncodingLimits.py#L123">123</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/EncodingLimits.py#L126">126</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/IData.py">IData.py</a></td><td>52</td><td>2</td><td>96%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/IData.py#L119">119</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/IData.py#L133">133</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/IHeader.py">IHeader.py</a></td><td>134</td><td>12</td><td>91%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/IHeader.py#L69-L72">69&ndash;72</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/IHeader.py#L251">251</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/IHeader.py#L255">255</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/IHeader.py#L259">259</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/IHeader.py#L263">263</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/IHeader.py#L297-L301">297&ndash;301</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/KData.py">KData.py</a></td><td>201</td><td>30</td><td>85%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/KData.py#L111-L112">111&ndash;112</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/KData.py#L127">127</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/KData.py#L134">134</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/KData.py#L145-L156">145&ndash;156</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/KData.py#L165">165</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/KData.py#L173">173</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/KData.py#L182">182</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/KData.py#L216">216</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/KData.py#L238-L240">238&ndash;240</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/KData.py#L276-L277">276&ndash;277</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/KData.py#L332-L343">332&ndash;343</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/KData.py#L472">472</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/KData.py#L474">474</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/KData.py#L545">545</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/KHeader.py">KHeader.py</a></td><td>140</td><td>21</td><td>85%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/KHeader.py#L23">23</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/KHeader.py#L107-L113">107&ndash;113</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/KHeader.py#L140">140</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/KHeader.py#L188">188</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/KHeader.py#L195-L196">195&ndash;196</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/KHeader.py#L199">199</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/KHeader.py#L206">206</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/KHeader.py#L223-L230">223&ndash;230</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/KHeader.py#L238-L249">238&ndash;249</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/KNoise.py">KNoise.py</a></td><td>22</td><td>1</td><td>95%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/KNoise.py#L44">44</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/KTrajectory.py">KTrajectory.py</a></td><td>102</td><td>3</td><td>97%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/KTrajectory.py#L164">164</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/KTrajectory.py#L166">166</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/KTrajectory.py#L186">186</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/QData.py">QData.py</a></td><td>32</td><td>1</td><td>97%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/QData.py#L43">43</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py">Rotation.py</a></td><td>729</td><td>36</td><td>95%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L102">102</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L200">200</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L337">337</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L435">435</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L479">479</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L497">497</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L584">584</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L586">586</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L595">595</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L629">629</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L631">631</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L694">694</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L771">771</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L776">776</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L779">779</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L794">794</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L811">811</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L816">816</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L892">892</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L1080">1080</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L1085">1085</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L1088">1088</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L1112">1112</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L1116">1116</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L1244">1244</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L1246">1246</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L1254-L1255">1254&ndash;1255</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L1319">1319</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L1401">1401</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L1710">1710</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L1862">1862</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L1897">1897</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L1901">1901</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L2077">2077</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/Rotation.py#L2098">2098</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/SpatialDimension.py">SpatialDimension.py</a></td><td>238</td><td>19</td><td>92%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/SpatialDimension.py#L34">34</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/SpatialDimension.py#L103">103</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/SpatialDimension.py#L150">150</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/SpatialDimension.py#L156">156</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/SpatialDimension.py#L276-L278">276&ndash;278</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/SpatialDimension.py#L291-L293">291&ndash;293</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/SpatialDimension.py#L327">327</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/SpatialDimension.py#L345">345</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/SpatialDimension.py#L358">358</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/SpatialDimension.py#L371">371</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/SpatialDimension.py#L384">384</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/SpatialDimension.py#L397">397</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/SpatialDimension.py#L406-L407">406&ndash;407</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/SpatialDimension.py#L435">435</a></td></tr><tr><td colspan="5"><b>src/mrpro/data/traj_calculators</b></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/traj_calculators/KTrajectoryCalculator.py">KTrajectoryCalculator.py</a></td><td>27</td><td>1</td><td>96%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/traj_calculators/KTrajectoryCalculator.py#L84">84</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/traj_calculators/KTrajectoryCartesian.py">KTrajectoryCartesian.py</a></td><td>31</td><td>4</td><td>87%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/traj_calculators/KTrajectoryCartesian.py#L107-L110">107&ndash;110</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/traj_calculators/KTrajectoryCartesian.py#L114">114</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/traj_calculators/KTrajectoryIsmrmrd.py">KTrajectoryIsmrmrd.py</a></td><td>19</td><td>1</td><td>95%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/data/traj_calculators/KTrajectoryIsmrmrd.py#L57">57</a></td></tr><tr><td colspan="5"><b>src/mrpro/operators</b></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/AveragingOp.py">AveragingOp.py</a></td><td>34</td><td>2</td><td>94%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/AveragingOp.py#L53">53</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/AveragingOp.py#L78">78</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/CartesianSamplingOp.py">CartesianSamplingOp.py</a></td><td>97</td><td>4</td><td>96%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/CartesianSamplingOp.py#L141">141</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/CartesianSamplingOp.py#L180">180</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/CartesianSamplingOp.py#L255">255</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/CartesianSamplingOp.py#L322">322</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/ConstraintsOp.py">ConstraintsOp.py</a></td><td>60</td><td>2</td><td>97%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/ConstraintsOp.py#L46">46</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/ConstraintsOp.py#L48">48</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/EndomorphOperator.py">EndomorphOperator.py</a></td><td>32</td><td>2</td><td>94%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/EndomorphOperator.py#L52">52</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/EndomorphOperator.py#L58">58</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/FiniteDifferenceOp.py">FiniteDifferenceOp.py</a></td><td>27</td><td>2</td><td>93%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/FiniteDifferenceOp.py#L40">40</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/FiniteDifferenceOp.py#L105">105</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/FourierOp.py">FourierOp.py</a></td><td>90</td><td>3</td><td>97%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/FourierOp.py#L185">185</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/FourierOp.py#L257">257</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/FourierOp.py#L262">262</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/Functional.py">Functional.py</a></td><td>77</td><td>9</td><td>88%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/Functional.py#L20-L22">20&ndash;22</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/Functional.py#L117">117</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/Functional.py#L119">119</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/Functional.py#L226-L228">226&ndash;228</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/Functional.py#L242">242</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/GridSamplingOp.py">GridSamplingOp.py</a></td><td>136</td><td>9</td><td>93%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/GridSamplingOp.py#L71-L72">71&ndash;72</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/GridSamplingOp.py#L81-L82">81&ndash;82</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/GridSamplingOp.py#L89-L90">89&ndash;90</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/GridSamplingOp.py#L93">93</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/GridSamplingOp.py#L95">95</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/GridSamplingOp.py#L97">97</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/LinearOperator.py">LinearOperator.py</a></td><td>201</td><td>8</td><td>96%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/LinearOperator.py#L107">107</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/LinearOperator.py#L217">217</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/LinearOperator.py#L244">244</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/LinearOperator.py#L251">251</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/LinearOperator.py#L292">292</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/LinearOperator.py#L301">301</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/LinearOperator.py#L309">309</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/LinearOperator.py#L326">326</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/LinearOperatorMatrix.py">LinearOperatorMatrix.py</a></td><td>162</td><td>19</td><td>88%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/LinearOperatorMatrix.py#L82">82</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/LinearOperatorMatrix.py#L119">119</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/LinearOperatorMatrix.py#L152">152</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/LinearOperatorMatrix.py#L161">161</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/LinearOperatorMatrix.py#L166">166</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/LinearOperatorMatrix.py#L175-L178">175&ndash;178</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/LinearOperatorMatrix.py#L191-L194">191&ndash;194</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/LinearOperatorMatrix.py#L202-L203">202&ndash;203</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/LinearOperatorMatrix.py#L208-L209">208&ndash;209</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/LinearOperatorMatrix.py#L221">221</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/LinearOperatorMatrix.py#L310">310</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/LinearOperatorMatrix.py#L340">340</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/LinearOperatorMatrix.py#L367">367</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/MultiIdentityOp.py">MultiIdentityOp.py</a></td><td>13</td><td>2</td><td>85%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/MultiIdentityOp.py#L43">43</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/MultiIdentityOp.py#L48">48</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/NonUniformFastFourierOp.py">NonUniformFastFourierOp.py</a></td><td>190</td><td>10</td><td>95%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/NonUniformFastFourierOp.py#L69">69</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/NonUniformFastFourierOp.py#L96">96</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/NonUniformFastFourierOp.py#L206">206</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/NonUniformFastFourierOp.py#L208">208</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/NonUniformFastFourierOp.py#L242">242</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/NonUniformFastFourierOp.py#L244">244</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/NonUniformFastFourierOp.py#L321">321</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/NonUniformFastFourierOp.py#L379">379</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/NonUniformFastFourierOp.py#L429">429</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/NonUniformFastFourierOp.py#L434">434</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/Operator.py">Operator.py</a></td><td>79</td><td>2</td><td>97%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/Operator.py#L32">32</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/Operator.py#L88">88</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/PatchOp.py">PatchOp.py</a></td><td>47</td><td>3</td><td>94%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/PatchOp.py#L81">81</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/PatchOp.py#L112">112</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/PatchOp.py#L127">127</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/ProximableFunctionalSeparableSum.py">ProximableFunctionalSeparableSum.py</a></td><td>39</td><td>3</td><td>92%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/ProximableFunctionalSeparableSum.py#L50">50</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/ProximableFunctionalSeparableSum.py#L103">103</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/ProximableFunctionalSeparableSum.py#L110">110</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/SliceProjectionOp.py">SliceProjectionOp.py</a></td><td>178</td><td>10</td><td>94%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/SliceProjectionOp.py#L45">45</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/SliceProjectionOp.py#L62">62</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/SliceProjectionOp.py#L64">64</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/SliceProjectionOp.py#L70">70</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/SliceProjectionOp.py#L153">153</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/SliceProjectionOp.py#L179">179</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/SliceProjectionOp.py#L215">215</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/SliceProjectionOp.py#L236">236</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/SliceProjectionOp.py#L269">269</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/SliceProjectionOp.py#L309">309</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/WaveletOp.py">WaveletOp.py</a></td><td>119</td><td>5</td><td>96%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/WaveletOp.py#L151">151</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/WaveletOp.py#L169">169</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/WaveletOp.py#L204">204</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/WaveletOp.py#L209">209</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/WaveletOp.py#L232">232</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/ZeroPadOp.py">ZeroPadOp.py</a></td><td>16</td><td>1</td><td>94%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/ZeroPadOp.py#L30">30</a></td></tr><tr><td colspan="5"><b>src/mrpro/operators/functionals</b></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/functionals/SSIM.py">SSIM.py</a></td><td>71</td><td>7</td><td>90%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/functionals/SSIM.py#L60-L80">60&ndash;80</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/functionals/SSIM.py#L82">82</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/functionals/SSIM.py#L86">86</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/functionals/SSIM.py#L115">115</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/functionals/SSIM.py#L148">148</a></td></tr><tr><td colspan="5"><b>src/mrpro/operators/models</b></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/models/EPG.py">EPG.py</a></td><td>202</td><td>41</td><td>80%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/models/EPG.py#L85-L97">85&ndash;97</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/models/EPG.py#L145-L149">145&ndash;149</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/models/EPG.py#L169-L172">169&ndash;172</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/models/EPG.py#L193-L198">193&ndash;198</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/models/EPG.py#L261">261</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/models/EPG.py#L266">266</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/models/EPG.py#L282-L284">282&ndash;284</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/models/EPG.py#L304-L305">304&ndash;305</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/models/EPG.py#L310">310</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/models/EPG.py#L334">334</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/models/EPG.py#L339">339</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/models/EPG.py#L364">364</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/models/EPG.py#L369">369</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/models/EPG.py#L423">423</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/models/EPG.py#L480">480</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/models/EPG.py#L581">581</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/operators/models/EPG.py#L608">608</a></td></tr><tr><td colspan="5"><b>src/mrpro/phantoms</b></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/phantoms/brainweb.py">brainweb.py</a></td><td>276</td><td>31</td><td>89%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/phantoms/brainweb.py#L326-L336">326&ndash;336</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/phantoms/brainweb.py#L372">372</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/phantoms/brainweb.py#L428-L431">428&ndash;431</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/phantoms/brainweb.py#L453-L454">453&ndash;454</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/phantoms/brainweb.py#L459-L460">459&ndash;460</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/phantoms/brainweb.py#L462-L463">462&ndash;463</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/phantoms/brainweb.py#L467">467</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/phantoms/brainweb.py#L475">475</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/phantoms/brainweb.py#L482-L483">482&ndash;483</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/phantoms/brainweb.py#L521">521</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/phantoms/brainweb.py#L587-L588">587&ndash;588</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/phantoms/brainweb.py#L607-L610">607&ndash;610</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/phantoms/brainweb.py#L621">621</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/phantoms/brainweb.py#L623">623</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/phantoms/brainweb.py#L656-L657">656&ndash;657</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/phantoms/brainweb.py#L670">670</a></td></tr><tr><td colspan="5"><b>src/mrpro/utils</b></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/RandomGenerator.py">RandomGenerator.py</a></td><td>144</td><td>13</td><td>91%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/RandomGenerator.py#L25">25</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/RandomGenerator.py#L30">30</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/RandomGenerator.py#L32">32</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/RandomGenerator.py#L164">164</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/RandomGenerator.py#L188">188</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/RandomGenerator.py#L391">391</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/RandomGenerator.py#L409">409</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/RandomGenerator.py#L491">491</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/RandomGenerator.py#L774-L777">774&ndash;777</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/RandomGenerator.py#L840">840</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/RandomGenerator.py#L843">843</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/filters.py">filters.py</a></td><td>62</td><td>2</td><td>97%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/filters.py#L44">44</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/filters.py#L49">49</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/pad_or_crop.py">pad_or_crop.py</a></td><td>31</td><td>6</td><td>81%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/pad_or_crop.py#L26">26</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/pad_or_crop.py#L30">30</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/pad_or_crop.py#L58">58</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/pad_or_crop.py#L61">61</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/pad_or_crop.py#L64">64</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/pad_or_crop.py#L67">67</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/reshape.py">reshape.py</a></td><td>101</td><td>2</td><td>98%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/reshape.py#L111">111</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/reshape.py#L305">305</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/slice_profiles.py">slice_profiles.py</a></td><td>49</td><td>6</td><td>88%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/slice_profiles.py#L21">21</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/slice_profiles.py#L37">37</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/slice_profiles.py#L119-L122">119&ndash;122</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/slice_profiles.py#L155">155</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/split_idx.py">split_idx.py</a></td><td>10</td><td>2</td><td>80%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/split_idx.py#L43">43</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/split_idx.py#L47">47</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/summarize_tensorvalues.py">summarize_tensorvalues.py</a></td><td>12</td><td>3</td><td>75%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/summarize_tensorvalues.py#L23">23</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/summarize_tensorvalues.py#L25">25</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/summarize_tensorvalues.py#L27">27</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/typing.py">typing.py</a></td><td>73</td><td>56</td><td>23%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/typing.py#L9-L236">9&ndash;236</a></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/unit_conversion.py">unit_conversion.py</a></td><td>60</td><td>14</td><td>77%</td><td><a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/unit_conversion.py#L32">32</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/unit_conversion.py#L40">40</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/unit_conversion.py#L42">42</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/unit_conversion.py#L49">49</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/unit_conversion.py#L51">51</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/unit_conversion.py#L58">58</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/unit_conversion.py#L60">60</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/unit_conversion.py#L69">69</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/unit_conversion.py#L80">80</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/unit_conversion.py#L82">82</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/unit_conversion.py#L101">101</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/unit_conversion.py#L103">103</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/unit_conversion.py#L124">124</a>, <a href="https://github.com/PTB-MR/mrpro/blob/6f326a402eeb152a07c40a07a0d6c10eb5eaf59b/src/mrpro/utils/unit_conversion.py#L126">126</a></td></tr><tr><td><b>TOTAL</b></td><td><b>6632</b></td><td><b>502</b></td><td><b>92%</b></td><td>&nbsp;</td></tr></tbody></table></details>

| Tests | Skipped | Failures | Errors | Time |
| ----- | ------- | -------- | -------- | ------------------ |
| 2854 | 0 :zzz: | 0 :x: | 0 :fire: | 1m 52s :stopwatch: |

author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-07T11:00:43Z
body: <!-- documentation build 771 -->
### :books: Documentation
:file_folder: [Download as zip](https://github.com/PTB-MR/mrpro/actions/runs/14523612071/artifacts/2966430144)
:mag: [View online](https://zimf.de/zipserve/https://github.com/PTB-MR/mrpro/actions/runs/14523612071/artifacts/2966430144/)
author_association: CONTRIBUTOR
user: login: fzimmermann89
created_at: 2025-04-17T11:23:35Z
body: So even in the simple case where we use
![image](https://github.com/user-attachments/assets/60a085e5-88ae-4bc0-8ef6-d732b4f323ff)
 to consructed a self adjoint operator
![image](https://github.com/user-attachments/assets/ae39cf8a-5be7-4f3e-b40c-893dc5811e89)
we don have a good way to check if to sums of linear operators are adjoint to each other.... Nor if a linear operator is selfadjoint... 
Nor if it is symmetric.....

author_association: MEMBER
user: login: fzimmermann89
created_at: 2025-04-17T11:30:41Z
body: @koflera Another question: The docstring says "This implementation assumes H is self-adjoint"
Wouldnt it be better to say: "Conjugate gradient works only with self-adjoint positive semi-definite operators. This implementation does not verify these properties. The results will be wrong if the assumptions are not met"
author_association: MEMBER
user: login: koflera
created_at: 2025-04-17T12:06:00Z
body: > So even in the simple case where we use ![image](https://private-user-images.githubusercontent.com/5222871/434765834-60a085e5-88ae-4bc0-8ef6-d732b4f323ff.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDQ4OTExMzYsIm5iZiI6MTc0NDg5MDgzNiwicGF0aCI6Ii81MjIyODcxLzQzNDc2NTgzNC02MGEwODVlNS04OGFlLTRiYzAtOGVmNi1kNzMyYjRmMzIzZmYucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI1MDQxNyUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNTA0MTdUMTE1MzU2WiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9YjJiNDJiNzI2MDIzOThmZGY3Y2MxY2QwNzY1MzQ2YTM1YjA2NGE1MTliMzljYThkYTA2NWE1YWI2ZjQzMzE3NSZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.akKIE-9OgCNbqY6QrOR1g6uX1L4tBnbF_04KhHf_t2g) to consructed a self adjoint operator ![image](https://private-user-images.githubusercontent.com/5222871/434765972-ae39cf8a-5be7-4f3e-b40c-893dc5811e89.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDQ4OTExMzYsIm5iZiI6MTc0NDg5MDgzNiwicGF0aCI6Ii81MjIyODcxLzQzNDc2NTk3Mi1hZTM5Y2Y4YS01YmU3LTRmM2UtYjQwYy04OTNkYzU4MTFlODkucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI1MDQxNyUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNTA0MTdUMTE1MzU2WiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9ZDAwYWY0MzM3ZjNiZTA1YjU2MGI3NmMwOGE5NGFiMmZmYjIzN2I3YWQ2MDhjNTBiMmQzZWVlMDgwYzg4NTdjMiZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.xoZjFWsiUDxvuvnW87UzapP0v9CWNpYwylkI-n76HNQ) we don have a good way to check if to sums of linear operators are adjoint to each other.... Nor if a linear operator is selfadjoint... Nor if it is symmetric.....

No no, that's not what I meant.... what I meant is that a matrix of linear operators $M$ that is self adjoint (and thus for which it makes sense to use CG) has this somewhat general form
![grafik](https://github.com/user-attachments/assets/4a733cfc-957c-4246-92f8-f082e7120cc6)
with i) all $A_j$ and $B_j$ being self adjoint and ii) $M_{ij} = M_{ji}$ for $i\neq j$. While we don't have a method for checking if an operator is self-adjoint (i), i thought that we could maybe check (ii) by checking whether the elements in the off-diagonals are equal. Therefore, I was asking, if we have the notion of two operators being "equal".
author_association: COLLABORATOR
user: login: koflera
created_at: 2025-04-17T12:06:31Z
body: > @koflera Another question: The docstring says "This implementation assumes H is self-adjoint" Wouldnt it be better to say: "Conjugate gradient works only with self-adjoint positive semi-definite operators. This implementation does not verify these properties. The results will be wrong if the assumptions are not met"

Yes, good point. That would be much clearer.
author_association: COLLABORATOR