|
| 1 | +# The GPI core node library is licensed under |
| 2 | +# either the BSD 3-clause or the LGPL v. 3. |
| 3 | +# |
| 4 | +# Under either license, the following additional term applies: |
| 5 | +# |
| 6 | +# NO CLINICAL USE. THE SOFTWARE IS NOT INTENDED FOR COMMERCIAL |
| 7 | +# PURPOSES AND SHOULD BE USED ONLY FOR NON-COMMERCIAL RESEARCH PURPOSES. THE |
| 8 | +# SOFTWARE MAY NOT IN ANY EVENT BE USED FOR ANY CLINICAL OR DIAGNOSTIC |
| 9 | +# PURPOSES. YOU ACKNOWLEDGE AND AGREE THAT THE SOFTWARE IS NOT INTENDED FOR |
| 10 | +# USE IN ANY HIGH RISK OR STRICT LIABILITY ACTIVITY, INCLUDING BUT NOT LIMITED |
| 11 | +# TO LIFE SUPPORT OR EMERGENCY MEDICAL OPERATIONS OR USES. LICENSOR MAKES NO |
| 12 | +# WARRANTY AND HAS NOR LIABILITY ARISING FROM ANY USE OF THE SOFTWARE IN ANY |
| 13 | +# HIGH RISK OR STRICT LIABILITY ACTIVITIES. |
| 14 | +# |
| 15 | +# If you elect to license the GPI core node library under the LGPL the |
| 16 | +# following applies: |
| 17 | +# |
| 18 | +# This file is part of the GPI core node library. |
| 19 | +# |
| 20 | +# The GPI core node library is free software: you can redistribute it |
| 21 | +# and/or modify it under the terms of the GNU Lesser General Public License as |
| 22 | +# published by the Free Software Foundation, either version 3 of the License, |
| 23 | +# or (at your option) any later version. GPI core node library is distributed |
| 24 | +# in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even |
| 25 | +# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
| 26 | +# See the GNU Lesser General Public License for more details. |
| 27 | +# |
| 28 | +# You should have received a copy of the GNU Lesser General Public |
| 29 | +# License along with the GPI core node library. If not, see |
| 30 | +# <http://www.gnu.org/licenses/>. |
| 31 | + |
| 32 | + |
| 33 | +# Author: Jim Pipe |
| 34 | +# Date: 2020Oct |
| 35 | + |
| 36 | +import gpi |
| 37 | + |
| 38 | +class ExternalNode(gpi.NodeAPI): |
| 39 | + |
| 40 | + """Module to generate the gradient waveforms for a desired k-space |
| 41 | + waveform. Uses the core files spiralgencf_gen.c and spiralgencf_fill.cpp |
| 42 | + |
| 43 | + INPUTS: |
| 44 | + GIRF_in - (optional) gradient impulse response function for gradient preconditioning |
| 45 | +
|
| 46 | + OUTPUTS: |
| 47 | + crds_out - output coordinates: the last dimension is 2 (kx/ky). |
| 48 | + grd_out - gradient waveforms used to produce crds_out. Dwell time is SPGRAST |
| 49 | +
|
| 50 | + WIDGETS: most widgets self-explanatory, here are a few clarifications: |
| 51 | + min undersample - R, i.e. the undersampling relative to (1/FOV) before |
| 52 | + kr reaches usamp st |
| 53 | + max undersample - R, i.e. the undersampling relative to (1/FOV) after |
| 54 | + kr exceeds usamp end |
| 55 | + usamp st (0-1) - the relative value of kr at which undersampling begins (0 |
| 56 | + at the center, 1 at the edge of collected k-space) the samples are |
| 57 | + collected at the nyquist limit (1/FOV) prior to that |
| 58 | + usamp end (0-1) - the relative value of kr at which undersampling ends |
| 59 | + the samples are collected at the R times the nyquist limit (1/FOV) |
| 60 | + prior to that |
| 61 | + Max G Freq - limits the maximum frequency of the gradient waveform during |
| 62 | + the spiral readout. If set to 0, there is no limit (default minimum is 0.5 kHz) |
| 63 | + Start Window - time for rounding enforced when starting |
| 64 | + Corner Window - an angle determining the rounding enforced when |
| 65 | + transitioning between (freq, slew,grad) limits |
| 66 | + spinout - controls spiral in, out, etc. |
| 67 | + OUT - generate spiral-out waveform |
| 68 | + IN - generate spiral-in waveform |
| 69 | + OUT180 - generate spiral-out waveform and negate the waveform |
| 70 | +
|
| 71 | + **************************************** |
| 72 | + *** The concept of "true resolution" *** |
| 73 | + k-space data are typically normalized, for the gridding process, to values |
| 74 | + between -0.5 and 0.5 For spiral data (which measure circular k-space) to |
| 75 | + have the same resolution as Cartesian (square k-space) they must measure a |
| 76 | + diameter of 2/sqrt(pi) ~ 1.13 larger than conventional k-space limits (so |
| 77 | + that the area of the circle equals the area of the square). k-space |
| 78 | + coordinates, therefore, are multiplied by 0.8, so that their range of |
| 79 | + 0.8*2/sqrt(pi) = 0.903, or -0.451 to +0.451, fits within the gridded space. |
| 80 | + The resulting image, with no further zero-padding, will have pixels that |
| 81 | + are 0.8 times smaller than the requested resolution, with a matrix 25% |
| 82 | + larger in each dimension. This is a semi-complicated way of making sure |
| 83 | + that this works routinely, and is referred to by the authors as "true |
| 84 | + resolution". |
| 85 | + ***************************************** |
| 86 | + """ |
| 87 | + |
| 88 | + def execType(self): |
| 89 | + return gpi.GPI_PROCESS |
| 90 | + |
| 91 | + def initUI(self): |
| 92 | + |
| 93 | + import numpy as np |
| 94 | + |
| 95 | + # Widgets |
| 96 | + self.addWidget('PushButton', 'compute', toggle=True) |
| 97 | + self.addWidget('TextBox', 'Info:') |
| 98 | + |
| 99 | + self.addWidget('DoubleSpinBox', 'FOV (cm)', |
| 100 | + val=24.0, min=0.1, decimals=6) |
| 101 | + self.addWidget('DoubleSpinBox', 'Res (mm)', |
| 102 | + val=0.8, min=0.1, singlestep=0.1, decimals=5) |
| 103 | + self.addWidget('SpinBox', '# of Spiral Arms', val=16, min=1) |
| 104 | + |
| 105 | + self.addWidget('ExclusivePushButtons', 'spinout', |
| 106 | + buttons=['OUT', 'IN', 'OUT180'],val=0) |
| 107 | + |
| 108 | + self.addWidget('DoubleSpinBox', 'AD dwell time (us)', |
| 109 | + val=1.0, min=0.1, decimals=6) |
| 110 | + self.addWidget('DoubleSpinBox', 'MaxSlw (mT/m/ms)', |
| 111 | + val=150.0, min=0.01, decimals=6) |
| 112 | + self.addWidget('DoubleSpinBox', 'MaxGrd (mT/m)', |
| 113 | + val=40.0, min=0.01, decimals=6) |
| 114 | + |
| 115 | + self.addWidget('DoubleSpinBox', 'min undersample', |
| 116 | + val=1.0, min=0.0, max=100.0) |
| 117 | + self.addWidget('DoubleSpinBox', 'max undersample', |
| 118 | + val=1.0, min=0.0, max=100.0) |
| 119 | + self.addWidget('DoubleSpinBox', 'usamp st (0 - 1)', |
| 120 | + val=0.0, min=0.0, max=1.0, singlestep=0.01) |
| 121 | + self.addWidget('DoubleSpinBox', 'usamp end (0 - 1)', |
| 122 | + val=1.0, min=0.0, max=1.0, singlestep=0.01) |
| 123 | + |
| 124 | + self.addWidget('PushButton', 'Precompensate', toggle=True, val=1) |
| 125 | + self.addWidget('PushButton', 'Precondition', toggle=True, val=1) |
| 126 | + |
| 127 | + self.addWidget('DoubleSpinBox', 'Max G Freq (kHz)', val=1.0, min=0.5, max=20.) |
| 128 | + self.addWidget('DoubleSpinBox', 'Start Window (us)', val=200.0, min=0.0) |
| 129 | + self.addWidget('DoubleSpinBox', 'End Window (us)', val=100.0, min=0.0) |
| 130 | + self.addWidget('DoubleSpinBox', 'Corner Window (cycles)', val=0.5, min=0.0) |
| 131 | + |
| 132 | + self.addWidget('DoubleSpinBox', 'TrueRes Factor', val=1.0, |
| 133 | + min=0.0, max=1.0) |
| 134 | + |
| 135 | + self.addWidget('DoubleSpinBox', 'x delay (us)', |
| 136 | + val=0.0, min=-100.0, max=100.0, visible = False) |
| 137 | + self.addWidget('DoubleSpinBox', 'y delay (us)', |
| 138 | + val=0.0, min=-100.0, max=100.0, visible = False) |
| 139 | + |
| 140 | + self.addWidget('DoubleSpinBox', 'Gam (kHz/mT)', |
| 141 | + val=42.577, min=0.01, decimals=6, visible = False) # hide this until we need it |
| 142 | + |
| 143 | + # IO Ports |
| 144 | + self.addInPort('GIRF_in', 'NPYarray',dtype=[np.float32,np.float64],ndim=1,obligation=gpi.OPTIONAL) |
| 145 | + self.addOutPort('crds_out', 'NPYarray') |
| 146 | + self.addOutPort('grd_out', 'NPYarray') |
| 147 | + self.addOutPort('gtf_out', 'NPYarray') |
| 148 | + |
| 149 | + def compute(self): |
| 150 | + |
| 151 | + import numpy as np |
| 152 | + |
| 153 | + # convert units to ms, kHz, m, mT |
| 154 | + dwell = 0.001 * self.getVal('AD dwell time (us)') |
| 155 | + xdely = 0.001 * self.getVal('x delay (us)') |
| 156 | + ydely = 0.001 * self.getVal('y delay (us)') |
| 157 | + |
| 158 | + mslew = self.getVal('MaxSlw (mT/m/ms)') |
| 159 | + mgrad = self.getVal('MaxGrd (mT/m)') |
| 160 | + gamma = self.getVal('Gam (kHz/mT)') |
| 161 | + |
| 162 | + fov = 0.01 * self.getVal('FOV (cm)') |
| 163 | + |
| 164 | + narms = float(self.getVal('# of Spiral Arms')) |
| 165 | + |
| 166 | + trures_fac = self.getVal('TrueRes Factor') |
| 167 | + trures_acq = trures_fac * np.sqrt(np.pi)/2 + 1 - trures_fac |
| 168 | + |
| 169 | + res = 0.001 * self.getVal('Res (mm)') |
| 170 | + |
| 171 | + us_0 = self.getVal('usamp st (0 - 1)') |
| 172 | + us_1 = self.getVal('usamp end (0 - 1)') |
| 173 | + us_r0 = self.getVal('min undersample') |
| 174 | + us_r = self.getVal('max undersample') |
| 175 | + |
| 176 | + precomp = self.getVal('Precompensate') |
| 177 | + precond = self.getVal('Precondition') |
| 178 | + |
| 179 | + mgfrq = self.getVal('Max G Freq (kHz)') |
| 180 | + start_win = 0.001*self.getVal('Start Window (us)') # change to ms |
| 181 | + end_win = 0.001*self.getVal('End Window (us)') # change to ms |
| 182 | + corner_win = 2.*np.pi*self.getVal('Corner Window (cycles)') # change to radians |
| 183 | + |
| 184 | + spinout = self.getVal('spinout') |
| 185 | + |
| 186 | + if self.getVal('compute'): |
| 187 | + |
| 188 | + girf = self.getData('GIRF_in') |
| 189 | + if girf is not None: |
| 190 | + # The first point should not be 0 |
| 191 | + while girf[0] == 0: |
| 192 | + girf = girf[1:] |
| 193 | + # normalize to have unit area, so DC part of MTF is 1 |
| 194 | + girf = np.float64(girf)/np.sum(np.float64(girf)) |
| 195 | + else: |
| 196 | + # Make it a delta function |
| 197 | + girf = np.float64(np.array([1.,0.])) |
| 198 | + |
| 199 | + gtf_res = 0.05 # spectral resolution in kHz |
| 200 | + spgrast = 0.005 # gradient raster in ms |
| 201 | + # gtf_len*spgrast = 1/gtf_res |
| 202 | + # force gtf_len to be even |
| 203 | + gtf_len = 2*int(0.5/(gtf_res*spgrast)) |
| 204 | + |
| 205 | + gtf = np.absolute(np.fft.fft(np.pad(girf,(0,gtf_len-girf.shape[0])))) |
| 206 | + |
| 207 | + # import in thread to save namespace |
| 208 | + # spiralgencf corresponds to spiralgencf_PyMOD.cpp |
| 209 | + import gpi_core.spiral.spiralgencf as sp |
| 210 | + |
| 211 | + print("end win",end_win) |
| 212 | + grd_out, crds_out = sp.coords( |
| 213 | + girf,gtf,dwell, xdely, ydely, mslew, mgrad, gamma, |
| 214 | + fov, res, narms, |
| 215 | + us_0, us_1, us_r0, us_r, |
| 216 | + mgfrq, precomp, precond, start_win, end_win, corner_win, spinout, |
| 217 | + trures_acq) |
| 218 | + |
| 219 | + # Report Back to User |
| 220 | + nsamp = np.array(crds_out.shape)[-2] |
| 221 | + |
| 222 | + spgrast = 0.005 # Gradient raster time in ms |
| 223 | + |
| 224 | + smp = "Samples: " + str(nsamp) + "\n" |
| 225 | + tau = "Tau (ms): " + str(dwell * nsamp) + "\n" |
| 226 | + tgrad = spgrast * np.array(grd_out.shape)[1] |
| 227 | + tgr = "TGrad (ms): " + str(tgrad) + "\n" |
| 228 | + info = smp + tau + tgr |
| 229 | + self.setAttr('Info:', val=info) |
| 230 | + |
| 231 | + grd_out = grd_out[..., 0:2] |
| 232 | + |
| 233 | + self.setData('crds_out', crds_out) |
| 234 | + self.setData('grd_out', grd_out) |
| 235 | + self.setData('gtf_out', gtf) |
| 236 | + |
| 237 | + return(0) |
0 commit comments