mikgroup / sigpy

Python package for signal processing, with emphasis on iterative methods

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Implementing PSF for faster evaluation of NUFFT normal operator.

sidward opened this issue · comments

Is your feature request related to a problem? Please describe.
A normal operator for the NUFFT linear operator in conjunction with Pull Request #86 should help in increasing reconstruction speed for cases where the NUFFT psf fits in memory.

Describe the solution you'd like
An NUFFT normal operator akin to BART.

Additional context
I talked with @frankong and we decided it'll be best to have this discussion on here to have it track-able.

What I've tried so far

  • Let F be the NUFFT linear operator with input dimension "n". We are working in 1D for simplicity.
  • From what I understand, the normal operator N = FH * F is linear and time invariant, and hence can be effectively modeled by a convolution.
  • Let d be a delta and p = FH * F * d be the impulse response/ point spread function/ convolution kernel. This has support equal to "n".
  • In order to model the linear convolution with FFTs, we have to zero-pad the input to "2n". Let R denote the (center) zero-padding from "n" to "2n".
  • Let P = FFT(R(p)). Then, in principle, the following should hold: F^H * F = R.H * FFT.H * P * FFT * R.
  • However, in practice, I get higher than expected errors.

1D Example

This gives me an error of ~13%.

import sigpy as sp
import sigpy.plot as pl
import sigpy.mri as mr

n      = 256
devnum = 1
zpad   = 2

device = sp.Device(devnum)
xp = device.xp

# Linear operators
coords = mr.spiral(n * 1E-3, n, 2, 2, 32, 1.5, 0.27, 1.8)[:, 0, None]
nF = sp.linop.NUFFT((n,), coords) # Non-uniform
uF = sp.linop.FFT([int(k * zpad) for k in nF.ishape]) # Uniform
R  = sp.linop.Resize(uF.ishape, nF.ishape)

with device:

    # Calculating PSF
    d = xp.zeros((nF.ishape), dtype=xp.complex64)
    d[d.shape[0]//2] = 1
    h = nF.H * nF * d
    
    # Creating Toeplitz operator
    psf = xp.zeros([int(k * zpad) for k in d.shape], xp.complex64)
    psf[(psf.shape[0] - d.shape[0])//2 : (psf.shape[0] - d.shape[0])//2 + d.shape[0]] = h
    PSF = uF(psf)
    T = R.H * uF.H * sp.linop.Multiply(uF.oshape, PSF) * uF * R 
    
    # Testing
    nrmse = lambda x, y: 100 * xp.linalg.norm(x/xp.linalg.norm(x) - y/xp.linalg.norm(y))
    vec = xp.random.standard_normal(T.ishape) + 1j * xp.random.standard_normal(T.ishape)
    print("Random test: %0.2f%%" % nrmse(nF.H * nF * vec, T * vec))
    print("Delta test:  %0.2f%%" % nrmse(h, T(d)))

2D Example

This gives me an error of ~70%.

import sigpy as sp
import sigpy.plot as pl
import sigpy.mri as mr

n = 256
devnum = 1
zpad = 2

device = sp.Device(devnum)
xp = device.xp

# Linear operators
coords = mr.spiral(n * 1E-3, n, 2, 2, 32, 1.5, 0.27, 1.8)
nF = sp.linop.NUFFT((n, n), coords) # Non-uniform
uF = sp.linop.FFT([int(k * zpad) for k in nF.ishape], axes=(0, 1)) # Uniform
R  = sp.linop.Resize(uF.ishape, nF.ishape)

with device:

    # Calculating PSF
    d = xp.zeros((nF.ishape), dtype=xp.complex64)
    d[d.shape[0]//2, d.shape[1]//2] = 1
    h = nF.H * nF * d
    
    # Creating Toeplitz operator
    psf = xp.zeros([int(k * zpad) for k in d.shape], xp.complex64)
    psf[(psf.shape[0] - d.shape[0])//2 : (psf.shape[0] - d.shape[0])//2 + d.shape[0],
        (psf.shape[1] - d.shape[1])//2 : (psf.shape[1] - d.shape[1])//2 + d.shape[1]] = h
    PSF = uF(psf)
    T = R.H * uF.H * sp.linop.Multiply(uF.oshape, PSF) * uF * R 
    
    # Testing
    nrmse = lambda x, y: 100 * xp.linalg.norm(x/xp.linalg.norm(x) - y/xp.linalg.norm(y))
    vec = xp.random.standard_normal(T.ishape) + 1j * xp.random.standard_normal(T.ishape)
    print("Random test: %0.2f%%" % nrmse(nF.H * nF * vec, T * vec))
    print("Delta test:  %0.2f%%" % nrmse(h, T(d)))

Addressed in PR #92