augmentrum 0.0.1__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 (42) hide show
  1. augmentrum/__init__.py +23 -0
  2. augmentrum/augmentation/__init__.py +32 -0
  3. augmentrum/augmentation/amplitude_scaling.py +170 -0
  4. augmentrum/augmentation/apodization.py +275 -0
  5. augmentrum/augmentation/artificial_peaks.py +334 -0
  6. augmentrum/augmentation/baseline_augmentation.py +544 -0
  7. augmentrum/augmentation/eddy_current.py +278 -0
  8. augmentrum/augmentation/gaussian_noise.py +211 -0
  9. augmentrum/augmentation/line_broadening.py +179 -0
  10. augmentrum/augmentation/phase_frequency.py +259 -0
  11. augmentrum/augmentation/residual_water.py +264 -0
  12. augmentrum/augmentation/spatial_augmentations.py +859 -0
  13. augmentrum/augmentation/spurious_echoes.py +378 -0
  14. augmentrum/augmentation/zero_fill.py +163 -0
  15. augmentrum/core/__init__.py +10 -0
  16. augmentrum/core/augmentrum.py +1172 -0
  17. augmentrum/core/base_module.py +423 -0
  18. augmentrum/core/dataset_utils.py +417 -0
  19. augmentrum/core/nifti_mrs_plus.py +1079 -0
  20. augmentrum/core/pipeline.py +358 -0
  21. augmentrum/dataset/brainbeats.py +537 -0
  22. augmentrum/dataset/cows.py +302 -0
  23. augmentrum/dataset/fmrsinpain.py +208 -0
  24. augmentrum/processing/bicubic_interpolator.py +296 -0
  25. augmentrum/processing/nifti_raw_processor.py +389 -0
  26. augmentrum/processing/tricubic_interpolator.py +293 -0
  27. augmentrum/processing/utils.py +414 -0
  28. augmentrum/resources/__init__.py +14 -0
  29. augmentrum/resources/scans/__init__.py +0 -0
  30. augmentrum/sampling/coil_average_sampler.py +274 -0
  31. augmentrum/sampling/kspace_reconstructor.py +275 -0
  32. augmentrum/sampling/kspace_sampling.py +2562 -0
  33. augmentrum/sampling/subject_splitter.py +131 -0
  34. augmentrum/utils/__init__.py +10 -0
  35. augmentrum/utils/philips.py +90 -0
  36. augmentrum/utils/plotting.py +651 -0
  37. augmentrum/utils/tensor_ops.py +203 -0
  38. augmentrum-0.0.1.dist-info/METADATA +189 -0
  39. augmentrum-0.0.1.dist-info/RECORD +42 -0
  40. augmentrum-0.0.1.dist-info/WHEEL +5 -0
  41. augmentrum-0.0.1.dist-info/licenses/LICENSE +202 -0
  42. augmentrum-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,259 @@
1
+ ####################################################################################################
2
+ # phase_frequency.py #
3
+ ####################################################################################################
4
+ # #
5
+ # Authors: K. C. Igwe (kci2104@columbia.edu) #
6
+ # J. P. Merkofer (j.p.merkofer@tue.nl) #
7
+ # #
8
+ # Created: 2026-02-07 #
9
+ # #
10
+ # Purpose: Implements zero-order phase, first-order phase, and frequency shift augmentations. #
11
+ # #
12
+ ####################################################################################################
13
+
14
+ import math
15
+ import numpy as np
16
+ from typing import Optional, List
17
+
18
+ from augmentrum.core.base_module import BaseModule
19
+ from augmentrum.utils.tensor_ops import fft, ifft, fftshift, ifftshift, match_backend
20
+
21
+
22
+ class PhaseShift(BaseModule):
23
+ """
24
+ Apply phase shifts to MRS data.
25
+
26
+ Supports:
27
+ - Zero-order phase: Constant phase shift across entire spectrum
28
+ - First-order phase: Linear phase ramp across spectrum
29
+
30
+ Parameters
31
+ ----------
32
+ zero_order_deg : float
33
+ Zero-order phase shift in degrees (default: 0.0)
34
+ first_order_deg : float
35
+ First-order phase shift in degrees (default: 0.0)
36
+ Applied as linear ramp from left to right edge
37
+
38
+ Examples
39
+ --------
40
+ >>> # Zero-order phase only
41
+ >>> phase = PhaseShift(zero_order_deg=60.0)
42
+ >>>
43
+ >>> # First-order phase only
44
+ >>> phase = PhaseShift(first_order_deg=90.0)
45
+ >>>
46
+ >>> # Both
47
+ >>> phase = PhaseShift(zero_order_deg=30.0, first_order_deg=45.0)
48
+ """
49
+
50
+ SUPPORTED_BACKENDS = [] # Supports all backends
51
+
52
+ def __init__(self, zero_order_deg: float = 0.0, first_order_deg: float = 0.0):
53
+ """Initialize phase shift module."""
54
+ super().__init__(zero_order_deg=zero_order_deg, first_order_deg=first_order_deg)
55
+
56
+ self.zero_order_deg = zero_order_deg
57
+ self.first_order_deg = first_order_deg
58
+
59
+ def process_nifti_list(self, data_list: List, water_list: Optional[List] = None, **kwargs):
60
+ """
61
+ Apply phase shift to list of NIFTI_MRS objects.
62
+
63
+ Args:
64
+ data_list: List of NIFTI_MRS objects
65
+ water_list: Optional list of water reference NIFTI_MRS objects
66
+ **kwargs: Additional arguments
67
+
68
+ Returns:
69
+ Tuple of (processed_data_list, processed_water_list)
70
+ """
71
+ processed_data = []
72
+
73
+ for nifti in data_list:
74
+ # Get FID data
75
+ fid = nifti[:]
76
+
77
+ # Get spectral width (for first-order phase)
78
+ sw_hz = 1.0 / nifti.dwelltime
79
+
80
+ # Apply phase shifts
81
+ fid_phased = self._apply_phase(fid, sw_hz)
82
+
83
+ # Update NIFTI_MRS data
84
+ nifti[:] = fid_phased
85
+ processed_data.append(nifti)
86
+
87
+ return processed_data, water_list
88
+
89
+ def process_tensor(self, data_array, water_array=None, backend=None, **kwargs):
90
+ """
91
+ Apply phase shift to tensor/array data (**any backend**).
92
+
93
+ Uses ``keras.ops`` — works with NumPy, PyTorch, JAX, or TensorFlow.
94
+ Gradients are preserved.
95
+
96
+ Args:
97
+ data_array: Input tensor of shape ``(batch, ..., n_points)``
98
+ water_array: Optional water reference tensor (unchanged)
99
+ backend: Backend enum (unused — ops dispatch automatically)
100
+ **kwargs: Must contain ``'sw_hz'`` (spectral width in Hz)
101
+
102
+ Returns:
103
+ Tuple of (processed_data, water_array)
104
+ """
105
+ sw_hz = kwargs.get('sw_hz', 1.0)
106
+ result = self._apply_phase(data_array, sw_hz)
107
+ return result, water_array
108
+
109
+ def _apply_phase(self, fid, sw_hz: float):
110
+ """
111
+ Apply phase shifts to FID data (any backend tensor).
112
+
113
+ Zero-order phase is a scalar complex multiplication (fully vectorised).
114
+ First-order phase requires FFT → ramp → IFFT (uses tensor_ops).
115
+ """
116
+ # Zero-order: fully vectorised scalar multiply
117
+ if self.zero_order_deg != 0.0:
118
+ phi = math.radians(self.zero_order_deg)
119
+ # complex scalar * any-backend tensor — works everywhere
120
+ phase_factor = np.exp(-1j * phi)
121
+ fid = fid * match_backend(np.array(phase_factor), fid)
122
+
123
+ # First-order: needs spectral domain
124
+ if self.first_order_deg != 0.0:
125
+ fid = self._first_order_phase(fid, self.first_order_deg)
126
+
127
+ return fid
128
+
129
+ @staticmethod
130
+ def _zero_order_phase(fid, phase_deg: float):
131
+ """Apply zero-order phase shift (any backend tensor)."""
132
+ phi_rad = math.radians(phase_deg)
133
+ factor = np.array(np.exp(-1j * phi_rad))
134
+ return fid * match_backend(factor, fid)
135
+
136
+ @staticmethod
137
+ def _first_order_phase(fid, phc1_deg: float):
138
+ """
139
+ Apply first-order phase shift (any backend tensor).
140
+
141
+ Uses backend-agnostic FFT from tensor_ops.
142
+ The linear phase ramp is a numpy array; multiplication with the
143
+ spectrum tensor auto-promotes to the correct backend.
144
+ """
145
+ # FFT to spectrum (backend-agnostic)
146
+ spec = fftshift(ifft(fid))
147
+ N = fid.shape[-1]
148
+
149
+ # Linear ramp (numpy — no gradients needed for coordinates)
150
+ u = np.linspace(0.0, 1.0, N, dtype=np.float64)
151
+ ramp_shape = [1] * (len(fid.shape) - 1) + [N]
152
+ ramp = np.exp(1j * np.deg2rad(phc1_deg * u)).reshape(ramp_shape)
153
+
154
+ # Apply ramp and back to FID (backend-agnostic)
155
+ return fft(ifftshift(spec * match_backend(ramp, spec)))
156
+
157
+
158
+ class FrequencyShift(BaseModule):
159
+ """
160
+ Apply frequency shift to MRS data.
161
+
162
+ Shifts the entire spectrum by a specified frequency offset in Hz.
163
+ Typical range: [-40, 40] Hz, safe mode: [-20, 20] Hz.
164
+
165
+ Parameters
166
+ ----------
167
+ shift_hz : float
168
+ Frequency shift in Hz (positive = upfield shift)
169
+
170
+ Examples
171
+ --------
172
+ >>> # Shift by +10 Hz
173
+ >>> freq_shift = FrequencyShift(shift_hz=10.0)
174
+ >>>
175
+ >>> # Shift by -20 Hz
176
+ >>> freq_shift = FrequencyShift(shift_hz=-20.0)
177
+ """
178
+
179
+ SUPPORTED_BACKENDS = [] # Supports all backends
180
+
181
+ def __init__(self, shift_hz: float = 0.0):
182
+ """Initialize frequency shift module."""
183
+ super().__init__(shift_hz=shift_hz)
184
+
185
+ self.shift_hz = shift_hz
186
+
187
+ def process_nifti_list(self, data_list: List, water_list: Optional[List] = None, **kwargs):
188
+ """
189
+ Apply frequency shift to list of NIFTI_MRS objects.
190
+
191
+ Args:
192
+ data_list: List of NIFTI_MRS objects
193
+ water_list: Optional list of water reference NIFTI_MRS objects
194
+ **kwargs: Additional arguments
195
+
196
+ Returns:
197
+ Tuple of (processed_data_list, processed_water_list)
198
+ """
199
+ processed_data = []
200
+
201
+ for nifti in data_list:
202
+ # Get FID data
203
+ fid = nifti[:]
204
+
205
+ # Get spectral width
206
+ sw_hz = 1.0 / nifti.dwelltime
207
+
208
+ # Apply frequency shift
209
+ fid_shifted = self._apply_shift(fid, sw_hz)
210
+
211
+ # Update NIFTI_MRS data
212
+ nifti[:] = fid_shifted
213
+ processed_data.append(nifti)
214
+
215
+ return processed_data, water_list
216
+
217
+ def process_tensor(self, data_array, water_array=None, backend=None, **kwargs):
218
+ """
219
+ Apply frequency shift to tensor/array data (**any backend**).
220
+
221
+ Uses ``keras.ops`` — works with NumPy, PyTorch, JAX, or TensorFlow.
222
+
223
+ Args:
224
+ data_array: Input tensor of shape ``(batch, ..., n_points)``
225
+ water_array: Optional water reference tensor (unchanged)
226
+ backend: Backend enum (unused — ops dispatch automatically)
227
+ **kwargs: Must contain ``'sw_hz'`` (spectral width in Hz)
228
+
229
+ Returns:
230
+ Tuple of (processed_data, water_array)
231
+ """
232
+ sw_hz = kwargs.get('sw_hz')
233
+ if sw_hz is None:
234
+ raise ValueError("FrequencyShift.process_tensor requires 'sw_hz' in kwargs")
235
+
236
+ result = self._apply_shift(data_array, sw_hz)
237
+ return result, water_array
238
+
239
+ def _apply_shift(self, fid, sw_hz: float):
240
+ """
241
+ Apply frequency shift to FID data (any backend tensor).
242
+
243
+ The shift phasor is a numpy array; multiplication with the FID tensor
244
+ auto-promotes to the correct backend.
245
+ """
246
+ if self.shift_hz == 0.0:
247
+ return fid
248
+
249
+ N = fid.shape[-1]
250
+
251
+ # Time axis (numpy — no gradients needed for coordinates)
252
+ t = np.arange(N, dtype=np.float64) / float(sw_hz)
253
+ t = t.reshape([1] * (len(fid.shape) - 1) + [N])
254
+
255
+ # Shift phasor (numpy complex)
256
+ shift_factor = np.exp(1j * 2.0 * math.pi * float(self.shift_hz) * t)
257
+
258
+ # Multiply: convert phasor to same backend, preserves gradients
259
+ return fid * match_backend(shift_factor, fid)
@@ -0,0 +1,264 @@
1
+ ####################################################################################################
2
+ # residual_water.py #
3
+ ####################################################################################################
4
+ # #
5
+ # Authors: J. T. LaMaster (jlamaste@gmail.com) #
6
+ # K. C. Igwe (kci2104@columbia.edu) #
7
+ # J. P. Merkofer (j.p.merkofer@tue.nl) #
8
+ # #
9
+ # Created: 2026-02-07 #
10
+ # #
11
+ # Purpose: Residual water augmentation using original working implementation from augment_mrs.py #
12
+ # (LaMaster et al.'s Lorentzian water peaks in frequency domain) #
13
+ # #
14
+ ####################################################################################################
15
+
16
+ import numpy as np
17
+ from typing import Optional, List
18
+ from augmentrum.core.base_module import BaseModule
19
+ from augmentrum.core.nifti_mrs_plus import Backend, NIfTI_MRS_Plus
20
+ from augmentrum.utils.tensor_ops import fft, ifft, fftshift, ifftshift, to_numpy, match_backend
21
+
22
+
23
+ # ============================================================================
24
+ # Helper Function (from original augment_mrs.py lines 379-408)
25
+ # ============================================================================
26
+
27
+ def add_residual_water(spec, ppm_axis, *, center_ppm=4.7,
28
+ peaks=((0.0, 0.20, 1.0), # (delta_ppm, FWHM_ppm, rel_amp)
29
+ (+0.12, 0.18, 0.4),
30
+ (-0.15, 0.25, 0.3)),
31
+ phase_deg=0.0, amplitude_scale=0.1):
32
+ """
33
+ Add Lorentzian residual water lobes (original implementation).
34
+
35
+ Args:
36
+ spec: Complex spectrum
37
+ ppm_axis: PPM axis
38
+ center_ppm: Center position of water peak (default: 4.7 ppm)
39
+ peaks: Tuple of (delta_ppm, FWHM_ppm, rel_amp) for each lobe
40
+ phase_deg: Phase of water peaks in degrees
41
+ amplitude_scale: Scale factor relative to spectrum max (default: 0.1 = 10%)
42
+
43
+ Returns:
44
+ Spectrum with water peaks added
45
+ """
46
+ s = np.asarray(spec, dtype=complex)
47
+ ppm = np.asarray(ppm_axis, float)
48
+
49
+ phi = np.deg2rad(phase_deg)
50
+ w = np.zeros_like(ppm, float)
51
+
52
+ # Simple Lorentzians
53
+ for dppm, fwhm_ppm, rel_amp in peaks:
54
+ x = ppm - (center_ppm + dppm)
55
+ hw = 0.5 * fwhm_ppm
56
+ w += rel_amp * (hw / (x**2 + hw**2)) / np.pi
57
+
58
+ # Normalize lobes to ~unit max, then scale to a fraction of spec
59
+ w /= np.max(w) if np.max(w) > 0 else 1.0
60
+ water_amp = amplitude_scale * np.max(np.abs(np.real(s)))
61
+ water = water_amp * w * (np.cos(phi) + 1j*np.sin(phi))
62
+
63
+ return s + water
64
+
65
+
66
+ # ============================================================================
67
+ # ResidualWater Module
68
+ # ============================================================================
69
+
70
+ class ResidualWater(BaseModule):
71
+ """
72
+ Add residual water peaks to MRS data.
73
+
74
+ Based on the original implementation from augment_mrs.py (LaMaster et al.).
75
+ Adds Lorentzian-shaped water peaks around 4.7 ppm to simulate imperfect
76
+ water suppression.
77
+
78
+ Parameters
79
+ ----------
80
+ center_ppm : float
81
+ Center position of water peak in ppm (default: 4.7)
82
+ peaks : tuple of tuples
83
+ Each tuple is (delta_ppm, FWHM_ppm, rel_amp)
84
+ - delta_ppm: offset from center in ppm
85
+ - FWHM_ppm: Full Width at Half Maximum in ppm
86
+ - rel_amp: relative amplitude
87
+ Default: Three lobes at 0.0, +0.12, -0.15 ppm
88
+ phase_deg : float
89
+ Phase of water peaks in degrees (default: 0.0)
90
+ amplitude_scale : float
91
+ Scale factor for water amplitude relative to spectrum (default: 0.1 = 10%)
92
+
93
+ Examples
94
+ --------
95
+ >>> # Default water (10% amplitude, 3 lobes)
96
+ >>> water = ResidualWater()
97
+ >>> result_data, _ = water(nifti_plus, None)
98
+
99
+ >>> # Subtle water (5%)
100
+ >>> water = ResidualWater(amplitude_scale=0.05)
101
+
102
+ >>> # Strong water (30%)
103
+ >>> water = ResidualWater(amplitude_scale=0.30)
104
+
105
+ >>> # Custom peaks
106
+ >>> water = ResidualWater(
107
+ ... peaks=((0.0, 0.25, 1.0), (0.10, 0.20, 0.5)),
108
+ ... phase_deg=15.0
109
+ ... )
110
+ """
111
+
112
+ SUPPORTED_BACKENDS = [] # Supports all backends
113
+
114
+ def __init__(self, center_ppm: float = 4.7,
115
+ peaks: tuple = ((0.0, 0.20, 1.0),
116
+ (0.12, 0.18, 0.4),
117
+ (-0.15, 0.25, 0.3)),
118
+ phase_deg: float = 0.0,
119
+ amplitude_scale: float = 0.1):
120
+ """Initialize residual water module."""
121
+ super().__init__(center_ppm=center_ppm, peaks=peaks,
122
+ phase_deg=phase_deg, amplitude_scale=amplitude_scale)
123
+
124
+ self.center_ppm = center_ppm
125
+ self.peaks = peaks
126
+ self.phase_deg = phase_deg
127
+ self.amplitude_scale = amplitude_scale
128
+
129
+ def process_nifti_list(self, data_list: List, water_list: Optional[List] = None, **kwargs):
130
+ """
131
+ Add residual water to list of NIFTI_MRS objects.
132
+
133
+ Args:
134
+ data_list: List of NIFTI_MRS objects
135
+ water_list: Optional list of water reference NIFTI_MRS objects (unchanged)
136
+ **kwargs: Additional arguments
137
+
138
+ Returns:
139
+ Tuple of (processed_data_list, water_list)
140
+ """
141
+ processed_data = []
142
+
143
+ for nifti in data_list:
144
+ # Get FID data
145
+ fid = nifti[:]
146
+
147
+ # Get parameters
148
+ sw_hz = 1.0 / nifti.dwelltime
149
+ sf_mhz = nifti.spectrometer_frequency[0]
150
+
151
+ # Add water to FID
152
+ fid_with_water = self._add_water_to_fid(fid, sw_hz, sf_mhz)
153
+
154
+ # Update NIFTI_MRS data
155
+ nifti[:] = fid_with_water
156
+ processed_data.append(nifti)
157
+
158
+ return processed_data, water_list
159
+
160
+ def process_tensor(self, data_array, water_array=None, backend=None, **kwargs):
161
+ """
162
+ Add residual water peaks to tensor/array data (**any backend**).
163
+
164
+ FFT/IFFT are done **natively** via ``tensor_ops``. Water peak shapes
165
+ and amplitude scaling are computed in NumPy per-FID, then promoted
166
+ to the target backend via ``match_backend``.
167
+
168
+ Args:
169
+ data_array: Input tensor of shape ``(batch, ..., n_points)``
170
+ water_array: Optional water reference (unchanged)
171
+ backend: Backend enum (unused)
172
+ **kwargs: Must contain ``'sw_hz'`` and ``'sf_mhz'``
173
+
174
+ Returns:
175
+ Tuple of (processed_data, water_array)
176
+ """
177
+ sw_hz = kwargs.get('sw_hz')
178
+ sf_mhz = kwargs.get('sf_mhz')
179
+ if sw_hz is None or sf_mhz is None:
180
+ raise ValueError("ResidualWater.process_tensor requires 'sw_hz' and 'sf_mhz' in kwargs")
181
+
182
+ N = data_array.shape[-1]
183
+ n_batch = int(np.prod(data_array.shape[:-1]))
184
+ ref_ppm = 4.7
185
+
186
+ # 1. Spectrum (backend-native FFT)
187
+ spec = fftshift(ifft(data_array))
188
+
189
+ # 2. ppm axis (numpy)
190
+ dt = 1.0 / float(sw_hz)
191
+ freq_hz = np.fft.fftshift(np.fft.fftfreq(N, d=dt))
192
+ ppm = ref_ppm - freq_hz / float(sf_mhz)
193
+
194
+ # 3. Compute additive water contribution per FID (numpy)
195
+ spec_np = to_numpy(spec).reshape(n_batch, N)
196
+ water_add_np = np.zeros((n_batch, N), dtype=np.complex128)
197
+
198
+ for i in range(n_batch):
199
+ spec_with = add_residual_water(
200
+ spec_np[i], ppm,
201
+ center_ppm=self.center_ppm,
202
+ peaks=self.peaks,
203
+ phase_deg=self.phase_deg,
204
+ amplitude_scale=self.amplitude_scale,
205
+ )
206
+ water_add_np[i] = spec_with - spec_np[i]
207
+
208
+ water_add_np = water_add_np.reshape(data_array.shape)
209
+
210
+ # 4. Add water in spectral domain (backend-native)
211
+ spec_aug = spec + match_backend(water_add_np, spec)
212
+
213
+ # 5. Back to FID (backend-native IFFT)
214
+ return fft(ifftshift(spec_aug)), water_array
215
+
216
+ def _add_water_to_fid(self, fid: np.ndarray, sw_hz: float, sf_mhz: float,
217
+ ref_ppm: float = 4.7) -> np.ndarray:
218
+ """
219
+ Add water peaks to FID data using original augment_mrs.py implementation.
220
+
221
+ Args:
222
+ fid: Input FID data
223
+ sw_hz: Spectral width in Hz
224
+ sf_mhz: Spectrometer frequency in MHz
225
+ ref_ppm: Reference ppm (default: 4.7)
226
+
227
+ Returns:
228
+ FID with water peaks added
229
+ """
230
+ # Work with last dimension (FID points)
231
+ original_shape = fid.shape
232
+ N = original_shape[-1]
233
+
234
+ # Reshape to 2D for processing
235
+ fid_2d = fid.reshape(-1, N)
236
+ result = np.zeros_like(fid_2d)
237
+
238
+ # Process each FID
239
+ for i in range(fid_2d.shape[0]):
240
+ fid_1d = fid_2d[i]
241
+
242
+ # 1) IFFT to frequency domain (FID -> spectrum) - MRS convention
243
+ spec = np.fft.fftshift(np.fft.ifft(fid_1d))
244
+
245
+ # 2) Create ppm axis
246
+ dt = 1.0 / float(sw_hz)
247
+ freq_hz = np.fft.fftshift(np.fft.fftfreq(N, d=dt))
248
+ ppm = ref_ppm - freq_hz / sf_mhz
249
+
250
+ # 3) Add water peaks in frequency domain
251
+ spec_with_water = add_residual_water(
252
+ spec, ppm,
253
+ center_ppm=self.center_ppm,
254
+ peaks=self.peaks,
255
+ phase_deg=self.phase_deg,
256
+ amplitude_scale=self.amplitude_scale
257
+ )
258
+
259
+ # 4) FFT back to time domain (spectrum -> FID) - MRS convention
260
+ fid_with_water = np.fft.fft(np.fft.ifftshift(spec_with_water))
261
+ result[i] = fid_with_water
262
+
263
+ # Reshape back to original shape
264
+ return result.reshape(original_shape)