"""Off Resonance correction Operator wrapper.Based on the implementation of Guillaume Daval-Frérot in pysap-mri:https://github.com/CEA-COSMIC/pysap-mri/blob/master/mri/operators/fourier/orc_wrapper.py"""importmathimportnumpyasnpfrom.._array_compatimportCUPY_AVAILABLE,AUTOGRAD_AVAILABLE,with_numpy_cupyfrom.._utilsimportget_array_modulefrom.baseimportFourierOperatorBasefrom.interfaces.utilsimportis_cuda_arrayifCUPY_AVAILABLE:importcupyascpifAUTOGRAD_AVAILABLE:importtorch
[docs]@with_numpy_cupydefget_interpolators_from_fieldmap(b0_map,readout_time,n_time_segments=6,n_bins=(40,10),mask=None,r2star_map=None):r"""Approximate ``exp(-2j*pi*fieldmap*readout_time) ≈ Σ B_n(t)C_n(r)``. Here, B_n(t) are n_time_segments temporal coefficients and C_n(r) are n_time_segments temporal spatial coefficients. The matrix B has shape ``(n_time_segments, len(readout_time))`` and C has shape ``(n_time_segments, *b0_map.shape)``. From Sigpy: https://github.com/mikgroup/sigpy and MIRT (mri_exp_approx.m): https://web.eecs.umich.edu/~fessler/code/ Parameters ---------- b0_map : np.ndarray Static field inhomogeneities map. ``b0_map`` and ``readout_time`` should have reciprocal units. Also supports Cupy arrays and Torch tensors. readout_time : np.ndarray Readout time in ``[s]`` of shape ``(n_shots, n_pts)`` or ``(n_shots * n_pts,)``. Also supports Cupy arrays and Torch tensors. n_time_segments : int, optional Number of time segments. The default is ``6``. n_bins : int | Sequence[int] optional Number of histogram bins to use for ``(B0, T2*)``. The default is ``(40, 10)`` If it is a scalar, assume ``n_bins = (n_bins, 10)``. For real fieldmap (B0 only), ``n_bins[1]`` is ignored. mask : np.ndarray, optional Boolean mask of the region of interest (e.g., corresponding to the imaged object). This is used to exclude the background fieldmap values from histogram computation. Must have same shape as ``b0_map``. The default is ``None`` (use the whole map). Also supports Cupy arrays and Torch tensors. r2star_map : np.ndarray, optional Effective transverse relaxation map (R2*). ``r2star_map`` and ``readout_time`` should have reciprocal units. Must have same shape as ``b0_map``. The default is ``None`` (purely imaginary field). Also supports Cupy arrays and Torch tensors. Notes ----- The total field map used to calculate the field coefficients is ``field_map = R2*_map + 1j * B0_map``. If R2* is not provided, the field is purely immaginary: ``field_map = 1j * B0_map``. Returns ------- B : np.ndarray Temporal interpolator of shape ``(n_time_segments, len(t))``. Array module is the same as input field_map. tl : np.ndarray Time segment centers of shape ``(n_time_segments,)``. Array module is the same as input field_map. """# defaultifnotisinstance(n_bins,(list,tuple)):n_bins=(n_bins,10)n_bins=list(n_bins)# get backend and devicexp=get_array_module(b0_map)# enforce data typesb0_map=xp.asarray(b0_map,dtype=xp.float32)readout_time=xp.asarray(readout_time,dtype=xp.float32).ravel()ifmaskisNone:mask=xp.ones_like(b0_map,dtype=bool)else:mask=xp.asarray(mask,dtype=bool)ifr2star_mapisnotNone:r2star_map=xp.asarray(r2star_map,dtype=xp.float32)# Hz to radians / sfield_map=_get_complex_fieldmap(b0_map,r2star_map)# enforce precisionfield_map=xp.asarray(field_map,dtype=xp.complex64)# create histogramsz=field_map[mask].ravel()ifr2star_mapisnotNone:z=xp.stack((z.imag,z.real),axis=1)hk,ze=xp.histogramdd(z,bins=n_bins)ze=list(ze)# get bin centerszc=[e[1:]-(e[1]-e[0])/2foreinze]# complexifyzk=_outer_sum(1j*zc[0],zc[1])# [K1 K2]zk=zk.Thk=hk.Telse:hk,ze=xp.histogram(z.imag,bins=n_bins[0])# get bin centerszc=ze[1:]-(ze[1]-ze[0])/2# complexifyzk=1j*zc# [K 1]# flatten histogram values and centershk=hk.ravel()zk=zk.ravel()# generate time for each segmenttl=xp.linspace(readout_time.min(),readout_time.max(),n_time_segments,dtype=xp.float32)# time seg centers in [s]# prepare for basis calculationch=xp.exp(-tl[:,None,...]@zk[None,...])w=xp.diag(hk**0.5)p=xp.linalg.pinv(w@ch.T)@w# actual temporal basis calculationB=p@xp.exp(-zk[:,None,...]*readout_time[None,...])B=B.astype(xp.complex64)returnB,tl
def_outer_sum(xx,yy):xx=xx[:,None,...]# add a singleton dimension at axis 1yy=yy[None,...]# add a singleton dimension at axis 0ss=xx+yy# compute the outer sumreturnss
[docs]classMRIFourierCorrected(FourierOperatorBase):"""Fourier Operator with B0 Inhomogeneities compensation. This is a wrapper around the Fourier Operator to compensate for the B0 inhomogeneities in the k-space. Parameters ---------- b0_map : np.ndarray Static field inhomogeneities map. ``b0_map`` and ``readout_time`` should have reciprocal units. Also supports Cupy arrays and Torch tensors. readout_time : np.ndarray Readout time in ``[s]`` of shape ``(n_shots, n_pts)`` or ``(n_shots * n_pts,)``. Also supports Cupy arrays and Torch tensors. n_time_segments : int, optional Number of time segments. The default is ``6``. n_bins : int | Sequence[int] optional Number of histogram bins to use for ``(B0, T2*)``. The default is ``(40, 10)`` If it is a scalar, assume ``n_bins = (n_bins, 10)``. For real fieldmap (B0 only), ``n_bins[1]`` is ignored. mask : np.ndarray, optional Boolean mask of the region of interest (e.g., corresponding to the imaged object). This is used to exclude the background fieldmap values from histogram computation. The default is ``None`` (use the whole map). Also supports Cupy arrays and Torch tensors. B : np.ndarray, optional Temporal interpolator of shape ``(n_time_segments, len(readout_time))``. tl : np.ndarray, optional Time segment centers of shape ``(n_time_segments,)``. Also supports Cupy arrays and Torch tensors. r2star_map : np.ndarray, optional Effective transverse relaxation map (R2*). ``r2star_map`` and ``readout_time`` should have reciprocal units. Must have same shape as ``b0_map``. The default is ``None`` (purely imaginary field). Also supports Cupy arrays and Torch tensors. backend: str, optional The backend to use for computations. Either 'cpu', 'gpu' or 'torch'. The default is `cpu`. Notes ----- The total field map used to calculate the field coefficients is ``field_map = R2*_map + 1j * B0_map``. If R2* is not provided, the field is purely immaginary: ``field_map = 1j * B0_map``. """def__init__(self,fourier_op,b0_map=None,readout_time=None,n_time_segments=6,n_bins=(40,10),mask=None,r2star_map=None,B=None,tl=None,backend="cpu",):ifbackend=="gpu"andnotCUPY_AVAILABLE:raiseRuntimeError("Cupy is required for gpu computations.")elifbackend=="torch":self.xp=torchelifbackend=="gpu":self.xp=cpelifbackend=="cpu":self.xp=npelse:raiseValueError("Unsupported backend.")self._fourier_op=fourier_opself.n_coils=fourier_op.n_coilsself.shape=fourier_op.shapeself.smaps=fourier_op.smapsself.autograd_available=fourier_op.autograd_availableifBisnotNoneandtlisnotNone:self.B=self.xp.asarray(B)self.tl=self.xp.asarray(tl)else:b0_map=self.xp.asarray(b0_map)self.B,self.tl=get_interpolators_from_fieldmap(b0_map,readout_time,n_time_segments,n_bins,mask,r2star_map,)ifself.BisNoneorself.tlisNone:raiseValueError("Please either provide fieldmap and t or B and tl")self.n_interpolators=self.B.shape[0]# create spatial interpolatorfield_map=_get_complex_fieldmap(b0_map,r2star_map)ifis_cuda_array(b0_map):self.C=Noneself.field_map=field_mapelse:self.C=_get_spatial_coefficients(field_map,self.tl)self.field_map=None
[docs]defop(self,data,*args):"""Compute Forward Operation with off-resonance effect. Parameters ---------- x: numpy.ndarray N-D input image. Also supports Cupy arrays and Torch tensors. Returns ------- numpy.ndarray Masked distorted N-D k-space. Array module is the same as input data. """y=0.0data_d=self.xp.asarray(data)ifself.CisnotNone:foridxinrange(self.n_interpolators):y+=self.B[idx]*self._fourier_op.op(self.C[idx]*data_d,*args)else:foridxinrange(self.n_interpolators):C=self.xp.exp(-self.field_map*self.tl[idx].item())y+=self.B[idx]*self._fourier_op.op(C*data_d,*args)returny
[docs]defadj_op(self,coeffs,*args):""" Compute Adjoint Operation with off-resonance effect. Parameters ---------- x: numpy.ndarray Masked distorted N-D k-space. Also supports Cupy arrays and Torch tensors. Returns ------- numpy.ndarray Inverse Fourier transform of the distorted input k-space. Array module is the same as input coeffs. """y=0.0coeffs_d=self.xp.asarray(coeffs)ifself.CisnotNone:foridxinrange(self.n_interpolators):y+=self.xp.conj(self.C[idx])*self._fourier_op.adj_op(self.xp.conj(self.B[idx])*coeffs_d,*args)else:foridxinrange(self.n_interpolators):C=self.xp.exp(-self.field_map*self.tl[idx].item())y+=self.xp.conj(C)*self._fourier_op.adj_op(self.xp.conj(self.B[idx])*coeffs_d,*args)returny
[docs]@staticmethoddefget_spatial_coefficients(field_map,tl):"""Compute spatial coefficients for field approximation. Parameters ---------- field_map : np.ndarray Total field map used to calculate the field coefficients is ``field_map = R2*_map + 1j * B0_map``. Also supports Cupy arrays and Torch tensors. tl : np.ndarray Time segment centers of shape ``(n_time_segments,)``. Also supports Cupy arrays and Torch tensors. Returns ------- C : np.ndarray Off-resonance phase map at each time segment center of shape ``(n_time_segments, *field_map.shape)``. Array module is the same as input field_map. """return_get_spatial_coefficients(field_map,tl)
def_get_complex_fieldmap(b0_map,r2star_map=None):xp=get_array_module(b0_map)ifr2star_mapisnotNone:r2star_map=xp.asarray(r2star_map,dtype=xp.float32)field_map=2*math.pi*(r2star_map+1j*b0_map)else:field_map=2*math.pi*1j*b0_mapreturnfield_mapdef_get_spatial_coefficients(field_map,tl):xp=get_array_module(field_map)# get spatial coeffsC=xp.exp(-tl*field_map[...,None])C=C[None,...].swapaxes(0,-1)[...,0]# (..., n_time_segments) -> (n_time_segments, ...)C=xp.asarray(C,dtype=xp.complex64)# clean-up of spatial coeffsC=xp.nan_to_num(C,nan=0.0,posinf=0.0,neginf=0.0)returnC