Esempio n. 1
0
    def test_shepp_logan(self):
        '''The much-abused Shepp-Logan.'''

        ph = shepp_logan(self.N)
        ph /= np.max(ph.flatten())
        phs = ph[..., None] * self.mps

        kspace = np.fft.ifftshift(np.fft.fft2(np.fft.fftshift(phs,
                                                              axes=(0, 1)),
                                              axes=(0, 1)),
                                  axes=(0, 1))
        kspace_u = np.array(np.zeros_like(kspace))  # wrap for pylint
        kspace_u[:, ::2, :] = kspace[:, ::2, :]
        ctr = int(self.N / 2)
        pd = 5
        calib = kspace[:, ctr - pd:ctr + pd, :].copy()

        recon = grappa(kspace_u, calib, (5, 5), coil_axis=-1)
        recon = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift(recon,
                                                              axes=(0, 1)),
                                             axes=(0, 1)),
                                axes=(0, 1))
        recon = np.abs(np.sqrt(np.sum(recon * np.conj(recon), axis=-1)))
        recon /= np.max(recon.flatten())

        # Make sure less than 4% NRMSE
        # print(compare_nrmse(ph, recon))
        self.assertTrue(compare_nrmse(ph, recon) < .04)
Esempio n. 2
0
def vcgrappa(kspace, calib, *args, coil_axis=-1, **kwargs):
    '''Virtual Coil GRAPPA.

    See pygrappa.grappa() for argument list.

    Notes
    -----
    Implements modifications to GRAPPA as described in [1]_.  The
    only change I can see is stacking the conjugate coils in the
    coil dimension.  For best results, make sure there is a suitably
    chosen background phase variation as described in the paper.

    This function is a wrapper of pygrappa.cgrappa().  The existing
    coils are conjugated, added to the coil dimension, and passed
    through along with all other arguments.

    References
    ----------
    .. [1] Blaimer, Martin, et al. "Virtual coil concept for improved
           parallel MRI employing conjugate symmetric signals."
           Magnetic Resonance in Medicine: An Official Journal of the
           International Society for Magnetic Resonance in Medicine
           61.1 (2009): 93-102.
    '''

    # Move coil axis to end
    kspace = np.moveaxis(kspace, coil_axis, -1)
    calib = np.moveaxis(calib, coil_axis, -1)
    ax = (0, 1)

    # remember the type we started out with, np.fft will change
    # to complex128 regardless of what we started with
    tipe = kspace.dtype

    # We will return twice the number of coils we started with
    nc = kspace.shape[-1]

    # In and out of kspace to get conjugate coils
    vc_kspace = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift(
        kspace, axes=ax), axes=ax), axes=ax)
    vc_kspace = np.fft.ifftshift(np.fft.fft2(np.fft.fftshift(
        np.conj(vc_kspace), axes=ax), axes=ax), axes=ax)

    # Same deal for calib...
    vc_calib = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift(
        calib, axes=ax), axes=ax), axes=ax)
    vc_calib = np.fft.ifftshift(np.fft.fft2(np.fft.fftshift(
        np.conj(vc_calib), axes=ax), axes=ax), axes=ax)

    # Put all our ducks in a row...
    kspace = np.concatenate((kspace, vc_kspace), axis=-1)
    calib = np.concatenate((calib, vc_calib), axis=-1)

    # Pass through to GRAPPA
    return grappa(
        kspace, calib, coil_axis=-1, nc_desired=2*nc,
        *args, **kwargs).astype(tipe)
Esempio n. 3
0
    def test_validP_issue(self):
        '''Replication.'''

        # Pull in data we know will break grappa
        pc = np.load('pc0.npy')
        print('Loaded PC0')
        _sx, _sy, sz, _sc = pc.shape[:]
        pd = 12
        ctr = int(pc.shape[1] / 2)

        # I think it happens somewhere in this dataset: slice 35
        for ii in range(35, sz):
            calib = pc[:, ctr - pd:ctr + pd + 1, ii, :].copy()
            _recon = grappa(pc[:, :, ii, :], calib, (5, 5), coil_axis=-1)
            print('done with slice %d' % ii)
Esempio n. 4
0
def vcgrappa(kspace, calib, *args, coil_axis=-1, **kwargs):
    '''Virtual Coil GRAPPA.

    See pygrappa.grappa() for argument list.

    Notes
    -----
    Implements modifications to GRAPPA as described in [1]_.  The
    only change I can see is stacking the conjugate coils in the
    coil dimension.

    This function is a wrapper of pygrappa.cgrappa().  The existing
    coils are conjugated, added to the coil dimension, and passed
    through along with all other arguments.

    References
    ----------
    .. [1] Blaimer, Martin, et al. "Virtual coil concept for improved
           parallel MRI employing conjugate symmetric signals."
           Magnetic Resonance in Medicine: An Official Journal of the
           International Society for Magnetic Resonance in Medicine
           61.1 (2009): 93-102.
    '''

    # Move coil axis to end
    kspace = np.moveaxis(kspace, coil_axis, -1)
    calib = np.moveaxis(calib, coil_axis, -1)

    # Create conjugate virtual coils for kspace
    ax = (0, 1)
    vc_kspace = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift(
        kspace, axes=ax), axes=ax), axes=ax)
    vc_kspace = np.fft.ifftshift(np.fft.fft2(np.fft.fftshift(
        np.conj(vc_kspace), axes=ax), axes=ax), axes=ax)
    # Make sure zeros stay the same through FFTs:
    vc_kspace = vc_kspace*(np.abs(kspace) > 0)
    kspace = np.concatenate((kspace, vc_kspace), axis=-1)

    # Create conjugate virtual coils for calib
    vc_calib = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift(
        calib, axes=ax), axes=ax), axes=ax)
    vc_calib = np.fft.ifftshift(np.fft.fft2(np.fft.fftshift(
        np.conj(vc_calib), axes=ax), axes=ax), axes=ax)
    calib = np.concatenate((calib, vc_calib), axis=-1)

    # Pass through to GRAPPA
    return grappa(kspace, calib, coil_axis=-1, *args, **kwargs)
Esempio n. 5
0
        # ========================================================== #
        # Open up a new readonly memmap -- this is where you would
        # likely start with data you really wanted to process
        kspace = np.memmap(kspace_file,
                           mode='r',
                           shape=(N, N, ncoils),
                           dtype='complex')

        # calibrate a kernel
        kernel_size = (5, 5)

        # reconstruct, write res out to a memmap with name res_file
        grappa(kspace,
               calib,
               kernel_size,
               coil_axis=-1,
               lamda=0.01,
               memmap=True,
               memmap_filename=res_file)

        # Take a look by opening up the memmap
        res = np.memmap(res_file,
                        mode='r',
                        shape=(N, N, ncoils),
                        dtype='complex')
        res = np.abs(
            np.sqrt(N**2) * np.fft.fftshift(np.fft.ifft2(
                np.fft.ifftshift(res, axes=ax), axes=ax),
                                            axes=ax))

        res0 = np.zeros((2 * N, 2 * N))
Esempio n. 6
0
    # crop 20x20 window from the center of k-space for calibration
    pd = 10
    ctr = int(N / 2)
    calib = kspace[ctr - pd:ctr + pd, ctr - pd:ctr + pd, :].copy()

    # calibrate a kernel
    kernel_size = (5, 5)

    # undersample by a factor of 2 in both kx and ky
    kspace[::2, 1::2, :] = 0
    kspace[1::2, ::2, :] = 0

    # reconstruct:
    res = grappa(kspace,
                 calib,
                 kernel_size,
                 coil_axis=-1,
                 lamda=0.01,
                 memmap=False)

    # Take a look
    res = np.abs(
        np.sqrt(N**2) * np.fft.fftshift(
            np.fft.ifft2(np.fft.ifftshift(res, axes=ax), axes=ax), axes=ax))
    res0 = np.zeros((2 * N, 2 * N))
    kk = 0
    for idx in np.ndindex((2, 2)):
        ii, jj = idx[:]
        res0[ii * N:(ii + 1) * N, jj * N:(jj + 1) * N] = res[..., kk]
        kk += 1
    plt.imshow(res0, cmap='gray')
    plt.show()
    def process(self, recondata):

        # receive kspace data and
        # extract acq_data and acq_header
        array_acq_headers = recondata[0].data.headers
        kspace_data = recondata[0].data.data

        try:
            if recondata[0].ref.data is not None:
                print("reference data exist")
                print(
                    np.shape(recondata[0].ref.data)
                )  # only for repetition 0 # il faut creer le bucket recon grappa
                reference = recondata[0].ref.data
                data = recondata[0].data.data
                print(np.shape(reference))
                print(np.shape(data))
                self.array_calib = reference
                np.save('/tmp/gadgetron/reference', reference)
                np.save('/tmp/gadgetron/data', data)
        except:
            print("reference data not exist")

        # grappa
        array_data = recondata[0].data.data
        dims = np.shape(recondata[0].data.data)

        kspace_data_tmp = np.ndarray(dims, dtype=np.complex64)

        for slc in range(0, dims[6]):
            for n in range(0, dims[5]):
                for s in range(0, dims[4]):

                    kspace = array_data[:, :, :, :, s, n, slc]
                    calib = self.array_calib[:, :, :, :, s, n, slc]

                    calib = np.squeeze(calib, axis=2)
                    kspace = np.squeeze(kspace, axis=2)

                    sx, sy, ncoils = kspace.shape[:]
                    cx, cy, ncoils = calib.shape[:]

                    # Here's the actual reconstruction
                    res = grappa(kspace,
                                 calib,
                                 kernel_size=(5, 5),
                                 coil_axis=-1)

                    # Here's the resulting shape of the reconstruction.  The coil
                    # axis will end up in the same place you provided it in
                    sx, sy, ncoils = res.shape[:]
                    kspace_data_tmp[:, :, 0, :, s, n, slc] = res

        # ifft, this is necessary for the next gadget
        #image = transform.transform_kspace_to_image(kspace_data_tmp,dim=(0,1,2))
        image = transform.transform_kspace_to_image(kspace_data_tmp,
                                                    dim=(0, 1, 2))

        # create a new IsmrmrdImageArray
        array_data = IsmrmrdImageArray()

        # attache the images to the IsmrmrdImageArray
        array_data.data = image

        # get dimension for the acq_headers
        dims_header = np.shape(recondata[0].data.headers)

        # get one header with typical info
        acq = np.ravel(array_acq_headers)[0]

        print("acq.idx.repetition", acq.idx.repetition)

        if (acq.idx.repetition == 0):
            np.save('/tmp/gadgetron/image', image)

        headers_list = []
        base_header = ismrmrd.ImageHeader()
        base_header.version = 2
        ndims_image = np.shape(image)
        base_header.channels = ndims_image[3]
        base_header.matrix_size = (image.shape[0], image.shape[1],
                                   image.shape[2])
        print((image.shape[0], image.shape[1], image.shape[2]))
        base_header.position = acq.position
        base_header.read_dir = acq.read_dir
        base_header.phase_dir = acq.phase_dir
        base_header.slice_dir = acq.slice_dir
        base_header.patient_table_position = acq.patient_table_position
        base_header.acquisition_time_stamp = acq.acquisition_time_stamp
        base_header.image_index = 0
        base_header.image_series_index = 0
        base_header.data_type = ismrmrd.DATATYPE_CXFLOAT
        base_header.image_type = ismrmrd.IMTYPE_MAGNITUDE
        print("ready to list")

        for slc in range(0, dims_header[4]):
            for n in range(0, dims_header[3]):
                for s in range(0, dims_header[2]):
                    #for e2 in range(0, dims_header[1]):
                    #  for e1 in range(0, dims_header[0]):
                    headers_list.append(base_header)

        array_headers_test = np.array(headers_list, dtype=np.dtype(object))
        print(type(array_headers_test))
        print(np.shape(array_headers_test))
        array_headers_test = np.reshape(
            array_headers_test,
            (dims_header[2], dims_header[3], dims_header[4]))
        print(type(array_headers_test))
        print(np.shape(array_headers_test))

        print("---> ok 0")
        # how to copy acquisition header into image header in python  ?
        for slc in range(0, dims_header[4]):
            for n in range(0, dims_header[3]):
                for s in range(0, dims_header[2]):
                    # for e2 in range(0, dims_header[1]):
                    # for e1 in range(0, dims_header[0]):
                    array_headers_test[s, n, slc].slice = slc
                    #print(s,n,slc)
                    #print(type(array_headers_test[s,n,slc]))
                    #print(array_headers_test[s,n,slc].slice)

        #print("---> ok 1")
        # print(np.shape(array_image_header))
        # attache the image headers to the IsmrmrdImageArray
        array_data.headers = array_headers_test

        #print("---> ok 2")
        # Return image to Gadgetron
        #print(np.shape(array_data.data))
        #print(np.shape(array_data.headers))

        #print(type(array_data.data))
        #print(type(array_data.headers))
        #print(type(array_data))

        # send the data to the next gadget

        for slc in range(0, dims_header[4]):
            for n in range(0, dims_header[3]):
                for s in range(0, dims_header[2]):
                    #print("send out image %d-%d-%d" % (s, n, slc))
                    a = array_data.data[:, :, :, :, s, n, slc]
                    #array_data.headers[s,n,slc].slice=slc
                    #print(a.shape, array_data.headers[s,n,slc].slice)
                    self.put_next(array_data.headers[s, n, slc], a)

        #self.put_next( [IsmrmrdReconBit(array_data.headers, array_data.data)] ,  )

        print("----------------------------------------------")
        return 0
Esempio n. 8
0
    ax = (0, 1)
    kspace = np.fft.ifftshift(np.fft.fft2(np.fft.fftshift(ph, axes=ax),
                                          axes=ax),
                              axes=ax)

    # 20x20 ACS region
    pad = 10
    ctr = int(N / 2)
    calib = kspace[ctr - pad:ctr + pad, ctr - pad:ctr + pad, :].copy()

    # R=2x2
    kspace[::2, 1::2, :] = 0
    kspace[1::2, ::2, :] = 0

    # Reconstruct using both GRAPPA and VC-GRAPPA
    res_grappa = grappa(kspace, calib)
    res_vcgrappa = vcgrappa(kspace, calib)

    # Bring back to image space
    imspace_vcgrappa = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift(
        res_vcgrappa, axes=ax),
                                                    axes=ax),
                                       axes=ax)
    imspace_grappa = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift(res_grappa,
                                                                   axes=ax),
                                                  axes=ax),
                                     axes=ax)

    # Coil combine (sum-of-squares)
    cc_vcgrappa = np.sqrt(np.sum(np.abs(imspace_vcgrappa)**2, axis=-1))
    cc_grappa = np.sqrt(np.sum(np.abs(imspace_grappa)**2, axis=-1))
def PyGrappaBufferedDataPythonGadget(connection):
   logging.info("Python reconstruction running - reading readout data")
   start = time.time()
   counter=0

   reference=[]

   for acquisition in connection:
      
       #acquisition is a vector of structure called reconBit
       print(type(acquisition[0]))

       for reconBit in acquisition:

           print(type(reconBit))
           # reconBit.ref is the calibration for parallel imaging
           # reconBit.data is the undersampled dataset
           print('-----------------------')
	   # each of them include a specific header and the kspace data
           print(type(reconBit.data.headers))
           print(type(reconBit.data.data))

           print(reconBit.data.headers.shape)
           print(reconBit.data.data.shape)

           index= get_first_index_of_non_empty_header(reconBit.data.headers.flat)
           repetition=reconBit.data.headers.flat[index].idx.repetition 

           print(repetition)           
           
           reference_header=reconBit.data.headers.flat[0]

           try: 
              if reconBit.ref.data is not None:
                print("reference data exist")            
                np.save('/tmp/gadgetron/reference', reconBit.ref.data)
                reference=reconBit.ref.data
              else:
                print("reference data not exist")
           except:
              print("issue with reference data")

           dims=reconBit.data.data.shape
           kspace_data_tmp=np.zeros(dims, reconBit.data.data.dtype)
           for slc in range(0, dims[6]):
             for n in range(0, dims[5]):
               for s in range(0, dims[4]):
        
                 kspace=reconBit.data.data[:,:,:,:,s,n,slc]
                 calib=reference[:,:,:,:,s,n,slc]        
                
                 calib=np.squeeze(calib,axis=2)
                 kspace=np.squeeze(kspace,axis=2)
               
                 sx, sy,  ncoils = kspace.shape[:]
                 cx, cy,  ncoils = calib.shape[:]

                 # Here's the actual reconstruction
                 res = grappa(kspace, calib, kernel_size=(5, 5), coil_axis=-1)

                 # Here's the resulting shape of the reconstruction.  The coil
                 # axis will end up in the same place you provided it in
                 sx, sy, ncoils = res.shape[:]                
                 kspace_data_tmp[:,:,0,:,s,n,slc]=res

        
           # ifft, this is necessary for the next gadget        
           #image = transform.transform_kspace_to_image(kspace_data_tmp,dim=(0,1,2))
           im = transform.transform_kspace_to_image(kspace_data_tmp,dim=(0,1,2))
           
           
           plt.subplot(121)
           plt.imshow(np.abs(np.squeeze(reconBit.data.data[:,:,0,0,0,0,0])))
           plt.subplot(122)
           plt.imshow(np.abs(np.squeeze(im[:,:,0,0,0,0,0])))
           

           plt.show()
           send_reconstructed_images(connection,im,reference_header)

           
 
      
       #connection.send(acquisition)

   logging.info(f"Python reconstruction done. Duration: {(time.time() - start):.2f} s")
Esempio n. 10
0
def tgrappa(
        kspace, calib_size=(20, 20), kernel_size=(5, 5),
        coil_axis=-2, time_axis=-1):
    '''Temporal GRAPPA.

    Parameters
    ----------
    kspace : array_like
        2+1D multi-coil k-space data to reconstruct from (total of
        4 dimensions).  Missing entries should have exact zeros in
        them.
    calib_size : array_like, optional
        Size of calibration region at the center of kspace.
    kernel_size : tuple, optional
        Desired shape of the in-plane calibration regions: (kx, ky).
    coil_axis : int, optional
        Dimension holding coil data.
    time_axis : int, optional
        Dimension holding time data.

    Returns
    -------
    res : array_like
        Reconstructed k-space data.

    Raises
    ------
    ValueError
        When no complete ACS region can be found.

    Notes
    -----
    Implementation of the method proposed in [1]_.

    The idea is to form ACS regions using data from adjacent time
    frames.  For example, in the case of 1D undersampling using
    undersampling factor R, at least R time frames must be merged to
    form a completely sampled ACS.  Then we can simply supply the
    undersampled data and the synthesized ACS to GRAPPA.  Thus the
    heavy lifting of this function will be in determining the ACS
    calibration region at each time frame.

    References
    ----------
    .. [1] Breuer, Felix A., et al. "Dynamic autocalibrated parallel
           imaging using temporal GRAPPA (TGRAPPA)." Magnetic
           Resonance in Medicine: An Official Journal of the
           International Society for Magnetic Resonance in Medicine
           53.4 (2005): 981-985.
    '''

    # Move coil and time axes to a place we can find them
    kspace = np.moveaxis(kspace, (coil_axis, time_axis), (-2, -1))
    sx, sy, _sc, st = kspace.shape[:]
    cx, cy = calib_size[:]
    sx2, sy2 = int(sx/2), int(sy/2)
    cx2, cy2 = int(cx/2), int(cy/2)
    adjx, adjy = int(np.mod(cx, 2)), int(np.mod(cy, 2))

    # Make sure that it's even possible to create complete ACS, we'll
    # have problems in the loop if we don't catch it here!
    if not np.all(np.sum(np.abs(kspace[
            sx2-cx2:sx2+cx2+adjx,
            sy2-cy2:sy2+cy2+adjy, ...]), axis=-1)):
        raise ValueError('Full ACS region cannot be found!')

    # To avoid running GRAPPA more than once on one time frame,
    # we'll keep track of which frames have been reconstruced:
    completed_tframes = np.zeros(st, dtype=bool)

    # Initialize the progress bar
    pbar = tqdm(total=st, leave=False, desc='TGRAPPA')

    # Iterate through all time frames, construct ACS regions, and
    # run GRAPPA on each time slice
    res = np.zeros(kspace.shape, dtype=kspace.dtype)
    tt = 0 # time frame index
    done = False # True when all time frames have been consumed
    from_end = False # start at the end and go backwards
    while not done:

        # Find next feasible kernel -- Strategy: consume time frames
        # until all kernel elements have been filled.  We won't
        # assume that overlaps will not happen frame to frame, so we
        # will appropriately average each kernel position by keeping
        # track of how many samples are in a position with 'counts'
        got_kernel = False
        calib = []
        counts = np.zeros((cx, cy), dtype=int)
        tframes = [] # time frames over which the ACS is valid
        while not got_kernel:
            if not completed_tframes[tt]:
                tframes.append(tt)
            calib.append(kspace[
                sx2-cx2:sx2+cx2+adjx,
                sy2-cy2:sy2+cy2+adjy, :, tt].copy())
            counts += np.abs(calib[-1][..., 0]) > 0
            if np.all(counts > 0):
                got_kernel = True

            # Go to next time frame except maybe for the last ACS
            if not from_end:
                tt += 1 # Consume the next time frame
            else:
                tt -= 1 # Consume previous time frame

            # If we need more time frames than we have, then we need
            # to start from the end and come forward.  This can only
            # happen on the last iteration of the outer loop
            if not got_kernel and tt == st:
                # Start at the end
                tt = st-1

                # Reset ACS, counts, and tframes
                calib = []
                counts = np.zeros((cx, cy), dtype=int)
                tframes = []

                # Let the loop know we want to reverse directions
                from_end = True

        # Now average over all time frames to get a single ACS
        calib = np.sum(calib, axis=0)/counts[..., None]

        # This ACS region is valid over all time frames used to
        # create it.  Run GRAPPA on each valid time frame with calib:
        for t0 in tframes:
            res[..., t0] = grappa(
                kspace[..., t0], calib, kernel_size)
            completed_tframes[t0] = True
            pbar.update(1)

        # Stopping condition: end when all time frames are consumed
        if np.all(completed_tframes):
            done = True

    # Close out the progress bar
    pbar.close()

    # Move axes back to where the user had them
    return np.moveaxis(res, (-1, -2), (time_axis, coil_axis))
Esempio n. 11
0
    # 20x20 ACS region
    pad = 10
    ctr = int(N / 2)
    calib = kspace[ctr - pad:ctr + pad, ctr - pad:ctr + pad, :].copy()

    # R=2x2
    kspace[::2, 1::2, :] = 0
    kspace[1::2, ::2, :] = 0

    # Find Tikhonov param that minimizes NRMSE
    nlam = 20
    lamdas = np.linspace(1e-9, 5e-4, nlam)
    mse = np.zeros(lamdas.shape)
    akspace = np.abs(kspace_orig)
    for ii, lamda in tqdm(enumerate(lamdas), total=nlam, leave=False):
        recon = grappa(kspace, calib, lamda=lamda)
        mse[ii] = compare_nrmse(akspace, np.abs(recon))

    # Optimal param minimizes NRMSE
    idx = np.argmin(mse)

    # Take a look
    plt.plot(lamdas, mse)
    plt.plot(lamdas[idx], mse[idx], 'rx', label='Optimal lamda')
    plt.title('Tikhonov param vs NRMSE')
    plt.xlabel('lamda')
    plt.ylabel('NRMSE')
    plt.legend()
    plt.show()
Esempio n. 12
0
    # generate 4 coil phantom
    ph = shepp_logan(N)
    imspace = ph[..., None] * mps
    imspace = imspace.astype('complex')
    ax = (0, 1)
    kspace = 1 / np.sqrt(N**2) * np.fft.fftshift(
        np.fft.fft2(np.fft.ifftshift(imspace, axes=ax), axes=ax), axes=ax)

    # crop 20x20 window from the center of k-space for calibration
    pd = 10
    ctr = int(N / 2)
    calib = kspace[ctr - pd:ctr + pd, ctr - pd:ctr + pd, :].copy()

    # calibrate a kernel
    kernel_size = (5, 5)

    # undersample by a factor of 2 in both x and y
    kspace[::2, 1::2, :] = 0
    kspace[1::2, ::2, :] = 0

    # Time both implementations
    t0 = time()
    recon0 = grappa(kspace, calib, (5, 5))
    print(' GRAPPA: %g' % (time() - t0))

    t0 = time()
    recon1 = cgrappa(kspace, calib, (5, 5))
    print('CGRAPPA: %g' % (time() - t0))

    assert np.allclose(recon0, recon1)