1use crate::fft::{fft3d, fftfreq, ifft3d};
68use crate::utils::denoise::jacobi_eigh;
69use crate::Grid;
70use num_complex::Complex64;
71
72#[cfg(feature = "parallel")]
73use rayon::prelude::*;
74
75pub const N_TENSOR: usize = 6;
77
78#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
80#[derive(Clone, Debug)]
81pub struct StiParams {
82 pub lambda: f64,
89}
90
91impl Default for StiParams {
92 fn default() -> Self {
93 Self { lambda: 0.0 }
94 }
95}
96
97#[derive(Clone, Debug)]
102pub struct SusceptibilityTensor {
103 pub components: [Vec<f64>; N_TENSOR],
104}
105
106impl SusceptibilityTensor {
107 pub fn matrix_at(&self, i: usize) -> [f64; 9] {
109 let [c11, c12, c13, c22, c23, c33] = [
110 self.components[0][i],
111 self.components[1][i],
112 self.components[2][i],
113 self.components[3][i],
114 self.components[4][i],
115 self.components[5][i],
116 ];
117 [c11, c12, c13, c12, c22, c23, c13, c23, c33]
118 }
119}
120
121#[derive(Clone, Debug)]
123pub struct TensorMaps {
124 pub eigenvalues: [Vec<f64>; 3],
126 pub mms: Vec<f64>,
129 pub msa: Vec<f64>,
131 pub pev: [Vec<f64>; 3],
139}
140
141#[inline]
146fn sti_row(h: (f64, f64, f64), k: (f64, f64, f64), k2: f64) -> [f64; N_TENSOR] {
147 let (h1, h2, h3) = h;
148 let (kx, ky, kz) = k;
149 let hk = (h1 * kx + h2 * ky + h3 * kz) / k2;
152 [
153 h1 * h1 / 3.0 - h1 * kx * hk,
154 2.0 * h1 * h2 / 3.0 - (h1 * ky + h2 * kx) * hk,
155 2.0 * h1 * h3 / 3.0 - (h1 * kz + h3 * kx) * hk,
156 h2 * h2 / 3.0 - h2 * ky * hk,
157 2.0 * h2 * h3 / 3.0 - (h2 * kz + h3 * ky) * hk,
158 h3 * h3 / 3.0 - h3 * kz * hk,
159 ]
160}
161
162#[inline]
183fn is_nyquist_bin(i: usize, j: usize, k: usize, dims: (usize, usize, usize)) -> bool {
184 let (nx, ny, nz) = dims;
185 (nx % 2 == 0 && i == nx / 2)
186 || (ny % 2 == 0 && j == ny / 2)
187 || (nz % 2 == 0 && k == nz / 2)
188}
189
190fn cholesky6(a: &mut [f64; 36]) -> bool {
193 for i in 0..N_TENSOR {
194 for j in 0..=i {
195 let mut sum = a[i * N_TENSOR + j];
196 for k in 0..j {
197 sum -= a[i * N_TENSOR + k] * a[j * N_TENSOR + k];
198 }
199 if i == j {
200 if sum <= 0.0 {
201 return false;
202 }
203 a[i * N_TENSOR + i] = sum.sqrt();
204 } else {
205 a[i * N_TENSOR + j] = sum / a[j * N_TENSOR + j];
206 }
207 }
208 }
209 true
210}
211
212fn cholesky6_solve(l: &[f64; 36], b: &mut [f64; N_TENSOR]) {
214 for i in 0..N_TENSOR {
215 let mut sum = b[i];
216 for k in 0..i {
217 sum -= l[i * N_TENSOR + k] * b[k];
218 }
219 b[i] = sum / l[i * N_TENSOR + i];
220 }
221 for i in (0..N_TENSOR).rev() {
222 let mut sum = b[i];
223 for k in (i + 1)..N_TENSOR {
224 sum -= l[k * N_TENSOR + i] * b[k];
225 }
226 b[i] = sum / l[i * N_TENSOR + i];
227 }
228}
229
230pub fn sti<F: AsRef<[f64]>>(
244 local_fields: &[F],
245 bdirs: &[(f64, f64, f64)],
246 mask: &[u8],
247 grid: &Grid,
248 params: &StiParams,
249) -> SusceptibilityTensor {
250 let (nx, ny, nz) = grid.dims;
251 let n = grid.n_total();
252 let n_dir = local_fields.len();
253
254 assert!(
255 n_dir >= N_TENSOR,
256 "sti: needs at least {} orientations to determine {} tensor components, got {}",
257 N_TENSOR, N_TENSOR, n_dir
258 );
259 assert_eq!(
260 n_dir, bdirs.len(),
261 "sti: got {} field maps but {} B0 directions", n_dir, bdirs.len()
262 );
263 for (t, f) in local_fields.iter().enumerate() {
264 assert_eq!(
265 f.as_ref().len(), n,
266 "sti: orientation {} has {} voxels, expected {}", t, f.as_ref().len(), n
267 );
268 }
269
270 let h: Vec<(f64, f64, f64)> = bdirs
272 .iter()
273 .map(|&(x, y, z)| {
274 let m = (x * x + y * y + z * z).sqrt();
275 assert!(m > 0.0, "sti: B0 direction must be non-zero");
276 (x / m, y / m, z / m)
277 })
278 .collect();
279
280 let spectra: Vec<Vec<Complex64>> = local_fields
282 .iter()
283 .map(|f| {
284 let mut c: Vec<Complex64> =
285 f.as_ref().iter().map(|&v| Complex64::new(v, 0.0)).collect();
286 fft3d(&mut c, nx, ny, nz);
287 c
288 })
289 .collect();
290
291 let (kx, ky, kz) = (
292 fftfreq(nx, grid.voxel_size.0),
293 fftfreq(ny, grid.voxel_size.1),
294 fftfreq(nz, grid.voxel_size.2),
295 );
296
297 let mut out = vec![Complex64::new(0.0, 0.0); n * N_TENSOR];
299 let lambda = params.lambda;
300
301 let solve_chunk = |(idx, slot): (usize, &mut [Complex64])| {
302 let i = idx % nx;
303 let j = (idx / nx) % ny;
304 let k = idx / (nx * ny);
305 let kvec = (kx[i], ky[j], kz[k]);
306 let k2 = kvec.0 * kvec.0 + kvec.1 * kvec.1 + kvec.2 * kvec.2;
307
308 if k2 <= 1e-20 || is_nyquist_bin(i, j, k, grid.dims) {
311 for s in slot.iter_mut() {
312 *s = Complex64::new(0.0, 0.0);
313 }
314 return;
315 }
316
317 let mut ata = [0.0f64; 36];
319 let mut atb = [Complex64::new(0.0, 0.0); N_TENSOR];
320 for (t, &hv) in h.iter().enumerate() {
321 let row = sti_row(hv, kvec, k2);
322 let b = spectra[t][idx];
323 for a in 0..N_TENSOR {
324 atb[a] += b * row[a];
325 for c in 0..=a {
326 ata[a * N_TENSOR + c] += row[a] * row[c];
327 }
328 }
329 }
330 for a in 0..N_TENSOR {
331 ata[a * N_TENSOR + a] += lambda;
332 }
333
334 if !cholesky6(&mut ata) {
335 for s in slot.iter_mut() {
338 *s = Complex64::new(0.0, 0.0);
339 }
340 return;
341 }
342
343 let mut re = [0.0f64; N_TENSOR];
345 let mut im = [0.0f64; N_TENSOR];
346 for a in 0..N_TENSOR {
347 re[a] = atb[a].re;
348 im[a] = atb[a].im;
349 }
350 cholesky6_solve(&ata, &mut re);
351 cholesky6_solve(&ata, &mut im);
352 for a in 0..N_TENSOR {
353 slot[a] = Complex64::new(re[a], im[a]);
354 }
355 };
356
357 #[cfg(feature = "parallel")]
358 out.par_chunks_mut(N_TENSOR).enumerate().for_each(solve_chunk);
359 #[cfg(not(feature = "parallel"))]
360 out.chunks_mut(N_TENSOR).enumerate().for_each(solve_chunk);
361
362 let mut components: [Vec<f64>; N_TENSOR] = Default::default();
364 for (a, comp) in components.iter_mut().enumerate() {
365 let mut channel: Vec<Complex64> = (0..n).map(|i| out[i * N_TENSOR + a]).collect();
366 ifft3d(&mut channel, nx, ny, nz);
367 *comp = channel
368 .iter()
369 .zip(mask.iter())
370 .map(|(c, &m)| if m == 0 { 0.0 } else { c.re })
371 .collect();
372 }
373
374 SusceptibilityTensor { components }
375}
376
377pub fn sti_forward(
382 tensor: &SusceptibilityTensor,
383 bdirs: &[(f64, f64, f64)],
384 grid: &Grid,
385) -> Vec<Vec<f64>> {
386 let (nx, ny, nz) = grid.dims;
387 let n = grid.n_total();
388
389 let spectra: Vec<Vec<Complex64>> = tensor
390 .components
391 .iter()
392 .map(|c| {
393 let mut z: Vec<Complex64> = c.iter().map(|&v| Complex64::new(v, 0.0)).collect();
394 fft3d(&mut z, nx, ny, nz);
395 z
396 })
397 .collect();
398
399 let (kxs, kys, kzs) = (
400 fftfreq(nx, grid.voxel_size.0),
401 fftfreq(ny, grid.voxel_size.1),
402 fftfreq(nz, grid.voxel_size.2),
403 );
404
405 bdirs
406 .iter()
407 .map(|&(x, y, z)| {
408 let m = (x * x + y * y + z * z).sqrt();
409 let hv = (x / m, y / m, z / m);
410 let mut field = vec![Complex64::new(0.0, 0.0); n];
411 for idx in 0..n {
412 let i = idx % nx;
413 let j = (idx / nx) % ny;
414 let k = idx / (nx * ny);
415 let kvec = (kxs[i], kys[j], kzs[k]);
416 let k2 = kvec.0 * kvec.0 + kvec.1 * kvec.1 + kvec.2 * kvec.2;
417 if k2 <= 1e-20 || is_nyquist_bin(i, j, k, grid.dims) {
418 continue;
419 }
420 let row = sti_row(hv, kvec, k2);
421 let mut acc = Complex64::new(0.0, 0.0);
422 for a in 0..N_TENSOR {
423 acc += spectra[a][idx] * row[a];
424 }
425 field[idx] = acc;
426 }
427 ifft3d(&mut field, nx, ny, nz);
428 field.iter().map(|c| c.re).collect()
429 })
430 .collect()
431}
432
433pub fn tensor_maps(tensor: &SusceptibilityTensor, mask: &[u8]) -> TensorMaps {
437 let n = tensor.components[0].len();
438 let mut eigenvalues: [Vec<f64>; 3] = [vec![0.0; n], vec![0.0; n], vec![0.0; n]];
439 let mut pev: [Vec<f64>; 3] = [vec![0.0; n], vec![0.0; n], vec![0.0; n]];
440 let mut mms = vec![0.0; n];
441 let mut msa = vec![0.0; n];
442
443 for i in 0..n {
444 if mask[i] == 0 {
445 continue;
446 }
447 let (d, v) = jacobi_eigh(&tensor.matrix_at(i), 3);
449 for a in 0..3 {
450 eigenvalues[a][i] = d[a];
451 pev[a][i] = v[a * 3 + 2];
452 }
453 mms[i] = (d[0] + d[1] + d[2]) / 3.0;
454 msa[i] = d[2] - (d[0] + d[1]) / 2.0;
455 }
456
457 TensorMaps { eigenvalues, mms, msa, pev }
458}
459
460#[cfg(test)]
461mod tests {
462 use super::*;
463 use crate::kernels::dipole::dipole_kernel;
464 use crate::fft::apply_real_kernel;
465
466 fn hemisphere_dirs(n: usize) -> Vec<(f64, f64, f64)> {
469 let golden = std::f64::consts::PI * (3.0 - 5.0f64.sqrt());
470 (0..n)
471 .map(|t| {
472 let z = 1.0 - (t as f64) / (n as f64);
473 let r = (1.0 - z * z).max(0.0).sqrt();
474 let az = golden * t as f64;
475 (r * az.cos(), r * az.sin(), z)
476 })
477 .collect()
478 }
479
480 fn smooth_volume(grid: &Grid, scale: f64, phase: f64) -> Vec<f64> {
482 let (nx, ny, nz) = grid.dims;
483 let mut v = vec![0.0f64; grid.n_total()];
484 for k in 0..nz {
485 for j in 0..ny {
486 for i in 0..nx {
487 let (x, y, z) = (i as f64, j as f64, k as f64);
488 v[i + j * nx + k * nx * ny] =
489 scale * ((0.6 * x + phase).sin() * (0.4 * y).cos() + 0.5 * (0.35 * z + phase).sin());
490 }
491 }
492 }
493 let mean = v.iter().sum::<f64>() / v.len() as f64;
494 for x in v.iter_mut() {
495 *x -= mean;
496 }
497 v
498 }
499
500 fn random_tensor(grid: &Grid) -> SusceptibilityTensor {
501 SusceptibilityTensor {
502 components: [
503 smooth_volume(grid, 0.10, 0.0),
504 smooth_volume(grid, 0.03, 1.1),
505 smooth_volume(grid, 0.02, 2.2),
506 smooth_volume(grid, 0.09, 3.3),
507 smooth_volume(grid, 0.025, 4.4),
508 smooth_volume(grid, 0.11, 5.5),
509 ],
510 }
511 }
512
513 fn max_abs_diff(a: &[f64], b: &[f64]) -> f64 {
514 a.iter().zip(b.iter()).map(|(x, y)| (x - y).abs()).fold(0.0, f64::max)
515 }
516
517 #[test]
525 fn isotropic_tensor_reduces_to_the_dipole_kernel() {
526 let grid = Grid::new(13, 13, 13, 1.0, 1.0, 1.0);
527 let chi = smooth_volume(&grid, 0.1, 0.0);
528 let zero = vec![0.0; grid.n_total()];
529 let iso = SusceptibilityTensor {
530 components: [chi.clone(), zero.clone(), zero.clone(), chi.clone(), zero, chi.clone()],
531 };
532
533 for bdir in [(0.0, 0.0, 1.0), (0.6, 0.0, 0.8), (0.3, -0.5, 0.81)] {
534 let via_sti = &sti_forward(&iso, &[bdir], &grid)[0];
535 let (nx, ny, nz) = grid.dims;
536 let via_dipole = apply_real_kernel(&chi, &dipole_kernel(&grid, bdir), nx, ny, nz);
537 let err = max_abs_diff(via_sti, &via_dipole);
538 let scale = via_dipole.iter().fold(0.0f64, |m, v| m.max(v.abs()));
539 assert!(
540 err / scale < 1e-12,
541 "isotropic STI forward disagrees with the dipole kernel for {:?}: rel {}",
542 bdir, err / scale
543 );
544 }
545 }
546
547 #[test]
549 fn recovers_tensor_from_simulated_orientations() {
550 let grid = Grid::new(13, 13, 13, 1.0, 1.0, 1.0);
553 let mask = vec![1u8; grid.n_total()];
554 let truth = random_tensor(&grid);
555 let dirs = hemisphere_dirs(16);
556
557 let fields = sti_forward(&truth, &dirs, &grid);
558 let rec = sti(&fields, &dirs, &mask, &grid, &StiParams::default());
559
560 for a in 0..N_TENSOR {
561 let scale = truth.components[a].iter().fold(0.0f64, |m, v| m.max(v.abs()));
562 let rel = max_abs_diff(&rec.components[a], &truth.components[a]) / scale;
563 assert!(rel < 1e-12, "component {} not recovered: rel err {}", a, rel);
564 }
565 }
566
567 #[test]
568 fn recovers_tensor_with_anisotropic_voxels() {
569 let grid = Grid::new(13, 13, 11, 0.9, 0.9, 2.0);
570 let mask = vec![1u8; grid.n_total()];
571 let truth = random_tensor(&grid);
572 let dirs = hemisphere_dirs(16);
573
574 let fields = sti_forward(&truth, &dirs, &grid);
575 let rec = sti(&fields, &dirs, &mask, &grid, &StiParams::default());
576
577 for a in 0..N_TENSOR {
578 let scale = truth.components[a].iter().fold(0.0f64, |m, v| m.max(v.abs()));
579 assert!(max_abs_diff(&rec.components[a], &truth.components[a]) / scale < 1e-12);
580 }
581 }
582
583 #[test]
585 fn isotropic_tensor_has_no_anisotropy() {
586 let grid = Grid::new(10, 10, 10, 1.0, 1.0, 1.0);
587 let n = grid.n_total();
588 let mask = vec![1u8; n];
589 let chi = smooth_volume(&grid, 0.1, 0.0);
590 let zero = vec![0.0; n];
591 let iso = SusceptibilityTensor {
592 components: [chi.clone(), zero.clone(), zero.clone(), chi.clone(), zero, chi.clone()],
593 };
594
595 let maps = tensor_maps(&iso, &mask);
596 let scale = chi.iter().fold(0.0f64, |m, v| m.max(v.abs()));
597 assert!(max_abs_diff(&maps.mms, &chi) / scale < 1e-12, "MMS should equal the scalar");
598 assert!(
599 maps.msa.iter().all(|v| v.abs() / scale < 1e-12),
600 "an isotropic tensor must have zero MSA"
601 );
602 }
603
604 #[test]
607 fn recovers_known_anisotropy_and_principal_axis() {
608 let grid = Grid::new(8, 8, 8, 1.0, 1.0, 1.0);
609 let n = grid.n_total();
610 let mask = vec![1u8; n];
611
612 let t = SusceptibilityTensor {
615 components: [
616 vec![0.02; n], vec![0.0; n], vec![0.0; n],
617 vec![0.09; n], vec![0.0; n], vec![0.05; n],
618 ],
619 };
620 let maps = tensor_maps(&t, &mask);
621
622 for i in 0..n {
623 assert!((maps.mms[i] - (0.02 + 0.09 + 0.05) / 3.0).abs() < 1e-12);
624 assert!((maps.msa[i] - 0.055).abs() < 1e-12, "MSA {} != 0.055", maps.msa[i]);
625 assert!(maps.pev[0][i].abs() < 1e-9 && maps.pev[2][i].abs() < 1e-9);
627 assert!((maps.pev[1][i].abs() - 1.0).abs() < 1e-9);
628 }
629 }
630
631 #[test]
633 fn eigenvalues_are_ascending() {
634 let grid = Grid::new(6, 6, 6, 1.0, 1.0, 1.0);
635 let mask = vec![1u8; grid.n_total()];
636 let maps = tensor_maps(&random_tensor(&grid), &mask);
637 for i in 0..grid.n_total() {
638 assert!(maps.eigenvalues[0][i] <= maps.eigenvalues[1][i]);
639 assert!(maps.eigenvalues[1][i] <= maps.eigenvalues[2][i]);
640 }
641 }
642
643 #[test]
644 fn masked_voxels_are_zero() {
645 let grid = Grid::new(8, 8, 8, 1.0, 1.0, 1.0);
646 let n = grid.n_total();
647 let mut mask = vec![1u8; n];
648 mask[0] = 0;
649 mask[3] = 0;
650 let truth = random_tensor(&grid);
651 let dirs = hemisphere_dirs(12);
652 let fields = sti_forward(&truth, &dirs, &grid);
653
654 let rec = sti(&fields, &dirs, &mask, &grid, &StiParams::default());
655 for a in 0..N_TENSOR {
656 assert_eq!(rec.components[a][0], 0.0);
657 assert_eq!(rec.components[a][3], 0.0);
658 }
659 }
660
661 #[test]
663 fn lambda_shrinks_the_solution() {
664 let grid = Grid::new(8, 8, 8, 1.0, 1.0, 1.0);
665 let mask = vec![1u8; grid.n_total()];
666 let truth = random_tensor(&grid);
667 let dirs = hemisphere_dirs(8);
668 let fields = sti_forward(&truth, &dirs, &grid);
669
670 let energy = |t: &SusceptibilityTensor| -> f64 {
671 t.components.iter().flat_map(|c| c.iter()).map(|v| v * v).sum()
672 };
673 let plain = sti(&fields, &dirs, &mask, &grid, &StiParams::default());
674 let reg = sti(&fields, &dirs, &mask, &grid, &StiParams { lambda: 0.05 });
675 assert!(energy(®) < energy(&plain), "lambda did not damp the solution");
676 }
677
678 #[test]
679 fn cholesky_solves_a_known_system() {
680 let mut a = [0.0f64; 36];
682 for i in 0..6 {
683 for j in 0..6 {
684 a[i * 6 + j] = if i == j { (i as f64) + 2.0 } else { 0.5 };
685 }
686 }
687 let x_true = [1.0, -2.0, 3.0, 0.5, -1.5, 2.5];
688 let mut b = [0.0f64; 6];
689 for i in 0..6 {
690 for j in 0..6 {
691 b[i] += a[i * 6 + j] * x_true[j];
692 }
693 }
694 let mut l = a;
695 assert!(cholesky6(&mut l), "matrix should be positive definite");
696 cholesky6_solve(&l, &mut b);
697 for i in 0..6 {
698 assert!((b[i] - x_true[i]).abs() < 1e-10, "x[{}] = {} != {}", i, b[i], x_true[i]);
699 }
700 }
701
702 #[test]
703 fn matrix_at_is_symmetric() {
704 let grid = Grid::new(4, 4, 4, 1.0, 1.0, 1.0);
705 let t = random_tensor(&grid);
706 let m = t.matrix_at(7);
707 assert_eq!(m[1], m[3]);
708 assert_eq!(m[2], m[6]);
709 assert_eq!(m[5], m[7]);
710 }
711
712 #[test]
715 fn nyquist_planes_are_excluded_on_even_grids() {
716 let grid = Grid::new(12, 12, 12, 1.0, 1.0, 1.0);
717 let (nx, ny, nz) = grid.dims;
718 let n = grid.n_total();
719 let mask = vec![1u8; n];
720 let dirs = hemisphere_dirs(16);
721 let fields = sti_forward(&random_tensor(&grid), &dirs, &grid);
722 let rec = sti(&fields, &dirs, &mask, &grid, &StiParams::default());
723
724 for comp in rec.components.iter() {
725 let mut spec: Vec<Complex64> =
726 comp.iter().map(|&v| Complex64::new(v, 0.0)).collect();
727 crate::fft::fft3d(&mut spec, nx, ny, nz);
728 let scale = spec.iter().map(|c| c.norm()).fold(0.0, f64::max);
729 for (idx, c) in spec.iter().enumerate() {
730 let (i, j, k) = (idx % nx, (idx / nx) % ny, idx / (nx * ny));
731 if is_nyquist_bin(i, j, k, grid.dims) {
732 assert!(
733 c.norm() / scale < 1e-12,
734 "Nyquist bin ({},{},{}) should be empty, got {:.3e}",
735 i, j, k, c.norm()
736 );
737 }
738 }
739 }
740 }
741
742 #[test]
744 fn odd_grids_have_no_excluded_bins() {
745 let dims = (13, 13, 13);
746 for idx in 0..(13 * 13 * 13) {
747 let (i, j, k) = (idx % 13, (idx / 13) % 13, idx / 169);
748 assert!(!is_nyquist_bin(i, j, k, dims));
749 }
750 assert!(is_nyquist_bin(6, 0, 0, (12, 13, 13)));
752 assert!(!is_nyquist_bin(0, 6, 0, (12, 13, 13)));
753 }
754
755 #[test]
756 #[should_panic(expected = "at least 6 orientations")]
757 fn rejects_too_few_orientations() {
758 let grid = Grid::new(6, 6, 6, 1.0, 1.0, 1.0);
759 let n = grid.n_total();
760 let fields: Vec<Vec<f64>> = (0..5).map(|_| vec![0.0; n]).collect();
761 let dirs = hemisphere_dirs(5);
762 sti(&fields, &dirs, &vec![1u8; n], &grid, &StiParams::default());
763 }
764
765 #[test]
766 #[should_panic(expected = "but 7 B0 directions")]
767 fn rejects_mismatched_direction_count() {
768 let grid = Grid::new(6, 6, 6, 1.0, 1.0, 1.0);
769 let n = grid.n_total();
770 let fields: Vec<Vec<f64>> = (0..8).map(|_| vec![0.0; n]).collect();
771 sti(&fields, &hemisphere_dirs(7), &vec![1u8; n], &grid, &StiParams::default());
772 }
773
774 #[test]
775 #[should_panic(expected = "orientation 2 has")]
776 fn rejects_wrong_sized_field() {
777 let grid = Grid::new(6, 6, 6, 1.0, 1.0, 1.0);
778 let n = grid.n_total();
779 let mut fields: Vec<Vec<f64>> = (0..8).map(|_| vec![0.0; n]).collect();
780 fields[2] = vec![0.0; n - 1];
781 sti(&fields, &hemisphere_dirs(8), &vec![1u8; n], &grid, &StiParams::default());
782 }
783}