mrtwo 0.260212__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mr2/VERSION +1 -0
- mr2/__init__.py +12 -0
- mr2/_version.py +8 -0
- mr2/algorithms/__init__.py +6 -0
- mr2/algorithms/csm/__init__.py +5 -0
- mr2/algorithms/csm/inati.py +70 -0
- mr2/algorithms/csm/walsh.py +71 -0
- mr2/algorithms/dcf/__init__.py +4 -0
- mr2/algorithms/dcf/dcf_voronoi.py +140 -0
- mr2/algorithms/optimizers/OptimizerStatus.py +14 -0
- mr2/algorithms/optimizers/__init__.py +10 -0
- mr2/algorithms/optimizers/adam.py +137 -0
- mr2/algorithms/optimizers/bicg.py +164 -0
- mr2/algorithms/optimizers/cg.py +186 -0
- mr2/algorithms/optimizers/lbfgs.py +147 -0
- mr2/algorithms/optimizers/pdhg.py +228 -0
- mr2/algorithms/optimizers/pgd.py +177 -0
- mr2/algorithms/prewhiten_kspace.py +73 -0
- mr2/algorithms/reconstruction/DirectReconstruction.py +95 -0
- mr2/algorithms/reconstruction/IterativeSENSEReconstruction.py +81 -0
- mr2/algorithms/reconstruction/Reconstruction.py +144 -0
- mr2/algorithms/reconstruction/RegularizedIterativeSENSEReconstruction.py +162 -0
- mr2/algorithms/reconstruction/TotalVariationRegularizedReconstruction.py +153 -0
- mr2/algorithms/reconstruction/__init__.py +14 -0
- mr2/algorithms/total_variation_denoising.py +106 -0
- mr2/data/AcqInfo.py +422 -0
- mr2/data/CsmData.py +235 -0
- mr2/data/Dataclass.py +1096 -0
- mr2/data/DcfData.py +70 -0
- mr2/data/EncodingLimits.py +175 -0
- mr2/data/IData.py +398 -0
- mr2/data/IHeader.py +381 -0
- mr2/data/KData.py +627 -0
- mr2/data/KHeader.py +326 -0
- mr2/data/KNoise.py +50 -0
- mr2/data/KTrajectory.py +257 -0
- mr2/data/QData.py +62 -0
- mr2/data/QHeader.py +79 -0
- mr2/data/Rotation.py +2205 -0
- mr2/data/SpatialDimension.py +437 -0
- mr2/data/__init__.py +42 -0
- mr2/data/acq_filters.py +80 -0
- mr2/data/enums.py +131 -0
- mr2/data/traj_calculators/KTrajectoryCalculator.py +110 -0
- mr2/data/traj_calculators/KTrajectoryCartesian.py +153 -0
- mr2/data/traj_calculators/KTrajectoryIsmrmrd.py +65 -0
- mr2/data/traj_calculators/KTrajectoryPulseq.py +64 -0
- mr2/data/traj_calculators/KTrajectoryRadial2D.py +56 -0
- mr2/data/traj_calculators/KTrajectoryRpe.py +124 -0
- mr2/data/traj_calculators/KTrajectorySpiral2D.py +145 -0
- mr2/data/traj_calculators/KTrajectorySunflowerGoldenRpe.py +96 -0
- mr2/data/traj_calculators/__init__.py +20 -0
- mr2/nn/AbsolutePositionEncoding.py +74 -0
- mr2/nn/AxialRoPE.py +106 -0
- mr2/nn/ComplexAsChannel.py +59 -0
- mr2/nn/CondMixin.py +22 -0
- mr2/nn/DropPath.py +55 -0
- mr2/nn/FiLM.py +68 -0
- mr2/nn/FourierFeatures.py +50 -0
- mr2/nn/GEGLU.py +56 -0
- mr2/nn/GroupNorm.py +71 -0
- mr2/nn/LayerNorm.py +85 -0
- mr2/nn/PermutedBlock.py +75 -0
- mr2/nn/PixelShuffle.py +285 -0
- mr2/nn/RMSNorm.py +73 -0
- mr2/nn/ResBlock.py +71 -0
- mr2/nn/Residual.py +45 -0
- mr2/nn/SeparableResBlock.py +89 -0
- mr2/nn/Sequential.py +60 -0
- mr2/nn/Upsample.py +66 -0
- mr2/nn/__init__.py +59 -0
- mr2/nn/attention/AttentionGate.py +74 -0
- mr2/nn/attention/LinearSelfAttention.py +98 -0
- mr2/nn/attention/MultiHeadAttention.py +106 -0
- mr2/nn/attention/NeighborhoodSelfAttention.py +237 -0
- mr2/nn/attention/ShiftedWindowAttention.py +131 -0
- mr2/nn/attention/SpatialTransformerBlock.py +216 -0
- mr2/nn/attention/SqueezeExcitation.py +57 -0
- mr2/nn/attention/TransposedAttention.py +76 -0
- mr2/nn/attention/__init__.py +17 -0
- mr2/nn/convert_linear_conv.py +100 -0
- mr2/nn/data_consistency/AnalyticCartesianDC.py +101 -0
- mr2/nn/data_consistency/ConjugateGradientDC.py +117 -0
- mr2/nn/data_consistency/GradientDescentDC.py +99 -0
- mr2/nn/data_consistency/__init__.py +5 -0
- mr2/nn/join.py +176 -0
- mr2/nn/ndmodules.py +178 -0
- mr2/nn/nets/BasicCNN.py +105 -0
- mr2/nn/nets/DiT.py +266 -0
- mr2/nn/nets/HourglassTransformer.py +147 -0
- mr2/nn/nets/MLP.py +119 -0
- mr2/nn/nets/Restormer.py +223 -0
- mr2/nn/nets/SwinIR.py +247 -0
- mr2/nn/nets/UNet.py +364 -0
- mr2/nn/nets/Uformer.py +230 -0
- mr2/nn/nets/VAE.py +142 -0
- mr2/nn/nets/__init__.py +22 -0
- mr2/operators/AveragingOp.py +123 -0
- mr2/operators/CartesianSamplingOp.py +387 -0
- mr2/operators/ConjugateGradientOp.py +234 -0
- mr2/operators/ConstraintsOp.py +255 -0
- mr2/operators/DensityCompensationOp.py +44 -0
- mr2/operators/DictionaryMatchOp.py +156 -0
- mr2/operators/EinsumOp.py +103 -0
- mr2/operators/EndomorphOperator.py +77 -0
- mr2/operators/FastFourierOp.py +166 -0
- mr2/operators/FiniteDifferenceOp.py +143 -0
- mr2/operators/FourierOp.py +320 -0
- mr2/operators/Functional.py +292 -0
- mr2/operators/GridSamplingOp.py +452 -0
- mr2/operators/IdentityOp.py +58 -0
- mr2/operators/Jacobian.py +122 -0
- mr2/operators/LinearOperator.py +673 -0
- mr2/operators/LinearOperatorMatrix.py +408 -0
- mr2/operators/MagnitudeOp.py +35 -0
- mr2/operators/MultiIdentityOp.py +63 -0
- mr2/operators/NonUniformFastFourierOp.py +472 -0
- mr2/operators/Operator.py +323 -0
- mr2/operators/OptimizerOp.py +192 -0
- mr2/operators/PCACompressionOp.py +103 -0
- mr2/operators/PatchOp.py +192 -0
- mr2/operators/PhaseOp.py +35 -0
- mr2/operators/ProximableFunctionalSeparableSum.py +232 -0
- mr2/operators/RearrangeOp.py +84 -0
- mr2/operators/SensitivityOp.py +73 -0
- mr2/operators/SignalModel.py +13 -0
- mr2/operators/SliceProjectionOp.py +504 -0
- mr2/operators/WaveletOp.py +395 -0
- mr2/operators/ZeroOp.py +94 -0
- mr2/operators/ZeroPadOp.py +87 -0
- mr2/operators/__init__.py +78 -0
- mr2/operators/functionals/L1Norm.py +110 -0
- mr2/operators/functionals/L1NormViewAsReal.py +99 -0
- mr2/operators/functionals/L2NormSquared.py +119 -0
- mr2/operators/functionals/MSE.py +82 -0
- mr2/operators/functionals/SSIM.py +259 -0
- mr2/operators/functionals/ZeroFunctional.py +97 -0
- mr2/operators/functionals/__init__.py +7 -0
- mr2/operators/models/EPG.py +861 -0
- mr2/operators/models/InversionRecovery.py +61 -0
- mr2/operators/models/MOLLI.py +76 -0
- mr2/operators/models/MonoExponentialDecay.py +63 -0
- mr2/operators/models/SaturationRecovery.py +62 -0
- mr2/operators/models/SpoiledGRE.py +76 -0
- mr2/operators/models/TransientSteadyStateWithPreparation.py +151 -0
- mr2/operators/models/WASABI.py +114 -0
- mr2/operators/models/WASABITI.py +105 -0
- mr2/operators/models/__init__.py +25 -0
- mr2/operators/models/cMRF.py +131 -0
- mr2/phantoms/EllipsePhantom.py +141 -0
- mr2/phantoms/__init__.py +20 -0
- mr2/phantoms/brainweb.py +725 -0
- mr2/phantoms/coils.py +96 -0
- mr2/phantoms/fastmri.py +215 -0
- mr2/phantoms/m4raw.py +152 -0
- mr2/phantoms/mdcnn.py +162 -0
- mr2/phantoms/phantom_elements.py +14 -0
- mr2/py.typed +0 -0
- mr2/utils/RandomGenerator.py +910 -0
- mr2/utils/TensorAttributeMixin.py +26 -0
- mr2/utils/__init__.py +54 -0
- mr2/utils/ema.py +95 -0
- mr2/utils/fill_range.py +23 -0
- mr2/utils/filters.py +169 -0
- mr2/utils/indexing.py +370 -0
- mr2/utils/interpolate.py +93 -0
- mr2/utils/pad_or_crop.py +82 -0
- mr2/utils/reduce_repeat.py +47 -0
- mr2/utils/reshape.py +531 -0
- mr2/utils/slice_profiles.py +158 -0
- mr2/utils/sliding_window.py +91 -0
- mr2/utils/smap.py +47 -0
- mr2/utils/split_idx.py +60 -0
- mr2/utils/summarize.py +109 -0
- mr2/utils/to_tuple.py +36 -0
- mr2/utils/typing.py +264 -0
- mr2/utils/unit_conversion.py +147 -0
- mr2/utils/vmf.py +64 -0
- mrtwo-0.260212.dist-info/METADATA +151 -0
- mrtwo-0.260212.dist-info/RECORD +183 -0
- mrtwo-0.260212.dist-info/WHEEL +5 -0
- mrtwo-0.260212.dist-info/licenses/LICENSE +202 -0
- mrtwo-0.260212.dist-info/top_level.txt +1 -0
mr2/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.260212
|
mr2/__init__.py
ADDED
mr2/_version.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# This file is used to get the version of the package from the VERSION file
|
|
2
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version('mrtwo')
|
|
7
|
+
except PackageNotFoundError:
|
|
8
|
+
__version__ = Path(__file__).parent.joinpath('VERSION').read_text().strip()
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Algorithms for reconstructions, optimization, density and sensitivity map estimation, etc."""
|
|
2
|
+
|
|
3
|
+
from mr2.algorithms import csm, optimizers, reconstruction, dcf
|
|
4
|
+
from mr2.algorithms.prewhiten_kspace import prewhiten_kspace
|
|
5
|
+
from mr2.algorithms.total_variation_denoising import total_variation_denoising
|
|
6
|
+
__all__ = ["csm", "dcf", "optimizers", "prewhiten_kspace", "reconstruction", "total_variation_denoising"]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Inati method for coil sensitivity map calculation."""
|
|
2
|
+
|
|
3
|
+
import torch
|
|
4
|
+
from einops import einsum
|
|
5
|
+
|
|
6
|
+
from mr2.data.SpatialDimension import SpatialDimension
|
|
7
|
+
from mr2.utils.sliding_window import sliding_window
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def inati(
|
|
11
|
+
coil_img: torch.Tensor,
|
|
12
|
+
smoothing_width: SpatialDimension[int] | int,
|
|
13
|
+
) -> torch.Tensor:
|
|
14
|
+
"""Calculate a coil sensitivity map (csm) using the Inati method [INA2013]_ [INA2014]_.
|
|
15
|
+
|
|
16
|
+
This is for a single set of coil images. The input should be a tensor with dimensions `(coils, z, y, x)`. The output
|
|
17
|
+
will have the same dimensions. Either apply this function individually to each set of coil images, or see
|
|
18
|
+
`~mr2.data.CsmData.from_idata_inati` which performs this operation on a whole dataset.
|
|
19
|
+
|
|
20
|
+
.. [INA2013] Inati S, Hansen M, Kellman P (2013) A solution to the phase problem in adaptvie coil combination.
|
|
21
|
+
in Proceedings of the 21st Annual Meeting of ISMRM, Salt Lake City, USA, 2672.
|
|
22
|
+
|
|
23
|
+
.. [INA2014] Inati S, Hansen M (2014) A Fast Optimal Method for Coil Sensitivity Estimation and Adaptive Coil
|
|
24
|
+
Combination for Complex images. in Proceedings of Joint Annual Meeting ISMRM-ESMRMB, Milan, Italy, 7115.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
coil_img
|
|
29
|
+
images for each coil element
|
|
30
|
+
smoothing_width
|
|
31
|
+
Size of the smoothing kernel
|
|
32
|
+
"""
|
|
33
|
+
# After 10 power iterations we will have a very good estimate of the singular vector
|
|
34
|
+
n_power_iterations = 10
|
|
35
|
+
eps = 1e-8 # for numerical stability
|
|
36
|
+
|
|
37
|
+
if isinstance(smoothing_width, int):
|
|
38
|
+
smoothing_width = SpatialDimension(
|
|
39
|
+
z=smoothing_width if coil_img.shape[-3] > 1 else 1, y=smoothing_width, x=smoothing_width
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if any(ks % 2 != 1 for ks in [smoothing_width.z, smoothing_width.y, smoothing_width.x]):
|
|
43
|
+
raise ValueError('kernel_size must be odd')
|
|
44
|
+
|
|
45
|
+
ks_halved = [ks // 2 for ks in smoothing_width.zyx]
|
|
46
|
+
padded_coil_img = torch.nn.functional.pad(
|
|
47
|
+
coil_img,
|
|
48
|
+
(ks_halved[-1], ks_halved[-1], ks_halved[-2], ks_halved[-2], ks_halved[-3], ks_halved[-3]),
|
|
49
|
+
mode='replicate',
|
|
50
|
+
)
|
|
51
|
+
# Get the voxels in an ROI defined by the smoothing_width around each voxel leading to shape
|
|
52
|
+
# (z y x coils window=prod(smoothing_width))
|
|
53
|
+
coil_img_roi = sliding_window(padded_coil_img, smoothing_width.zyx, dim=(-3, -2, -1)).flatten(-3)
|
|
54
|
+
coil_img_cov = einsum(
|
|
55
|
+
coil_img_roi.conj(),
|
|
56
|
+
coil_img_roi,
|
|
57
|
+
'... coils1 window,... coils2 window->... coils1 coils2',
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
singular_vector = torch.sum(coil_img_roi, dim=-1) # z y x coils
|
|
61
|
+
singular_vector /= singular_vector.norm(dim=-1, keepdim=True) + eps
|
|
62
|
+
for _ in range(n_power_iterations):
|
|
63
|
+
singular_vector = einsum(coil_img_cov, singular_vector, '... coils1 coils2,... coils2->... coils1')
|
|
64
|
+
singular_vector /= singular_vector.norm(dim=-1, keepdim=True) + eps
|
|
65
|
+
|
|
66
|
+
singular_value = einsum(coil_img_roi, singular_vector, '... coils window,... coils->... window')
|
|
67
|
+
phase = singular_value.sum(-1)
|
|
68
|
+
phase /= phase.abs() + eps
|
|
69
|
+
csm = einsum(singular_vector.conj(), phase, '... coils,...->coils ...') # coils z y x
|
|
70
|
+
return csm
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""(Iterative) Walsh method for coil sensitivity map calculation."""
|
|
2
|
+
|
|
3
|
+
import torch
|
|
4
|
+
|
|
5
|
+
from mr2.data.SpatialDimension import SpatialDimension
|
|
6
|
+
from mr2.utils.filters import uniform_filter
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def walsh(coil_images: torch.Tensor, smoothing_width: SpatialDimension[int] | int) -> torch.Tensor:
|
|
10
|
+
"""Calculate a coil sensitivity map (csm) using an iterative version of the Walsh method.
|
|
11
|
+
|
|
12
|
+
This function computes CSMs from a set of complex coil images assuming spatially
|
|
13
|
+
slowly changing sensitivity maps using Walsh's method [WAL2000]_.
|
|
14
|
+
|
|
15
|
+
The algorithm follows these steps:
|
|
16
|
+
|
|
17
|
+
1. **Compute Pointwise Covariance**:
|
|
18
|
+
Calculate the covariance matrix of the coil images at each voxel to capture inter-coil signal relationships.
|
|
19
|
+
|
|
20
|
+
2. **Apply Smoothing Filter**:
|
|
21
|
+
Smooth the covariance matrices across spatial dimensions using a uniform filter of specified width
|
|
22
|
+
to reduce noise and enforce spatial consistency.
|
|
23
|
+
|
|
24
|
+
3. **Dominant Eigenvector Estimation via Power Iteration**:
|
|
25
|
+
Perform power iterations to approximate the dominant eigenvector of the covariance matrix at each voxel,
|
|
26
|
+
representing the principal component of the signal.
|
|
27
|
+
|
|
28
|
+
4. **Normalize Sensitivity Maps**:
|
|
29
|
+
Normalize the resulting eigenvectors to produce the final CSMs.
|
|
30
|
+
|
|
31
|
+
This function works on a single set of coil images. The input should be a tensor with dimensions
|
|
32
|
+
`(coils, z, y, x)`. The output will have the same dimensions. Either apply this function individually to each set of
|
|
33
|
+
coil images, or see `~mr2.data.CsmData.from_idata_walsh` which performs this operation on a whole dataset
|
|
34
|
+
[WAL2000]_.
|
|
35
|
+
|
|
36
|
+
This implementation is inspired by `ismrmrd-python-tools <https://github.com/ismrmrd/ismrmrd-python-tools>`_.
|
|
37
|
+
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
coil_images
|
|
41
|
+
images for each coil element
|
|
42
|
+
smoothing_width
|
|
43
|
+
width of the smoothing filter
|
|
44
|
+
|
|
45
|
+
References
|
|
46
|
+
----------
|
|
47
|
+
.. [WAL2000] Walsh DO, Gmitro AF, Marcellin MW (2000) Adaptive reconstruction of phased array MR imagery. MRM 43
|
|
48
|
+
"""
|
|
49
|
+
# After 10 power iterations we will have a very good estimate of the singular vector
|
|
50
|
+
n_power_iterations = 10
|
|
51
|
+
|
|
52
|
+
if isinstance(smoothing_width, int):
|
|
53
|
+
smoothing_width = SpatialDimension(smoothing_width, smoothing_width, smoothing_width)
|
|
54
|
+
# Compute the pointwise covariance between coils
|
|
55
|
+
coil_covariance = torch.einsum('azyx,bzyx->abzyx', coil_images, coil_images.conj())
|
|
56
|
+
|
|
57
|
+
# Smooth the covariance along y-x for 2D and z-y-x for 3D data
|
|
58
|
+
coil_covariance = uniform_filter(coil_covariance, width=smoothing_width.zyx, dim=(-3, -2, -1))
|
|
59
|
+
|
|
60
|
+
# At each point in the image, find the dominant eigenvector
|
|
61
|
+
# of the signal covariance matrix using the power method
|
|
62
|
+
v = coil_covariance.sum(dim=0)
|
|
63
|
+
for _ in range(n_power_iterations):
|
|
64
|
+
v /= v.norm(dim=0)
|
|
65
|
+
v = torch.einsum('abzyx,bzyx->azyx', coil_covariance, v)
|
|
66
|
+
csm = v / v.norm(dim=0)
|
|
67
|
+
|
|
68
|
+
# Make sure there are no inf or nan-values due to very small values in the covariance matrix
|
|
69
|
+
# nan_to_num does not work for complexfloat, boolean indexing not with vmap.
|
|
70
|
+
csm = torch.where(torch.isfinite(csm), csm, 0.0)
|
|
71
|
+
return csm
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""1D, 2D and 3D density compensation function calculation with voronoi method."""
|
|
2
|
+
|
|
3
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
4
|
+
from itertools import product
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import torch
|
|
8
|
+
from numpy.typing import ArrayLike
|
|
9
|
+
from scipy.spatial import ConvexHull, Voronoi
|
|
10
|
+
|
|
11
|
+
UNIQUE_ROUNDING_DECIMALS = 15
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _volume(v: ArrayLike):
|
|
15
|
+
return ConvexHull(v).volume
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def dcf_1d(traj: torch.Tensor) -> torch.Tensor:
|
|
19
|
+
"""Calculate sample density compensation function for 1D trajectory.
|
|
20
|
+
|
|
21
|
+
This function operates on a single `other` sample.
|
|
22
|
+
See also `~mr2.data.DcfData` and `~mr2.utils.smap`.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
traj
|
|
27
|
+
k-space positions, 1D tensor
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
density compensation values
|
|
32
|
+
"""
|
|
33
|
+
traj_sorted, inverse, counts = torch.unique(
|
|
34
|
+
torch.round(traj, decimals=UNIQUE_ROUNDING_DECIMALS),
|
|
35
|
+
sorted=True,
|
|
36
|
+
return_inverse=True,
|
|
37
|
+
return_counts=True,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# For a sorted trajectory: x0 x1 x2 ... xN
|
|
41
|
+
# we assign the point at x1 the area (x1 - x0) / 2 + (x2 - x1) / 2,
|
|
42
|
+
# this is done by central differences (-1,0,1).
|
|
43
|
+
# For the edges, we append/prepend the values afterwards, such that:
|
|
44
|
+
# we assign the point at x0 the area (x1 - x0) / 2 + (x1 - x0) / 2, and
|
|
45
|
+
# we assign the point at xN the area (xN - xN-1) / 2 + (xN - xN-1) / 2.
|
|
46
|
+
|
|
47
|
+
kernel = torch.tensor([-1 / 2, 0, 1 / 2], dtype=torch.float32, device=traj.device).reshape(1, 1, 3)
|
|
48
|
+
|
|
49
|
+
if (elements := len(traj_sorted)) >= 3:
|
|
50
|
+
central_diff = torch.nn.functional.conv1d(traj_sorted[None, None, :], kernel)[0, 0]
|
|
51
|
+
first = traj_sorted[1] - traj_sorted[0]
|
|
52
|
+
last = traj_sorted[-1] - traj_sorted[-2]
|
|
53
|
+
central_diff = torch.cat((first[None], central_diff, last[None]), -1)
|
|
54
|
+
elif elements == 2:
|
|
55
|
+
diff = traj_sorted[1] - traj_sorted[0]
|
|
56
|
+
central_diff = torch.cat((diff[None], diff[None]), -1)
|
|
57
|
+
else:
|
|
58
|
+
central_diff = torch.ones_like(traj_sorted)
|
|
59
|
+
|
|
60
|
+
# Repeated points are reduced by the number of repeats
|
|
61
|
+
dcf = torch.nan_to_num(central_diff / counts)[inverse]
|
|
62
|
+
return dcf
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def dcf_2d3d_voronoi(traj: torch.Tensor) -> torch.Tensor:
|
|
66
|
+
"""Calculate sample density compensation function using Voronoi method.
|
|
67
|
+
|
|
68
|
+
This function computes the DCF by determining the area around each point in k-space using the Voronoi tessellation.
|
|
69
|
+
Points at the edge of k-space are detected as outliers and are assigned the area of the 1% largest DCF values.
|
|
70
|
+
|
|
71
|
+
The Voronoi tessellation assigns each point in k-space a region based on the proximity to its nearest neighbors. The
|
|
72
|
+
DCF is then computed based on the inverse of the area of these regions.
|
|
73
|
+
|
|
74
|
+
This function operates on a single `other` sample.
|
|
75
|
+
See also `~mr2.data.DcfData` and `~mr2.utils.smap`.
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
traj
|
|
80
|
+
k-space positions `(2 or 3, k2, k1, k0)`
|
|
81
|
+
|
|
82
|
+
Returns
|
|
83
|
+
-------
|
|
84
|
+
density compensation values `(1, k2, k1, k0)`
|
|
85
|
+
"""
|
|
86
|
+
# 2D and 3D trajectories supported
|
|
87
|
+
dim = traj.shape[0]
|
|
88
|
+
if dim not in (2, 3):
|
|
89
|
+
raise ValueError(f'Only 2D or 3D trajectories supported, not {dim}D.')
|
|
90
|
+
|
|
91
|
+
# Calculate dcf only for unique k-space positions
|
|
92
|
+
traj_dim = traj.shape
|
|
93
|
+
traj_numpy = np.round(traj.cpu().numpy(), decimals=UNIQUE_ROUNDING_DECIMALS)
|
|
94
|
+
traj_numpy = traj_numpy.reshape(dim, -1)
|
|
95
|
+
traj_unique, inverse, counts = np.unique(traj_numpy, return_inverse=True, return_counts=True, axis=1)
|
|
96
|
+
|
|
97
|
+
# Especially in 3D, errors in the calculation of the convex hull can occur for edge points. To avoid this,
|
|
98
|
+
# the corner points of a cube bounding box are added here. The bounding box is chosen very large to ensure these
|
|
99
|
+
# edge points of the trajectory can still be accurately detected in the outlier detection further down.
|
|
100
|
+
furthest_corner = np.max(np.abs(traj_unique))
|
|
101
|
+
corner_points = np.array(list(product([-1, 1], repeat=dim))) * furthest_corner * 10
|
|
102
|
+
traj_extendend = np.concatenate((traj_unique, corner_points.transpose()), axis=1)
|
|
103
|
+
|
|
104
|
+
# Carry out voronoi tessellation
|
|
105
|
+
vdiagram = Voronoi(traj_extendend.transpose())
|
|
106
|
+
regions = [vdiagram.regions[r] for r in vdiagram.point_region[: -len(corner_points)]] # Ignore corner points
|
|
107
|
+
vertices = [vdiagram.vertices[region] for region in regions]
|
|
108
|
+
|
|
109
|
+
if dim == 2:
|
|
110
|
+
# Shoelace equation for 2d
|
|
111
|
+
def cross2d(x: np.ndarray, y: np.ndarray) -> np.ndarray:
|
|
112
|
+
return x[..., 0] * y[..., 1] - x[..., 1] * y[..., 0]
|
|
113
|
+
|
|
114
|
+
dcf = np.array([np.abs(cross2d(v[:-1], v[1:]).sum(0) + cross2d(v[-1], v[0])) / 2 for v in vertices])
|
|
115
|
+
|
|
116
|
+
else:
|
|
117
|
+
# Calculate volume/area of voronoi cells using processes, as this is a very time-consuming operation
|
|
118
|
+
# and ConvexHull is singlethreaded and does not seem to drop the GIL
|
|
119
|
+
# TODO: this could maybe be made faster as the polyhedrons are known to be convex
|
|
120
|
+
future = ProcessPoolExecutor(max_workers=torch.get_num_threads()).map(_volume, vertices, chunksize=100)
|
|
121
|
+
dcf = np.array(list(future))
|
|
122
|
+
|
|
123
|
+
# Get outliers (i.e. voronoi cell which are unbound) and set them to a reasonable value
|
|
124
|
+
# Outliers are defined as values larger than 1.5 * inter quartile range of the values
|
|
125
|
+
# Outliers are set to the average of the 1% largest values.
|
|
126
|
+
dcf_sorted = np.sort(dcf)
|
|
127
|
+
q1, q3 = np.percentile(dcf_sorted, [25, 75])
|
|
128
|
+
iqr = q3 - q1
|
|
129
|
+
upper_bound = q3 + 1.5 * iqr
|
|
130
|
+
idx_outliers = np.nonzero(dcf > upper_bound)
|
|
131
|
+
n_outliers = len(idx_outliers[0])
|
|
132
|
+
high_values_start = int(0.99 * (len(dcf_sorted) - n_outliers))
|
|
133
|
+
high_values_end = len(dcf_sorted) - n_outliers # this works also for n_outliers==0
|
|
134
|
+
fill_value = np.average(dcf_sorted[high_values_start:high_values_end])
|
|
135
|
+
dcf[idx_outliers] = fill_value
|
|
136
|
+
|
|
137
|
+
# Sort dcf values back into the original order (i.e. before calling unique)
|
|
138
|
+
dcf = np.reshape((dcf / counts)[inverse], traj_dim[1:])
|
|
139
|
+
|
|
140
|
+
return torch.tensor(dcf, dtype=torch.float32, device=traj.device)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Optimizer Status base class."""
|
|
2
|
+
|
|
3
|
+
import torch
|
|
4
|
+
from typing_extensions import TypedDict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class OptimizerStatus(TypedDict):
|
|
8
|
+
"""Base class for OptimizerStatus."""
|
|
9
|
+
|
|
10
|
+
solution: tuple[torch.Tensor, ...]
|
|
11
|
+
"""Current estimate(s) of the solution. """
|
|
12
|
+
|
|
13
|
+
iteration_number: int
|
|
14
|
+
"""Current iteration of the (iterative) algorithm."""
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Optimizers."""
|
|
2
|
+
|
|
3
|
+
from mr2.algorithms.optimizers.OptimizerStatus import OptimizerStatus
|
|
4
|
+
from mr2.algorithms.optimizers.adam import adam
|
|
5
|
+
from mr2.algorithms.optimizers.bicg import bicg
|
|
6
|
+
from mr2.algorithms.optimizers.cg import cg
|
|
7
|
+
from mr2.algorithms.optimizers.lbfgs import lbfgs
|
|
8
|
+
from mr2.algorithms.optimizers.pdhg import pdhg
|
|
9
|
+
from mr2.algorithms.optimizers.pgd import pgd
|
|
10
|
+
__all__ = ["OptimizerStatus", "adam", "bicg", "cg", "lbfgs", "pdhg", "pgd"]
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""ADAM for solving non-linear minimization problems."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Sequence
|
|
4
|
+
|
|
5
|
+
import torch
|
|
6
|
+
from torch.optim import Adam, AdamW
|
|
7
|
+
|
|
8
|
+
from mr2.algorithms.optimizers.OptimizerStatus import OptimizerStatus
|
|
9
|
+
from mr2.operators.Operator import OperatorType
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def adam(
|
|
13
|
+
f: OperatorType,
|
|
14
|
+
initial_parameters: Sequence[torch.Tensor],
|
|
15
|
+
*,
|
|
16
|
+
max_iterations: int = 100,
|
|
17
|
+
learning_rate: float = 1e-3,
|
|
18
|
+
tolerance_change: float = 0,
|
|
19
|
+
betas: tuple[float, float] = (0.9, 0.999),
|
|
20
|
+
eps: float = 1e-8,
|
|
21
|
+
weight_decay: float = 0,
|
|
22
|
+
amsgrad: bool = False,
|
|
23
|
+
decoupled_weight_decay: bool = False,
|
|
24
|
+
callback: Callable[[OptimizerStatus], bool | None] | None = None,
|
|
25
|
+
) -> tuple[torch.Tensor, ...]:
|
|
26
|
+
r"""Adam for non-linear minimization problems.
|
|
27
|
+
|
|
28
|
+
Adam [KING2015]_ (Adaptive Moment Estimation) is a first-order optimization algorithm that adapts learning rates
|
|
29
|
+
for each parameter using estimates of the first and second moments of the gradients.
|
|
30
|
+
|
|
31
|
+
The parameter update rule is:
|
|
32
|
+
|
|
33
|
+
.. math::
|
|
34
|
+
|
|
35
|
+
m_t &= \beta_1 m_{t-1} + (1 - \beta_1) g_t \\
|
|
36
|
+
v_t &= \beta_2 v_{t-1} + (1 - \beta_2) g_t^2 \\
|
|
37
|
+
\hat{m}_t &= \frac{m_t}{1 - \beta_1^t}, \quad \hat{v}_t = \frac{v_t}{1 - \beta_2^t} \\
|
|
38
|
+
\theta_{t+1} &= \theta_t - \frac{\eta}{\sqrt{\hat{v}_t} + \epsilon} \hat{m}_t
|
|
39
|
+
|
|
40
|
+
where
|
|
41
|
+
:math:`g_t` is the gradient at step :math:`t`,
|
|
42
|
+
:math:`m_t` and :math:`v_t` are biased estimates of the first and second moments,
|
|
43
|
+
:math:`\hat{m}_t` and :math:`\hat{v}_t` are bias-corrected estimates,
|
|
44
|
+
:math:`\eta` is the learning rate,
|
|
45
|
+
:math:`\epsilon` is a small constant for numerical stability,
|
|
46
|
+
:math:`\beta_1` and :math:`\beta_2` are decay rates for the moment estimates.
|
|
47
|
+
|
|
48
|
+
Steps of the Adam algorithm:
|
|
49
|
+
|
|
50
|
+
1. Initialize parameters and moment estimates (:math:`m_0`, :math:`v_0`).
|
|
51
|
+
2. Compute the gradient of the objective function.
|
|
52
|
+
3. Compute bias-corrected estimates of the moments :math:`\hat{m}_t` and :math:`\hat{v}_t`.
|
|
53
|
+
4. Update parameters using the adaptive step size.
|
|
54
|
+
|
|
55
|
+
This function wraps PyTorch's :class:`torch.optim.Adam` and :class:`torch.optim.AdamW` implementations,
|
|
56
|
+
supporting both standard Adam and decoupled weight decay regularization (AdamW) [LOS2019]_
|
|
57
|
+
|
|
58
|
+
References
|
|
59
|
+
----------
|
|
60
|
+
.. [KING2015] Kingma DP, Ba J (2015) Adam: A Method for Stochastic Optimization. ICLR.
|
|
61
|
+
https://doi.org/10.48550/arXiv.1412.6980
|
|
62
|
+
.. [LOS2019] Loshchilov I, Hutter F (2019) Decoupled Weight Decay Regularization. ICLR.
|
|
63
|
+
https://doi.org/10.48550/arXiv.1711.05101
|
|
64
|
+
.. [REDDI2019] Sashank J. Reddi, Satyen Kale, Sanjiv Kumar (2019) On the Convergence of Adam and Beyond. ICLR.
|
|
65
|
+
https://doi.org/10.48550/arXiv.1904.09237
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
f
|
|
70
|
+
scalar-valued function to be optimized.
|
|
71
|
+
initial_parameters
|
|
72
|
+
sequence (for example list) of parameters to be optimized.
|
|
73
|
+
Note that these parameters will not be changed. Instead, we create a copy and
|
|
74
|
+
leave the initial values untouched.
|
|
75
|
+
max_iterations
|
|
76
|
+
maximum number of iterations.
|
|
77
|
+
learning_rate
|
|
78
|
+
learning rate.
|
|
79
|
+
tolerance_change
|
|
80
|
+
teriminate if the change of objective function is smaller than this tolerance.
|
|
81
|
+
betas
|
|
82
|
+
coefficients used for computing running averages of gradient and its square.
|
|
83
|
+
eps
|
|
84
|
+
term added to the denominator to improve numerical stability.
|
|
85
|
+
weight_decay
|
|
86
|
+
weight decay (L2 penalty if `decoupled_weight_decay` is `False`).
|
|
87
|
+
amsgrad
|
|
88
|
+
whether to use the AMSGrad variant [REDDI2019]_.
|
|
89
|
+
decoupled_weight_decay
|
|
90
|
+
whether to use Adam (default) or AdamW (if set to `True`) [LOS2019]_.
|
|
91
|
+
callback
|
|
92
|
+
function to be called after each iteration. This can be used to monitor the progress of the algorithm.
|
|
93
|
+
If it returns `False`, the algorithm stops at that iteration.
|
|
94
|
+
|
|
95
|
+
Returns
|
|
96
|
+
-------
|
|
97
|
+
optimized parameters
|
|
98
|
+
"""
|
|
99
|
+
parameters = tuple(p.detach().clone().requires_grad_(True) for p in initial_parameters)
|
|
100
|
+
|
|
101
|
+
optim: AdamW | Adam
|
|
102
|
+
|
|
103
|
+
if not decoupled_weight_decay:
|
|
104
|
+
optim = Adam(
|
|
105
|
+
params=parameters, lr=learning_rate, betas=betas, eps=eps, weight_decay=weight_decay, amsgrad=amsgrad
|
|
106
|
+
)
|
|
107
|
+
else:
|
|
108
|
+
optim = AdamW(
|
|
109
|
+
params=parameters, lr=learning_rate, betas=betas, eps=eps, weight_decay=weight_decay, amsgrad=amsgrad
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def closure():
|
|
113
|
+
optim.zero_grad()
|
|
114
|
+
(objective,) = f(*parameters)
|
|
115
|
+
objective.backward()
|
|
116
|
+
|
|
117
|
+
return objective
|
|
118
|
+
|
|
119
|
+
old_objective = float('inf')
|
|
120
|
+
|
|
121
|
+
for iteration in range(max_iterations):
|
|
122
|
+
objective = optim.step(closure)
|
|
123
|
+
|
|
124
|
+
if tolerance_change > 0:
|
|
125
|
+
objective_ = objective.item()
|
|
126
|
+
change = old_objective - objective_
|
|
127
|
+
if change < tolerance_change:
|
|
128
|
+
break
|
|
129
|
+
old_objective = objective_
|
|
130
|
+
|
|
131
|
+
if callback is not None:
|
|
132
|
+
continue_iterations = callback({'solution': parameters, 'iteration_number': iteration})
|
|
133
|
+
if continue_iterations is False:
|
|
134
|
+
break
|
|
135
|
+
|
|
136
|
+
parameters = tuple(p.detach() for p in parameters)
|
|
137
|
+
return parameters
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Stabilized Bi-Conjugate Gradient method for non-symmetric linear systems."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Sequence
|
|
4
|
+
from warnings import warn
|
|
5
|
+
|
|
6
|
+
import torch
|
|
7
|
+
from typing_extensions import TypedDict, Unpack
|
|
8
|
+
|
|
9
|
+
from mr2.operators import MultiIdentityOp
|
|
10
|
+
from mr2.operators.LinearOperator import LinearOperator
|
|
11
|
+
from mr2.operators.LinearOperatorMatrix import LinearOperatorMatrix
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def vdot(a: Sequence[torch.Tensor], b: Sequence[torch.Tensor], force_real: bool = False) -> torch.Tensor:
|
|
15
|
+
"""Vector dot product."""
|
|
16
|
+
if force_real:
|
|
17
|
+
a = tuple(torch.view_as_real(a_i) if a_i.is_complex() else a_i for a_i in a)
|
|
18
|
+
b = tuple(torch.view_as_real(b_i) if b_i.is_complex() else b_i for b_i in b)
|
|
19
|
+
return sum(
|
|
20
|
+
(torch.vdot(a_i.flatten(), b_i.flatten()) for a_i, b_i in zip(a, b, strict=True)), start=torch.tensor(0.0)
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
OperatorMatrixLikeCallable = Callable[[Unpack[tuple[torch.Tensor, ...]]], tuple[torch.Tensor, ...]]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class BiCGStatus(TypedDict):
|
|
28
|
+
"""BiCG callback status."""
|
|
29
|
+
|
|
30
|
+
solution: tuple[torch.Tensor, ...]
|
|
31
|
+
iteration_number: int
|
|
32
|
+
residual: tuple[torch.Tensor, ...]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def bicg(
|
|
36
|
+
operator: LinearOperator | LinearOperatorMatrix | OperatorMatrixLikeCallable,
|
|
37
|
+
right_hand_side: Sequence[torch.Tensor],
|
|
38
|
+
*,
|
|
39
|
+
initial_value: Sequence[torch.Tensor] | None = None,
|
|
40
|
+
preconditioner_inverse: LinearOperator | LinearOperatorMatrix | OperatorMatrixLikeCallable | None = None,
|
|
41
|
+
max_iterations: int = 128,
|
|
42
|
+
tolerance: float = 1e-6,
|
|
43
|
+
callback: Callable[[BiCGStatus], bool | None] | None = None,
|
|
44
|
+
) -> tuple[torch.Tensor, ...]:
|
|
45
|
+
r"""(Preconditioned) Bi-Conjugate Gradient Stabilized method for :math:`Hx=b`.
|
|
46
|
+
|
|
47
|
+
This algorithm solves systems of the form :math:`H x = b`, where :math:`H` is a
|
|
48
|
+
linear operator (potentially non-symmetric) and :math:`b` is the right-hand side.
|
|
49
|
+
The method can solve multiple systems jointly if the operator and tensors handle batch dimensions.
|
|
50
|
+
|
|
51
|
+
The method performs the following steps (simplified):
|
|
52
|
+
|
|
53
|
+
1. Initialize :math:`x_0`, compute residual :math:`r_0 = b - Hx_0`.
|
|
54
|
+
Set shadow residual :math:`\hat{r}_0 = r_0`.
|
|
55
|
+
2. Initialize :math:`\rho_0 = \alpha = \omega = 1`, :math:`p_0 = v_0 = 0`.
|
|
56
|
+
3. For iteration :math:`k = 0, 1, 2, ...`:
|
|
57
|
+
1. Compute inner product: :math:`\rho_k = \hat{r}_0^H r_{k-1}`.
|
|
58
|
+
2. Compute step direction modifier: :math:`\beta_{k-1} = (\rho_k / \rho_{k-1}) (\alpha_{k-1} / \omega_{k-1})`.
|
|
59
|
+
3. Update search direction: :math:`p_k = r_{k-1} + \beta_{k-1} (p_{k-1} - \omega_{k-1} v_{k-1})`.
|
|
60
|
+
4. Apply preconditioner: :math:`y_k = M^{-1} p_k`.
|
|
61
|
+
5. Compute operator applied to search direction: :math:`v_k = H y_k`.
|
|
62
|
+
6. Compute step size :math:`\alpha_k = \rho_k / (\hat{r}_0^H v_k)`.
|
|
63
|
+
7. Compute intermediate residual: :math:`s_k = r_{k-1} - \alpha_k v_k`.
|
|
64
|
+
8. Check convergence: If :math:`\|s_k\|_2` is small, update :math:`x_k = x_{k-1} + \alpha_k y_k` and stop.
|
|
65
|
+
9. Apply preconditioner to intermediate residual: :math:`z_k = M^{-1} s_k`.
|
|
66
|
+
10. Compute operator applied to preconditioned intermediate residual: :math:`t_k = H z_k`.
|
|
67
|
+
11. Compute stabilization factor: :math:`\omega_k = (t_k^H s_k) / (t_k^H t_k)`.
|
|
68
|
+
12. Update solution: :math:`x_k = x_{k-1} + \alpha_k y_k + \omega_k z_k`.
|
|
69
|
+
13. Update residual: :math:`r_k = s_k - \omega_k t_k`.
|
|
70
|
+
14. Update :math:`\rho_{k-1} = \rho_k` for the next iteration.
|
|
71
|
+
|
|
72
|
+
If `preconditioner_inverse` is provided, it solves :math:`M^{-1}Hx = M^{-1}b`
|
|
73
|
+
implicitly, where `preconditioner_inverse(r)` computes :math:`M^{-1}r`.
|
|
74
|
+
|
|
75
|
+
See [VanDerVorst1992]_ and [WikipediaBiCGSTAB]_ for more details.
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
operator
|
|
80
|
+
Linear operator :math:`H`. Does not need to be self-adjoint.
|
|
81
|
+
right_hand_side
|
|
82
|
+
Right-hand-side :math:`b`. Must be a sequence of tensors.
|
|
83
|
+
initial_value
|
|
84
|
+
Initial guess :math:`x_0`. If None, defaults to zeros. Must be a sequence of tensors.
|
|
85
|
+
preconditioner_inverse
|
|
86
|
+
Preconditioner :math:`M^{-1}`. If None, no preconditioning is applied (identity operator).
|
|
87
|
+
max_iterations
|
|
88
|
+
Maximum number of iterations.
|
|
89
|
+
tolerance
|
|
90
|
+
Tolerance for the L2 norm of the residual :math:`\|r_k\|_2`.
|
|
91
|
+
callback
|
|
92
|
+
Function called at the end of each iteration. See `BiCGSTABStatus`.
|
|
93
|
+
If the callback returns `False`, the iteration stops.
|
|
94
|
+
|
|
95
|
+
Returns
|
|
96
|
+
-------
|
|
97
|
+
An approximate solution :math:`x` of the linear system :math:`Hx=b` as a tuple of tensors.
|
|
98
|
+
|
|
99
|
+
References
|
|
100
|
+
----------
|
|
101
|
+
.. [VanDerVorst1992] Van der Vorst, H. A. (1992). Bi-CGSTAB: A Fast and Smoothly Converging Variant of Bi-CG for the
|
|
102
|
+
Solution of Nonsymmetric Linear Systems. SIAM Journal on Scientific and Statistical Computing, 13(2), 631-644.
|
|
103
|
+
.. [WikipediaBiCGSTAB] Wikipedia: Bi-conjugate gradient stabilized method https://en.wikipedia.org/wiki/Bi-conjugate_gradient_stabilized_method
|
|
104
|
+
"""
|
|
105
|
+
right_hand_side_ = tuple(right_hand_side)
|
|
106
|
+
if tolerance < torch.finfo(right_hand_side[0].dtype).eps:
|
|
107
|
+
warn(f'Tolerance is very small ({tolerance}), this can cause nan values.', stacklevel=2)
|
|
108
|
+
if not right_hand_side_:
|
|
109
|
+
raise ValueError('right_hand_side cannot be empty')
|
|
110
|
+
|
|
111
|
+
if initial_value is None:
|
|
112
|
+
solution = tuple(torch.zeros_like(r) for r in right_hand_side_)
|
|
113
|
+
else:
|
|
114
|
+
solution = tuple(initial_value)
|
|
115
|
+
if len(solution) != len(right_hand_side_):
|
|
116
|
+
raise ValueError('Length mismatch in initial_value and right_hand_side')
|
|
117
|
+
if any(s.shape != r.shape for s, r in zip(solution, right_hand_side_, strict=True)):
|
|
118
|
+
raise ValueError('Shape mismatch in initial_value and right_hand_side')
|
|
119
|
+
if any(r.is_complex() for r in right_hand_side_) and not all(r.is_complex() for r in right_hand_side_):
|
|
120
|
+
force_real = True
|
|
121
|
+
else:
|
|
122
|
+
force_real = False
|
|
123
|
+
residual = tuple(r - op_sol for r, op_sol in zip(right_hand_side_, operator(*solution), strict=True))
|
|
124
|
+
arbitrary = tuple(r for r in residual)
|
|
125
|
+
|
|
126
|
+
preconditioner = preconditioner_inverse if preconditioner_inverse is not None else MultiIdentityOp()
|
|
127
|
+
alpha = omega = previous_product = operator_search = None
|
|
128
|
+
search_direction = residual
|
|
129
|
+
|
|
130
|
+
for iteration in range(max_iterations):
|
|
131
|
+
if vdot(residual, residual, force_real).real < tolerance**2:
|
|
132
|
+
break
|
|
133
|
+
|
|
134
|
+
arbitrary_dot_residual = vdot(arbitrary, residual, force_real)
|
|
135
|
+
if (
|
|
136
|
+
previous_product is not None and alpha is not None and omega is not None and operator_search is not None
|
|
137
|
+
): # not first iteration
|
|
138
|
+
beta = (arbitrary_dot_residual / previous_product) * (alpha / omega)
|
|
139
|
+
search_direction = tuple(
|
|
140
|
+
r + beta * (p - omega * v) for r, p, v in zip(residual, search_direction, operator_search, strict=True)
|
|
141
|
+
)
|
|
142
|
+
preconditioned_search = preconditioner(*search_direction)
|
|
143
|
+
operator_search = operator(*preconditioned_search)
|
|
144
|
+
alpha = arbitrary_dot_residual / vdot(arbitrary, operator_search, force_real)
|
|
145
|
+
|
|
146
|
+
interim = tuple(r - alpha * v for r, v in zip(residual, operator_search, strict=True))
|
|
147
|
+
preconditioned_interim = preconditioner(*interim)
|
|
148
|
+
operator_interim = operator(*preconditioned_interim)
|
|
149
|
+
omega = vdot(operator_interim, interim, force_real) / vdot(operator_interim, operator_interim, force_real).real
|
|
150
|
+
|
|
151
|
+
solution = tuple(
|
|
152
|
+
x + alpha * p + omega * s
|
|
153
|
+
for x, p, s in zip(solution, preconditioned_search, preconditioned_interim, strict=True)
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
residual = tuple(i - omega * t for i, t in zip(interim, operator_interim, strict=True))
|
|
157
|
+
previous_product = arbitrary_dot_residual
|
|
158
|
+
|
|
159
|
+
if callback is not None:
|
|
160
|
+
continue_iterations = callback({'solution': solution, 'iteration_number': iteration, 'residual': residual})
|
|
161
|
+
if continue_iterations is False:
|
|
162
|
+
break
|
|
163
|
+
|
|
164
|
+
return solution
|