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.
Files changed (183) hide show
  1. mr2/VERSION +1 -0
  2. mr2/__init__.py +12 -0
  3. mr2/_version.py +8 -0
  4. mr2/algorithms/__init__.py +6 -0
  5. mr2/algorithms/csm/__init__.py +5 -0
  6. mr2/algorithms/csm/inati.py +70 -0
  7. mr2/algorithms/csm/walsh.py +71 -0
  8. mr2/algorithms/dcf/__init__.py +4 -0
  9. mr2/algorithms/dcf/dcf_voronoi.py +140 -0
  10. mr2/algorithms/optimizers/OptimizerStatus.py +14 -0
  11. mr2/algorithms/optimizers/__init__.py +10 -0
  12. mr2/algorithms/optimizers/adam.py +137 -0
  13. mr2/algorithms/optimizers/bicg.py +164 -0
  14. mr2/algorithms/optimizers/cg.py +186 -0
  15. mr2/algorithms/optimizers/lbfgs.py +147 -0
  16. mr2/algorithms/optimizers/pdhg.py +228 -0
  17. mr2/algorithms/optimizers/pgd.py +177 -0
  18. mr2/algorithms/prewhiten_kspace.py +73 -0
  19. mr2/algorithms/reconstruction/DirectReconstruction.py +95 -0
  20. mr2/algorithms/reconstruction/IterativeSENSEReconstruction.py +81 -0
  21. mr2/algorithms/reconstruction/Reconstruction.py +144 -0
  22. mr2/algorithms/reconstruction/RegularizedIterativeSENSEReconstruction.py +162 -0
  23. mr2/algorithms/reconstruction/TotalVariationRegularizedReconstruction.py +153 -0
  24. mr2/algorithms/reconstruction/__init__.py +14 -0
  25. mr2/algorithms/total_variation_denoising.py +106 -0
  26. mr2/data/AcqInfo.py +422 -0
  27. mr2/data/CsmData.py +235 -0
  28. mr2/data/Dataclass.py +1096 -0
  29. mr2/data/DcfData.py +70 -0
  30. mr2/data/EncodingLimits.py +175 -0
  31. mr2/data/IData.py +398 -0
  32. mr2/data/IHeader.py +381 -0
  33. mr2/data/KData.py +627 -0
  34. mr2/data/KHeader.py +326 -0
  35. mr2/data/KNoise.py +50 -0
  36. mr2/data/KTrajectory.py +257 -0
  37. mr2/data/QData.py +62 -0
  38. mr2/data/QHeader.py +79 -0
  39. mr2/data/Rotation.py +2205 -0
  40. mr2/data/SpatialDimension.py +437 -0
  41. mr2/data/__init__.py +42 -0
  42. mr2/data/acq_filters.py +80 -0
  43. mr2/data/enums.py +131 -0
  44. mr2/data/traj_calculators/KTrajectoryCalculator.py +110 -0
  45. mr2/data/traj_calculators/KTrajectoryCartesian.py +153 -0
  46. mr2/data/traj_calculators/KTrajectoryIsmrmrd.py +65 -0
  47. mr2/data/traj_calculators/KTrajectoryPulseq.py +64 -0
  48. mr2/data/traj_calculators/KTrajectoryRadial2D.py +56 -0
  49. mr2/data/traj_calculators/KTrajectoryRpe.py +124 -0
  50. mr2/data/traj_calculators/KTrajectorySpiral2D.py +145 -0
  51. mr2/data/traj_calculators/KTrajectorySunflowerGoldenRpe.py +96 -0
  52. mr2/data/traj_calculators/__init__.py +20 -0
  53. mr2/nn/AbsolutePositionEncoding.py +74 -0
  54. mr2/nn/AxialRoPE.py +106 -0
  55. mr2/nn/ComplexAsChannel.py +59 -0
  56. mr2/nn/CondMixin.py +22 -0
  57. mr2/nn/DropPath.py +55 -0
  58. mr2/nn/FiLM.py +68 -0
  59. mr2/nn/FourierFeatures.py +50 -0
  60. mr2/nn/GEGLU.py +56 -0
  61. mr2/nn/GroupNorm.py +71 -0
  62. mr2/nn/LayerNorm.py +85 -0
  63. mr2/nn/PermutedBlock.py +75 -0
  64. mr2/nn/PixelShuffle.py +285 -0
  65. mr2/nn/RMSNorm.py +73 -0
  66. mr2/nn/ResBlock.py +71 -0
  67. mr2/nn/Residual.py +45 -0
  68. mr2/nn/SeparableResBlock.py +89 -0
  69. mr2/nn/Sequential.py +60 -0
  70. mr2/nn/Upsample.py +66 -0
  71. mr2/nn/__init__.py +59 -0
  72. mr2/nn/attention/AttentionGate.py +74 -0
  73. mr2/nn/attention/LinearSelfAttention.py +98 -0
  74. mr2/nn/attention/MultiHeadAttention.py +106 -0
  75. mr2/nn/attention/NeighborhoodSelfAttention.py +237 -0
  76. mr2/nn/attention/ShiftedWindowAttention.py +131 -0
  77. mr2/nn/attention/SpatialTransformerBlock.py +216 -0
  78. mr2/nn/attention/SqueezeExcitation.py +57 -0
  79. mr2/nn/attention/TransposedAttention.py +76 -0
  80. mr2/nn/attention/__init__.py +17 -0
  81. mr2/nn/convert_linear_conv.py +100 -0
  82. mr2/nn/data_consistency/AnalyticCartesianDC.py +101 -0
  83. mr2/nn/data_consistency/ConjugateGradientDC.py +117 -0
  84. mr2/nn/data_consistency/GradientDescentDC.py +99 -0
  85. mr2/nn/data_consistency/__init__.py +5 -0
  86. mr2/nn/join.py +176 -0
  87. mr2/nn/ndmodules.py +178 -0
  88. mr2/nn/nets/BasicCNN.py +105 -0
  89. mr2/nn/nets/DiT.py +266 -0
  90. mr2/nn/nets/HourglassTransformer.py +147 -0
  91. mr2/nn/nets/MLP.py +119 -0
  92. mr2/nn/nets/Restormer.py +223 -0
  93. mr2/nn/nets/SwinIR.py +247 -0
  94. mr2/nn/nets/UNet.py +364 -0
  95. mr2/nn/nets/Uformer.py +230 -0
  96. mr2/nn/nets/VAE.py +142 -0
  97. mr2/nn/nets/__init__.py +22 -0
  98. mr2/operators/AveragingOp.py +123 -0
  99. mr2/operators/CartesianSamplingOp.py +387 -0
  100. mr2/operators/ConjugateGradientOp.py +234 -0
  101. mr2/operators/ConstraintsOp.py +255 -0
  102. mr2/operators/DensityCompensationOp.py +44 -0
  103. mr2/operators/DictionaryMatchOp.py +156 -0
  104. mr2/operators/EinsumOp.py +103 -0
  105. mr2/operators/EndomorphOperator.py +77 -0
  106. mr2/operators/FastFourierOp.py +166 -0
  107. mr2/operators/FiniteDifferenceOp.py +143 -0
  108. mr2/operators/FourierOp.py +320 -0
  109. mr2/operators/Functional.py +292 -0
  110. mr2/operators/GridSamplingOp.py +452 -0
  111. mr2/operators/IdentityOp.py +58 -0
  112. mr2/operators/Jacobian.py +122 -0
  113. mr2/operators/LinearOperator.py +673 -0
  114. mr2/operators/LinearOperatorMatrix.py +408 -0
  115. mr2/operators/MagnitudeOp.py +35 -0
  116. mr2/operators/MultiIdentityOp.py +63 -0
  117. mr2/operators/NonUniformFastFourierOp.py +472 -0
  118. mr2/operators/Operator.py +323 -0
  119. mr2/operators/OptimizerOp.py +192 -0
  120. mr2/operators/PCACompressionOp.py +103 -0
  121. mr2/operators/PatchOp.py +192 -0
  122. mr2/operators/PhaseOp.py +35 -0
  123. mr2/operators/ProximableFunctionalSeparableSum.py +232 -0
  124. mr2/operators/RearrangeOp.py +84 -0
  125. mr2/operators/SensitivityOp.py +73 -0
  126. mr2/operators/SignalModel.py +13 -0
  127. mr2/operators/SliceProjectionOp.py +504 -0
  128. mr2/operators/WaveletOp.py +395 -0
  129. mr2/operators/ZeroOp.py +94 -0
  130. mr2/operators/ZeroPadOp.py +87 -0
  131. mr2/operators/__init__.py +78 -0
  132. mr2/operators/functionals/L1Norm.py +110 -0
  133. mr2/operators/functionals/L1NormViewAsReal.py +99 -0
  134. mr2/operators/functionals/L2NormSquared.py +119 -0
  135. mr2/operators/functionals/MSE.py +82 -0
  136. mr2/operators/functionals/SSIM.py +259 -0
  137. mr2/operators/functionals/ZeroFunctional.py +97 -0
  138. mr2/operators/functionals/__init__.py +7 -0
  139. mr2/operators/models/EPG.py +861 -0
  140. mr2/operators/models/InversionRecovery.py +61 -0
  141. mr2/operators/models/MOLLI.py +76 -0
  142. mr2/operators/models/MonoExponentialDecay.py +63 -0
  143. mr2/operators/models/SaturationRecovery.py +62 -0
  144. mr2/operators/models/SpoiledGRE.py +76 -0
  145. mr2/operators/models/TransientSteadyStateWithPreparation.py +151 -0
  146. mr2/operators/models/WASABI.py +114 -0
  147. mr2/operators/models/WASABITI.py +105 -0
  148. mr2/operators/models/__init__.py +25 -0
  149. mr2/operators/models/cMRF.py +131 -0
  150. mr2/phantoms/EllipsePhantom.py +141 -0
  151. mr2/phantoms/__init__.py +20 -0
  152. mr2/phantoms/brainweb.py +725 -0
  153. mr2/phantoms/coils.py +96 -0
  154. mr2/phantoms/fastmri.py +215 -0
  155. mr2/phantoms/m4raw.py +152 -0
  156. mr2/phantoms/mdcnn.py +162 -0
  157. mr2/phantoms/phantom_elements.py +14 -0
  158. mr2/py.typed +0 -0
  159. mr2/utils/RandomGenerator.py +910 -0
  160. mr2/utils/TensorAttributeMixin.py +26 -0
  161. mr2/utils/__init__.py +54 -0
  162. mr2/utils/ema.py +95 -0
  163. mr2/utils/fill_range.py +23 -0
  164. mr2/utils/filters.py +169 -0
  165. mr2/utils/indexing.py +370 -0
  166. mr2/utils/interpolate.py +93 -0
  167. mr2/utils/pad_or_crop.py +82 -0
  168. mr2/utils/reduce_repeat.py +47 -0
  169. mr2/utils/reshape.py +531 -0
  170. mr2/utils/slice_profiles.py +158 -0
  171. mr2/utils/sliding_window.py +91 -0
  172. mr2/utils/smap.py +47 -0
  173. mr2/utils/split_idx.py +60 -0
  174. mr2/utils/summarize.py +109 -0
  175. mr2/utils/to_tuple.py +36 -0
  176. mr2/utils/typing.py +264 -0
  177. mr2/utils/unit_conversion.py +147 -0
  178. mr2/utils/vmf.py +64 -0
  179. mrtwo-0.260212.dist-info/METADATA +151 -0
  180. mrtwo-0.260212.dist-info/RECORD +183 -0
  181. mrtwo-0.260212.dist-info/WHEEL +5 -0
  182. mrtwo-0.260212.dist-info/licenses/LICENSE +202 -0
  183. mrtwo-0.260212.dist-info/top_level.txt +1 -0
mr2/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.260212
mr2/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ from mr2._version import __version__
2
+ from mr2 import algorithms, operators, data, phantoms, utils, nn
3
+
4
+ __all__ = [
5
+ "__version__",
6
+ "algorithms",
7
+ "data",
8
+ "nn",
9
+ "operators",
10
+ "phantoms",
11
+ "utils"
12
+ ]
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,5 @@
1
+ """Coil Sensitivity Estimation."""
2
+
3
+ from mr2.algorithms.csm.walsh import walsh
4
+ from mr2.algorithms.csm.inati import inati
5
+ __all__ = ["inati", "walsh"]
@@ -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,4 @@
1
+ """Density Compensation Calculation."""
2
+
3
+ from mr2.algorithms.dcf.dcf_voronoi import dcf_1d, dcf_2d3d_voronoi
4
+ __all__ = ["dcf_1d", "dcf_2d3d_voronoi"]
@@ -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