brkraw-sordino 0.1.3__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.
@@ -0,0 +1,168 @@
1
+ import os
2
+ import logging
3
+ from .helper import progressbar
4
+ import numpy as np
5
+ from typing import Dict, Any
6
+ from scipy.interpolate import interp1d
7
+ from .recon import get_num_frames, parse_fid_info
8
+ from .typing import Options
9
+
10
+ logger = logging.getLogger("brkraw.sordino")
11
+
12
+
13
+
14
+ def get_fid_filesize_in_gb(fid_fobj,
15
+ recon_info: Dict[str, Any],
16
+ options: Options) -> int:
17
+ fid_fobj.seek(0, os.SEEK_END)
18
+ file_size = fid_fobj.tell()
19
+ fid_fobj.seek(0)
20
+
21
+ if options.num_frames is not None:
22
+ file_size *= get_num_frames(recon_info, options) / options.num_frames
23
+ file_size_gb = file_size / (1024 ** 3)
24
+ return file_size_gb
25
+
26
+
27
+ def get_num_segment(file_size_gb, recon_info: Dict[str, Any], options: Options):
28
+ npro = recon_info['NPro']
29
+ num_segs = int(np.ceil(file_size_gb / options.mem_limit)) if options.mem_limit > 0 else 1
30
+ npro_per_seg = int(npro / num_segs)
31
+
32
+ if residual_pro := npro % npro_per_seg:
33
+ segs = [npro_per_seg for _ in range(num_segs - 1)] + [residual_pro]
34
+ else:
35
+ segs = [npro_per_seg for _ in range(num_segs)]
36
+ segs = np.asarray(segs, dtype=int)
37
+ return segs
38
+
39
+
40
+ def prep_fid_segmentation(fid_fobj,
41
+ recon_info: Dict[str, Any],
42
+ options: Options):
43
+ file_size = get_fid_filesize_in_gb(fid_fobj, recon_info, options)
44
+ segs = get_num_segment(file_size, recon_info, options)
45
+ return segs
46
+
47
+
48
+ def get_timestamps(repetition_time, npro, num_frames):
49
+ vol_scantime = float(repetition_time) * npro
50
+ base_timestamps = np.arange(num_frames) * vol_scantime
51
+ target_timestamps = base_timestamps + (vol_scantime / 2)
52
+ return {'base': base_timestamps,
53
+ 'target': target_timestamps}
54
+
55
+
56
+ def get_segmented_data(fid_f, fid_shape, fid_dtype,
57
+ buff_size, num_frames,
58
+ seg_size, pro_loc,
59
+ options):
60
+
61
+ seg = []
62
+
63
+ stc_buffer_size = int(buff_size / fid_shape[3])
64
+ pro_offset = pro_loc * stc_buffer_size
65
+ seg_buffer_size = stc_buffer_size * seg_size # total buffer size for current segment
66
+
67
+ for t in range(num_frames):
68
+ frame_offset = t * buff_size
69
+ seek_loc = int(options.offset or 0) * buff_size + frame_offset + pro_offset
70
+ fid_f.seek(seek_loc)
71
+ seg.append(fid_f.read(seg_buffer_size))
72
+
73
+ seg_data = np.frombuffer(b''.join(seg), dtype=fid_dtype)
74
+ seg_data = seg_data.reshape([2, np.prod(fid_shape[1:3]), seg_size, num_frames], order='F')
75
+ return {
76
+ 'data': seg_data,
77
+ 'offset': pro_offset,
78
+ }
79
+
80
+
81
+ def interpolate_spoketiming(complex_feed, sample_id, pro_id, ref_timestamps, timestamps, corrected_data):
82
+ """ interpolation step
83
+ each projection interpolated the timing at the middle of projection
84
+ number of spokes * receivers processed together
85
+ therefore, the spoke timing within a projection will not be corrected
86
+ instead this actually corrected the spoke timing for each projection
87
+ as single TR represent time of one projection
88
+
89
+ :param complex_feed: Description
90
+ :param sample_id: Description
91
+ :param pro_id: Description
92
+ :param ref_timestamps: Description
93
+ :param timestamps: Description
94
+ :param corrected_data: Description
95
+ """
96
+ mag = np.abs(complex_feed)
97
+ phase = np.angle(complex_feed)
98
+ phase_unw = np.unwrap(phase)
99
+ interp_mag = interp1d(ref_timestamps, mag, kind='linear',
100
+ bounds_error=False, fill_value='extrapolate') # pyright: ignore[reportArgumentType]
101
+ interp_phase = interp1d(ref_timestamps, phase_unw, kind='linear',
102
+ bounds_error=False, fill_value='extrapolate') # pyright: ignore[reportArgumentType]
103
+
104
+ mag_t = interp_mag(timestamps['target'])
105
+ phase_t = interp_phase(timestamps['target'])
106
+
107
+ phase_wrap = (phase_t + np.pi) % (2 * np.pi) - np.pi
108
+
109
+ z_t = mag_t * np.exp(1j * phase_wrap)
110
+ corrected_data[0, sample_id, pro_id, :] = z_t.real
111
+ corrected_data[1, sample_id, pro_id, :] = z_t.imag
112
+ return corrected_data
113
+
114
+
115
+ def correct_spoketiming(segs, fid_f, stc_f, recon_info, options):
116
+ """ Correct timing of each spoke to align center of scan time
117
+ (Same concept as slice timing correction, but applied to FID signal)
118
+ """
119
+ logger.debug("+ Spoketiming correction")
120
+ fid_shape, fid_dtype = parse_fid_info(recon_info)
121
+ buff_size = int(np.prod(fid_shape) * fid_dtype.itemsize)
122
+ num_frames = get_num_frames(recon_info, options)
123
+ repetition_time = recon_info['RepetitionTime_ms'] / 1000.0
124
+ timestamps = get_timestamps(repetition_time, fid_shape[3], num_frames)
125
+
126
+ pro_loc = 0
127
+ results = {
128
+ 'buffer_size': 0,
129
+ 'dtype': None,
130
+ }
131
+ for seg_size in progressbar(segs, desc=' - Segments', ncols=100):
132
+ # load data
133
+ segmented = get_segmented_data(
134
+ fid_f, fid_shape, fid_dtype, buff_size, num_frames, seg_size, pro_loc, options
135
+ )
136
+
137
+ seg_data = segmented['data']
138
+ corrected_seg_data = np.empty_like(seg_data)
139
+ for pro_id in range(seg_size):
140
+ cur_pro = pro_loc + pro_id
141
+ ref_timestamps = timestamps['base'] + (cur_pro * repetition_time)
142
+ for sample_id in range(np.prod(fid_shape[1:3])):
143
+ complex_feed = seg_data[0, sample_id, pro_id, :] + 1j * seg_data[1, sample_id, pro_id, :]
144
+ try:
145
+ corrected_seg_data = interpolate_spoketiming(complex_feed, sample_id, pro_id,
146
+ ref_timestamps, timestamps,
147
+ corrected_seg_data)
148
+ except Exception as e:
149
+ logger.debug("+ Exception occured during spoketiming correction")
150
+ logger.debug(f" - RefTimeStamps: {ref_timestamps}")
151
+ logger.debug(f" - DataFeed: {complex_feed}")
152
+ raise e
153
+
154
+ # Store data
155
+ for t in range(num_frames):
156
+ frame_offset = t * buff_size
157
+ stc_f.seek(frame_offset + segmented['offset'])
158
+ stc_f.write(corrected_seg_data[:,:,:, t].flatten(order='F').tobytes())
159
+
160
+ if results['dtype'] == None:
161
+ results['dtype'] = corrected_seg_data.dtype
162
+ else:
163
+ assert results['dtype'] == corrected_seg_data.dtype
164
+
165
+ pro_loc += seg_size
166
+
167
+ results['buffer_size'] = np.prod([fid_shape]) * results['dtype'].itemsize
168
+ return results
brkraw_sordino/traj.py ADDED
@@ -0,0 +1,316 @@
1
+ from typing import Optional, Dict, Any
2
+ from pathlib import Path
3
+ import numpy as np
4
+ import hashlib
5
+ import logging
6
+ from .helper import progressbar
7
+ from .typing import Options
8
+
9
+ logger = logging.getLogger("brkraw.sordino")
10
+
11
+
12
+ def radial_angles(n: int, factor: float) -> int:
13
+ return int(np.ceil((np.pi * n * factor) / 2))
14
+
15
+
16
+ def radial_angle(i: int, n: int) -> float:
17
+ return np.pi * (i + 0.5) / n
18
+
19
+
20
+ def recon_output_shape(matrix_size, ext_factors) -> list[int]:
21
+ output_shape = (matrix_size * ext_factors).astype(int).tolist()
22
+ return output_shape
23
+
24
+
25
+ def recon_n_frames(total_frames: int,
26
+ offset: int = 0,
27
+ num_frame: Optional[int] = None) -> int:
28
+ avail_frames = total_frames - offset
29
+ set_frames = num_frame or total_frames
30
+ if set_frames > avail_frames:
31
+ set_frames = avail_frames
32
+ return int(set_frames)
33
+
34
+
35
+ def recon_buffer_offset(buffer_size: int, offset: Optional[int] = 0) -> int:
36
+ return offset or 0 * buffer_size
37
+
38
+
39
+ def get_vol_scantime(repetition_time: float, fid_shape: np.ndarray) -> float:
40
+ return repetition_time * float(fid_shape[3])
41
+
42
+
43
+ def calc_npro(matrix_size: int, under_sampling: float) -> int:
44
+ usamp = np.sqrt(under_sampling)
45
+ n_theta = radial_angles(matrix_size, 1 / usamp)
46
+ n_pro = 0
47
+ for i_theta in range(n_theta):
48
+ theta = radial_angle(i_theta, n_theta)
49
+ n_phi = radial_angles(matrix_size, np.sin(theta) / usamp)
50
+ n_pro += n_phi
51
+ return int(n_pro)
52
+
53
+
54
+ def find_undersamp(matrix_size: int, n_pro_target: int) -> float:
55
+ from scipy.optimize import brentq
56
+
57
+ def func(under_sampling: float) -> float:
58
+ n_pro = calc_npro(matrix_size, under_sampling)
59
+ return float(n_pro - n_pro_target)
60
+
61
+ max_val = calc_npro(matrix_size, 1)
62
+ start = 1e-6
63
+ end = max_val / matrix_size
64
+ if func(start) * func(end) > 0:
65
+ raise ValueError("The function does not change sign over the interval.")
66
+ undersamp_solution = brentq(func, start, end, xtol=1e-6)
67
+ if isinstance(undersamp_solution, tuple):
68
+ return float(undersamp_solution[0])
69
+ return float(undersamp_solution)
70
+
71
+
72
+ def calc_radial_traj3d(
73
+ grad_array: np.ndarray,
74
+ matrix_size: int,
75
+ use_origin: bool,
76
+ over_sampling: float,
77
+ correct_ramptime: bool = False,
78
+ traj_offset: Optional[float] = None,
79
+ ) -> np.ndarray:
80
+ """
81
+ Calculate trajectory for SORDINO imaging.
82
+ For each projection, a vector with sampling positions (trajectory) is created.
83
+
84
+ Args:
85
+ grad_array (ndarray): Gradient vector profile for each projection, sized (3 x n_pro).
86
+ matrix_size (int): Matrix size of the final image.
87
+ over_sampling (float): Oversampling factor.
88
+ bandwidth (float): Bandwidth of the imaging process.
89
+ traj_offset (float, optional): Trajectory offset ratio compared to ADC sampling.
90
+
91
+ Returns:
92
+ ndarray: Calculated trajectory for each projection.
93
+ """
94
+ pro_offset = 1 if use_origin else 0
95
+ g = grad_array.copy()
96
+ npro = g.shape[-1]
97
+ traj_offset = traj_offset or 0
98
+ num_samples = int(matrix_size / 2 * over_sampling)
99
+ traj = np.zeros([npro, num_samples, 3])
100
+ scale_factor = (num_samples - 1 + traj_offset) / (num_samples-1)
101
+
102
+ logger.debug('++ Processing trajectory calculation...')
103
+ logger.debug(' + Input arguments')
104
+ logger.debug(f' - Size of Matrix: {matrix_size}')
105
+ logger.debug(f' - OverSampling: {over_sampling}')
106
+ logger.debug(f' - Trajectory offset ratio: {traj_offset}')
107
+ logger.debug(f' - Ramp-time Correction: {str(correct_ramptime)}')
108
+ logger.debug(f' - Size of Output Trajectory: {traj.shape}')
109
+ logger.debug(f' - Image Scailing Factor (*Subject to be corrected in future version): {scale_factor}')
110
+
111
+ for i_pro in progressbar(range(pro_offset, npro + pro_offset), desc="traj", ncols=100):
112
+ for i_samp in range(num_samples):
113
+ samp = ((i_samp + traj_offset) / (num_samples - 1)) / 2
114
+ if not correct_ramptime or i_pro == (npro + pro_offset) - 1:
115
+ correction = np.zeros(3)
116
+ traj[i_pro, i_samp, :] = samp * (g[:, i_pro] + correction)
117
+ else:
118
+ correction = (g[:, i_pro] - g[:, i_pro - 1]) / num_samples * i_samp
119
+ traj[i_pro, i_samp, :] = samp * (g[:, i_pro - 1] + correction)
120
+ return traj
121
+
122
+
123
+ def calc_radial_grad3d(
124
+ matrix_size: int,
125
+ npro_target: int,
126
+ half_sphere: bool,
127
+ use_origin: bool,
128
+ reorder: bool,
129
+ ) -> np.ndarray:
130
+ """
131
+ Generate 3D radial gradient profile based on input parameters.
132
+
133
+ Args:
134
+ matrix_size (int): Target matrix size.
135
+ n_pro_target (int): Target number of projections.
136
+ half_sphere (bool): If True, only generate for half the sphere.
137
+ use_origin (bool): If True, add center points at the start.
138
+ reorder (bool): Use reorder scheme provided by Bruker ZTE sequence.
139
+
140
+ Returns:
141
+ ndarray: The gradient profile as an array.
142
+ """
143
+
144
+ n_pro = int(npro_target / (1 if half_sphere else 2) - (1 if use_origin else 0))
145
+ usamp = np.sqrt(find_undersamp(matrix_size, n_pro))
146
+
147
+ logger.debug('\n++ Processing SORDINO 3D Radial Gradient Calculation...')
148
+ logger.debug(' + Input arguments')
149
+ logger.debug(f' - Matrix size: {matrix_size}')
150
+ logger.debug(f' - Undersampling factor: {usamp}')
151
+ logger.debug(f' - Number of Projections: {npro_target}')
152
+ logger.debug(f' - Half sphere only: {half_sphere}')
153
+ logger.debug(f' - Use origin: {use_origin}')
154
+ logger.debug(f' - Reorder Gradient: {reorder}')
155
+
156
+ grad = {"r": [], "p": [], "s": []}
157
+ radial_n_phi: list[int] = []
158
+
159
+ logger.debug(' + Start Calculating Gradient Vectors...')
160
+ n_theta = radial_angles(matrix_size, 1.0 / usamp)
161
+ for i_theta in range(n_theta):
162
+ theta = radial_angle(i_theta, n_theta)
163
+ n_phi = radial_angles(matrix_size, float(np.sin(theta) / usamp))
164
+ radial_n_phi.append(n_phi)
165
+ for i_phi in range(n_phi):
166
+ phi = radial_angle(i_phi, n_phi)
167
+ grad["r"].append(np.sin(theta) * np.cos(phi))
168
+ grad["p"].append(np.sin(theta) * np.sin(phi))
169
+ grad["s"].append(np.cos(theta))
170
+ logger.debug('done')
171
+
172
+ grad_array = np.stack([grad["r"], grad["p"], grad["s"]], axis=0)
173
+ n_pro_created = grad_array.shape[-1] * (1 if half_sphere else 2) + (1 if use_origin else 0)
174
+ if not usamp:
175
+ if n_pro_created != npro_target:
176
+ raise ValueError("Target number of projections can't be reached.")
177
+ grad_array = reorder_projections(n_theta, radial_n_phi, grad_array, reorder)
178
+ if not half_sphere:
179
+ grad_array = np.concatenate([grad_array, -1 * grad_array], axis=1)
180
+ if use_origin:
181
+ grad_array = np.concatenate([[[0, 0, 0]], grad_array.T], axis=0).T
182
+ return grad_array
183
+
184
+
185
+ def reorder_projections(
186
+ n_theta: int,
187
+ radial_n_phi: list[int],
188
+ grad_array: np.ndarray,
189
+ reorder: bool,
190
+ ) -> np.ndarray:
191
+ """
192
+ Reorder radial projections for improved image spoiling.
193
+
194
+ Args:
195
+ n_theta (int): Number of theta angles.
196
+ radial_n_phi (list): Number of phi angles for each theta.
197
+ grad_array (ndarray): Gradient array.
198
+ reorder (bool): Whether to apply the reordering scheme.
199
+
200
+ Returns:
201
+ ndarray: Reordered gradient array.
202
+ """
203
+ g = grad_array.copy()
204
+ if reorder:
205
+ logger.debug(' + Reordering projections...')
206
+ def reorder_incr_index(n: int, i: int, d: int) -> tuple[int, int]:
207
+ if (i + d > n - 1) or (i + d < 0):
208
+ d *= -1
209
+ i += d
210
+ return i, d
211
+
212
+ n_pro = g.shape[-1]
213
+ n_phi_max = max(radial_n_phi)
214
+ r_g = np.zeros_like(g)
215
+ r_mask = np.zeros([n_theta, n_phi_max])
216
+
217
+ for i_theta in range(n_theta):
218
+ for i_phi in range(radial_n_phi[i_theta], n_phi_max):
219
+ r_mask[i_theta][i_phi] = 1
220
+
221
+ i_theta = 0
222
+ d_theta = 1
223
+ i_phi = 0
224
+ d_phi = 1
225
+
226
+ for i in range(n_pro):
227
+ while not any(r_mask[i_theta] == 0):
228
+ i_theta, d_theta = reorder_incr_index(n_theta, i_theta, d_theta)
229
+
230
+ while r_mask[i_theta][i_phi] == 1:
231
+ i_phi, d_phi = reorder_incr_index(n_phi_max, i_phi, d_phi)
232
+ new_i = sum(radial_n_phi[:i_theta]) + i_phi
233
+ r_g[:, i] = g[:, new_i]
234
+ r_mask[i_theta][i_phi] = 1
235
+
236
+ i_theta, d_theta = reorder_incr_index(n_theta, i_theta, d_theta)
237
+ i_phi, d_phi = reorder_incr_index(n_phi_max, i_phi, d_phi)
238
+ logger.debug('done')
239
+ return r_g
240
+
241
+ i = 0
242
+ for i_theta in range(n_theta):
243
+ if i_theta % 2 == 1:
244
+ for i_phi in range(int(radial_n_phi[i_theta] / 2)):
245
+ i0 = i + i_phi
246
+ i1 = i + radial_n_phi[i_theta] - 1 - i_phi
247
+ g[:, i0], g[:, i1] = g[:, i1].copy(), g[:, i0].copy()
248
+ i += radial_n_phi[i_theta]
249
+ return g
250
+
251
+
252
+ def generate_hash(*args: Any) -> str:
253
+ """Generate a hash from the input arguments."""
254
+ hash_input = "".join(str(arg) for arg in args)
255
+ return hashlib.md5(hash_input.encode()).hexdigest()
256
+
257
+
258
+ def get_trajectory(recon_info: Dict[str, Any],
259
+ options: Options) -> np.ndarray:
260
+
261
+ correct_ramptime = getattr(options, "correct_ramptime", True)
262
+ ext_factors = getattr(options, "ext_factors", [1.0, 1.0, 1.0])
263
+ logger.debug(f' + Extension factors applied to matrix: {ext_factors}')
264
+
265
+ sample_size = recon_info['Matrix'][0]
266
+ npro = recon_info['NPro']
267
+ half_acquisition = recon_info['HalfAcquisition']
268
+ use_origin = recon_info['UseOrigin']
269
+ reorder = recon_info['Reorder']
270
+
271
+ eff_bandwidth = recon_info['EffBandwidth_Hz']
272
+ over_sampling = recon_info['OverSampling']
273
+ traj_offset = recon_info['AcqDelayTotal_us']
274
+
275
+ grad = calc_radial_grad3d(sample_size,
276
+ npro,
277
+ half_acquisition,
278
+ use_origin,
279
+ reorder)
280
+ offset_factor = traj_offset * (10 ** -6) * eff_bandwidth * over_sampling
281
+
282
+ option_for_hash = (
283
+ float(traj_offset),
284
+ sample_size,
285
+ eff_bandwidth,
286
+ over_sampling,
287
+ int(npro),
288
+ float(np.prod(ext_factors)),
289
+ bool(half_acquisition),
290
+ bool(use_origin),
291
+ bool(reorder),
292
+ correct_ramptime,
293
+ )
294
+
295
+ digest = generate_hash(*option_for_hash)
296
+ traj_path = options.cache_dir / f"{digest}.npy"
297
+ if traj_path.exists():
298
+ logger.debug("Trajectory cache hit: %s", traj_path)
299
+ traj = np.load(traj_path)
300
+ else:
301
+ logger.info("Computing trajectory (matrix=%s, n_pro=%s).", sample_size, npro)
302
+ traj = calc_radial_traj3d(
303
+ grad,
304
+ sample_size,
305
+ use_origin,
306
+ over_sampling,
307
+ correct_ramptime=correct_ramptime,
308
+ traj_offset=offset_factor,
309
+ )
310
+ np.save(traj_path, traj)
311
+ logger.debug("Saved trajectory cache: %s", traj_path)
312
+ return traj
313
+
314
+ __all__ = [
315
+ 'get_trajectory'
316
+ ]
@@ -0,0 +1,25 @@
1
+ from dataclasses import dataclass
2
+ from typing import Tuple, Optional
3
+ from pathlib import Path
4
+
5
+
6
+ @dataclass
7
+ class Options:
8
+ ext_factors: Tuple[float, float, float]
9
+ ignore_samples: int
10
+ offset: int
11
+ num_frames: Optional[int]
12
+ correct_spoketiming: bool
13
+ correct_ramptime: bool
14
+ offreso_ch: Optional[int]
15
+ offreso_freq: float
16
+ mem_limit: float
17
+ clear_cache: bool
18
+ split_ch: bool
19
+ cache_dir: Path
20
+ as_complex: bool
21
+
22
+
23
+ __all__ = [
24
+ 'Options'
25
+ ]
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: brkraw-sordino
3
+ Version: 0.1.3
4
+ Summary: BrkRaw converter hook for SORDINO-ZTE reconstruction
5
+ Author-email: SungHo Lee <shlee@unc.edu>
6
+ License: MIT
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: brkraw
10
+ Requires-Dist: numpy
11
+ Requires-Dist: scipy
12
+ Requires-Dist: mri-nufft[finufft]
13
+ Provides-Extra: dev
14
+ Requires-Dist: tomli; extra == "dev"
15
+
16
+ # brkraw-sordino
17
+
18
+ SORDINO-ZTE reconstruction hook for BrkRaw.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pip install -e .
24
+ ```
25
+
26
+ ## Hook install
27
+
28
+ ```bash
29
+ brkraw hook install brkraw-sordino
30
+ ```
31
+
32
+ This installs the hook rule from the package manifest (`brkraw_hook.yaml`).
33
+
34
+ ## Usage
35
+
36
+ Once installed, `brkraw` applies the hook automatically when a dataset matches the rule.
37
+
38
+ Basic conversion:
39
+
40
+ ```bash
41
+ brkraw convert /path/to/study --scan-id 3 --reco-id 1
42
+ ```
43
+
44
+ The hook behaves the same whether invoked via the CLI or via the Python API (the same hook entrypoint and arguments are used).
45
+
46
+ To explicitly pass hook options (or override defaults), use `--hook-arg` / `--hook-args-yaml` below.
47
+
48
+ ## Hook options
49
+
50
+ Hook arguments can be passed via `brkraw convert` using `--hook-arg` with the
51
+ entrypoint name (`sordino`):
52
+
53
+ ```bash
54
+ brkraw convert /path/to/study -s 3 -r 1 \
55
+ --hook-arg sordino:ext_factors=1.2 \
56
+ --hook-arg sordino:offset=2 \
57
+ --hook-arg sordino:split_ch=false
58
+ ```
59
+
60
+ ### Pass hook options via YAML (`--hook-args-yaml`)
61
+
62
+ BrkRaw can also load hook arguments from YAML. Generate a template like this:
63
+
64
+ ```bash
65
+ brkraw hook preset sordino -o hook_args.yaml
66
+ ```
67
+
68
+ Edit the generated YAML, then pass it to `brkraw convert` (repeatable):
69
+
70
+ ```bash
71
+ brkraw convert /path/to/study -s 3 -r 1 --hook-args-yaml hook_args.yaml
72
+ ```
73
+
74
+ Example:
75
+
76
+ ```yaml
77
+ hooks:
78
+ sordino:
79
+ ext_factors: 1.2
80
+ offset: 2
81
+ split_ch: false
82
+ # as_complex: true # optional, return (real, imag)
83
+ # cache_dir: ~/.brkraw/cache/sordino # optional (add manually if needed)
84
+ ```
85
+
86
+ Notes:
87
+
88
+ - CLI `--hook-arg` values override YAML.
89
+ - YAML supports both `{hooks: {sordino: {...}}}` and `{sordino: {...}}` shapes.
90
+ - You can also set `BRKRAW_CONVERT_HOOK_ARGS_YAML` (comma-separated paths).
91
+
92
+ Supported keys:
93
+
94
+ - `ext_factors`: scalar or 3-item sequence (default: 1.0)
95
+ - `ignore_samples`: int (default: 1)
96
+ - `offset`: int (default: 0)
97
+ - `num_frames`: int or null (default: None)
98
+ - `correct_spoketiming`: bool (default: false)
99
+ - `correct_ramptime`: bool (default: true)
100
+ - `offreso_ch`: int or null (default: None)
101
+ - `offreso_freq`: float (default: 0.0)
102
+ - `mem_limit`: float (default: 0.5)
103
+ - `clear_cache`: bool (default: true)
104
+ - `split_ch`: bool (default: false, merge channels)
105
+ - `as_complex`: bool (default: false, return complex as (real, imag))
106
+ - `cache_dir`: string path (default: ~/.brkraw/cache/sordino)
107
+
108
+ ## Notes
109
+
110
+ - The hook reconstructs data using an adjoint NUFFT and returns magnitude images by default.
111
+ - Multi-channel data defaults to merged channels; set `split_ch=true` to keep channels split.
112
+ - When `split_ch=false`, magnitude uses RSS while complex uses coherent sum.
113
+ - Orientation is normalized when the first 3D axes are spatial; see `notebooks/orientation.ipynb`.
114
+ - Cache files live under `~/.brkraw/cache/sordino` (or `BRKRAW_CONFIG_HOME`).
@@ -0,0 +1,21 @@
1
+ brkraw_sordino/__init__.py,sha256=4wLtPWxTOpwnXT6EA6_k23iayCUY4SxuAlSYuxR7jhk,145
2
+ brkraw_sordino/brkraw_hook.yaml,sha256=NA5j1wGUuakEsB49ATJuFXwYgUnH4RfEcqGES3x_PCU,136
3
+ brkraw_sordino/docs.md,sha256=ue04GqwuFc6-gTYwvvNPz8mspUQ8NSrb8_knD9HqD-0,2802
4
+ brkraw_sordino/helper.py,sha256=1_egvhiVFHU7fdjbLwlC5bxLJz6Ak9JtkClz-Ftn9t4,2210
5
+ brkraw_sordino/hook.py,sha256=1GAdfkPg6j6YftjZEQH3JsOe4Gh92gL_0V0hHCOazu4,6274
6
+ brkraw_sordino/orientation.py,sha256=A3znLacwAIXmqsEdd59qam4aYC_qGhoJ1g894zzZUv4,1082
7
+ brkraw_sordino/recon.py,sha256=2ERseWg_ga-HfugAbQzr4G0-m0dA_Buomc23RpWaKOs,4535
8
+ brkraw_sordino/spoketiming.py,sha256=cbHRZRw_N7tYPqAFwmppzwjUuB4scQYfBEYicFfBP4E,6569
9
+ brkraw_sordino/traj.py,sha256=GkxTWFubcPsiyh3doo3lskR2ozEJSFW_inaesMcRstA,11050
10
+ brkraw_sordino/typing.py,sha256=gh5w1HOZAAyHquFg0Wz-ps7V0bXkcESE5te7zV_FRL0,480
11
+ brkraw_sordino/rules/sordino.yaml,sha256=wb_KeOm50tWGUzfmN9BKPLa9sQYLRq-BnRNWOObmgE4,753
12
+ brkraw_sordino/specs/info_spec.yaml,sha256=NmiUuRuGOvwPEc8GJdrXDOMz3W5u8_loqG1_Ho5769c,2607
13
+ brkraw_sordino/specs/metadata_spec.yaml,sha256=8TynDvfqMIqP2U2dA5u5sauapbNmFh3S8NVIg-v8rDU,2854
14
+ brkraw_sordino/specs/prune4recon.yaml,sha256=qGAf-3Gz-4AfFgefHKR4x_rTLDyoN2a3_gD1RKD2kEE,1042
15
+ brkraw_sordino/specs/recon_spec.yaml,sha256=mBEbRniSIwTpWG_B9I_x1DmxVfUTsLqKK9PFBCk_XGo,1707
16
+ brkraw_sordino/specs/utils.py,sha256=BZA_0-He5hKlmEPF8tpDxq0zc74NelODdlPiFrH-ldk,4963
17
+ brkraw_sordino-0.1.3.dist-info/METADATA,sha256=6qZY2sIuIkrGpj0PyVXwauGQNxCgbhq_0FeaXn40LFI,3196
18
+ brkraw_sordino-0.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
19
+ brkraw_sordino-0.1.3.dist-info/entry_points.txt,sha256=6T_Lc6gPoonrjSSJ2fWRRzl966_ubEiKcvH6mFQBuU8,59
20
+ brkraw_sordino-0.1.3.dist-info/top_level.txt,sha256=i-aGVA4GDdQmfLl4H_ZsgEOhhIWVnSROojNLGgmK7G4,15
21
+ brkraw_sordino-0.1.3.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [brkraw.converter_hook]
2
+ sordino = brkraw_sordino.hook:HOOK
@@ -0,0 +1 @@
1
+ brkraw_sordino