├── .gitmodules ├── methods ├── KernelMode.m ├── SliceOrientation.m ├── PhaseEncodingDirection.m ├── GetMRSystemSpecs.m ├── fnint.m └── GetAxesOrderAndSign.m ├── waveforms ├── sweepWaveform.mat └── spiralGrad_FOV192_RES1mm_minRise6_maxAmp40_nitlv16.mat ├── exports ├── skope_offresAndPosCalib.seq ├── skope_gtf_one_ave.seq └── skope_gtf_linearityCheck.seq ├── sequences ├── skope_offresAndPosCalib.m ├── skope_sweep.m ├── SequenceParams.m ├── skope_localEddyCalib.m ├── skope_gtf.m ├── PulseqBase.m ├── skope_spiral_2d.m ├── skope_gre_3d.m └── skope_gre_2d.m ├── docs ├── sequenceDiagramGtf.svg ├── sequenceDiagramOffresAndPosCalibration.svg ├── sequenceDiagramEPI2D.svg ├── sequenceDiagramGRE2D.svg ├── sequenceDiagramLocalEddyCurrentCalibration.svg └── triggerSkipping.svg ├── CreateSequences.m └── README.md /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "pulseq"] 2 | path = pulseq 3 | url = https://github.com/pulseq/pulseq 4 | -------------------------------------------------------------------------------- /methods/KernelMode.m: -------------------------------------------------------------------------------- 1 | classdef KernelMode 2 | enumeration 3 | Sync, Dummy, Imaging 4 | end 5 | end -------------------------------------------------------------------------------- /methods/SliceOrientation.m: -------------------------------------------------------------------------------- 1 | classdef SliceOrientation 2 | enumeration 3 | TRA, SAG, COR 4 | end 5 | end -------------------------------------------------------------------------------- /methods/PhaseEncodingDirection.m: -------------------------------------------------------------------------------- 1 | classdef PhaseEncodingDirection 2 | enumeration 3 | AP, PA, RL, LR, HF, FH 4 | end 5 | end -------------------------------------------------------------------------------- /waveforms/sweepWaveform.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SkopeMagneticResonanceTechnologies/Pulseq-Sequences/HEAD/waveforms/sweepWaveform.mat -------------------------------------------------------------------------------- /waveforms/spiralGrad_FOV192_RES1mm_minRise6_maxAmp40_nitlv16.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SkopeMagneticResonanceTechnologies/Pulseq-Sequences/HEAD/waveforms/spiralGrad_FOV192_RES1mm_minRise6_maxAmp40_nitlv16.mat -------------------------------------------------------------------------------- /methods/GetMRSystemSpecs.m: -------------------------------------------------------------------------------- 1 | function specs = GetMRSystemSpecs(scannerType) 2 | % Define scanner specs 3 | 4 | % (c) 2024 Skope Magnetic Resonance Technologies AG 5 | 6 | specs = []; 7 | specs.maxGrad_unit = 'mT/m'; 8 | specs.maxSlew_unit = 'T/m/s'; 9 | 10 | switch scannerType 11 | case 'Siemens 3T' 12 | specs.type = scannerType; 13 | specs.maxGrad = 40; 14 | specs.maxSlew = 200; 15 | specs.B0 = 2.98; 16 | case 'Siemens Terra 7T SC72CD' 17 | specs.type = scannerType; 18 | specs.maxGrad = 40; 19 | specs.maxSlew = 200; 20 | specs.B0 = 6.98; 21 | case 'Siemens 9.4T SC72CD' 22 | specs.type = scannerType; 23 | specs.maxGrad = 40; 24 | specs.maxSlew = 200; 25 | specs.B0 = 9.385; 26 | otherwise 27 | error('Unknown scanner type'); 28 | end 29 | 30 | end -------------------------------------------------------------------------------- /exports/skope_offresAndPosCalib.seq: -------------------------------------------------------------------------------- 1 | # Pulseq sequence file 2 | # Created by MATLAB mr toolbox 3 | 4 | [VERSION] 5 | major 1 6 | minor 4 7 | revision 1 8 | 9 | [DEFINITIONS] 10 | AdcRasterTime 1e-07 11 | BlockDurationRaster 1e-05 12 | CameraAcqDuration 0.1 13 | CameraAqDelay 0 14 | CameraInterleaveTR 0.4 15 | CameraNrDynamics 4 16 | CameraNrSyncDynamics 0 17 | GradientRasterTime 1e-05 18 | Name opc 19 | RadiofrequencyRasterTime 1e-06 20 | 21 | # Format of blocks: 22 | # NUM DUR RF GX GY GZ ADC EXT 23 | [BLOCKS] 24 | 1 110002 0 1 0 0 0 1 25 | 2 10000 0 0 0 0 0 0 26 | 3 110004 0 2 0 0 0 1 27 | 4 10000 0 0 0 0 0 0 28 | 5 110004 0 0 2 0 0 1 29 | 6 10000 0 0 0 0 0 0 30 | 7 110004 0 0 0 2 0 1 31 | 8 10000 0 0 0 0 0 0 32 | 9 2 0 3 0 0 0 0 33 | 34 | # Format of arbitrary gradients: 35 | # time_shape_id of 0 means default timing (stepping with grad_raster starting at 1/2 of grad_raster) 36 | # id amplitude amp_shape_id time_shape_id delay 37 | # .. Hz/m .. .. us 38 | [GRADIENTS] 39 | 3 0 1 2 0 40 | 41 | # Format of trapezoid gradients: 42 | # id amplitude rise flat fall delay 43 | # .. Hz/m us us us us 44 | [TRAP] 45 | 1 0 10 1100000 10 0 46 | 2 106440 20 1100000 20 0 47 | 48 | # Format of extension lists: 49 | # id type ref next_id 50 | # next_id of 0 terminates the list 51 | # Extension list is followed by extension specifications 52 | [EXTENSIONS] 53 | 1 1 1 0 54 | 55 | # Extension specification for digital output and input triggers: 56 | # id type channel delay (us) duration (us) 57 | extension TRIGGERS 1 58 | 1 1 3 990000 10 59 | 60 | # Sequence Shapes 61 | [SHAPES] 62 | 63 | shape_id 1 64 | num_samples 2 65 | 0 66 | 0 67 | 68 | shape_id 2 69 | num_samples 2 70 | 0.5 71 | 1.5 72 | 73 | 74 | [SIGNATURE] 75 | # This is the hash of the Pulseq file, calculated right before the [SIGNATURE] section was added 76 | # It can be reproduced/verified with md5sum if the file trimmed to the position right above [SIGNATURE] 77 | # The new line character preceding [SIGNATURE] BELONGS to the signature (and needs to be sripped away for recalculating/verification) 78 | Type md5 79 | Hash ed2d0aa9fbbb8af66a10533f3e0eeb79 80 | -------------------------------------------------------------------------------- /methods/fnint.m: -------------------------------------------------------------------------------- 1 | %% Copyright notice 2 | % 3 | % Retrieved from https://www.mathworks.com/matlabcentral/fileexchange/71225-splinefit 4 | % 5 | % Copyright (c) 2009, Jonas Lundgren 6 | % All rights reserved. 7 | % 8 | % Redistribution and use in source and binary forms, with or without 9 | % modification, are permitted provided that the following conditions are met: 10 | % 11 | % * Redistributions of source code must retain the above copyright notice, this 12 | % list of conditions and the following disclaimer. 13 | % * Redistributions in binary form must reproduce the above copyright notice, 14 | % this list of conditions and the following disclaimer in the documentation 15 | % and/or other materials provided with the distribution 16 | % * Neither the name of none nor the names of its 17 | % contributors may be used to endorse or promote products derived from this 18 | % software without specific prior written permission. 19 | % THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | % AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | % IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | % DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 23 | % FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | % DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | % SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | % CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | % OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | % OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | function output = fnint(pp,a,b) 31 | %PPINT Integrate piecewise polynomial. 32 | % QQ = PPINT(PP,A) returns the indefinite integral from A to X of a 33 | % piecewise polynomial PP. PP must be on the form evaluated by PPVAL. 34 | % QQ is a piecewise polynomial on the same form. Default value for A is 35 | % the leftmost break of PP. 36 | % 37 | % I = PPINT(PP,A,B) returns the definite integral from A to B. 38 | % 39 | % Example: 40 | % x = linspace(-pi,pi,7); 41 | % y = sin(x); 42 | % pp = spline(x,y); 43 | % I = ppint(pp,0,pi) 44 | % 45 | % qq = ppint(pp,pi/2); 46 | % xx = linspace(-pi,pi,201); 47 | % plot(xx,-cos(xx),xx,ppval(qq,xx),'r') 48 | % 49 | % See also PPVAL, SPLINE, SPLINEFIT, PPDIFF 50 | % Author: Jonas Lundgren 2009 51 | 52 | if nargin < 1, help ppint, return, end 53 | if nargin < 2, a = pp.breaks(1); end 54 | % Get coefficients and breaks 55 | coefs = pp.coefs; 56 | [m,n] = size(coefs); 57 | xb = pp.breaks; 58 | pdim = prod(pp.dim); 59 | % Interval lengths 60 | hb = diff(xb); 61 | hb = repmat(hb,pdim,1); 62 | hb = hb(:); 63 | % Integration 64 | coefs(:,1) = coefs(:,1)/n; 65 | y = coefs(:,1).*hb; 66 | for k = 2:n 67 | coefs(:,k) = coefs(:,k)/(n-k+1); 68 | y = (y + coefs(:,k)).*hb; 69 | end 70 | y = reshape(y,pdim,[]); 71 | I = cumsum(y,2); 72 | I = I(:); 73 | coefs(:,n+1) = [zeros(pdim,1); I(1:m-pdim)]; 74 | % Set preliminary indefinite integral 75 | qq = pp; 76 | qq.coefs = coefs; 77 | qq.order = n+1; 78 | % Set output 79 | if nargin < 3 80 | % Indefinite integral from a to x 81 | if a ~= xb(1) 82 | I0 = ppval(qq,a); 83 | I0 = I0(:); 84 | I0 = repmat(I0,m/pdim,1); 85 | qq.coefs(:,n+1) = qq.coefs(:,n+1) - I0; 86 | end 87 | output = qq; 88 | else 89 | % Definite integral from a to b 90 | output = ppval(qq,b) - ppval(qq,a); 91 | end 92 | -------------------------------------------------------------------------------- /sequences/skope_offresAndPosCalib.m: -------------------------------------------------------------------------------- 1 | classdef skope_offresAndPosCalib < PulseqBase 2 | % Sequence for off-resonance and position calibration 3 | 4 | % (c) 2024 Skope Magnetic Resonance Technologies AG 5 | 6 | properties 7 | axis = ['x', 'y', 'z']; % Axis with blips 8 | flattopTime = 1100e-3; % [s] 9 | gradientAmplitude = 2.5; % [mT/m] 10 | end 11 | 12 | methods 13 | 14 | function obj = skope_offresAndPosCalib(seqParams, varargin) 15 | 16 | %% Check input structure 17 | if not(isa(seqParams,'SequenceParams')) 18 | error('Input need to be a SequenceParams object.'); 19 | end 20 | 21 | %% Set base class properties 22 | obj.TR = seqParams.TR; % Repetition time [Unit: s] 23 | obj.gradFreeTime = 0.5e-3; % Delay between trigger and blip-train [Unit: s] 24 | 25 | T_trig_delay = 990e-3; % trigger delay [s] 26 | 27 | %% Get system limits 28 | specs = GetMRSystemSpecs(seqParams.scannerType); 29 | 30 | if not(strcmpi(specs.maxGrad_unit,'mT/m')) 31 | error('Expected mT/m for maximum gradient.'); 32 | end 33 | 34 | if not(strcmpi(specs.maxSlew_unit,'T/m/s')) 35 | error('Expected T/m/s for slew rate.'); 36 | end 37 | 38 | %% Check specs 39 | if seqParams.maxGrad > specs.maxGrad 40 | error('Scanner does not support requested gradient amplitude.'); 41 | end 42 | if seqParams.maxSlew > specs.maxSlew 43 | error('Scanner does not support requested slew rate.'); 44 | end 45 | 46 | % Set system limits 47 | obj.sys = mr.opts('MaxGrad', seqParams.maxGrad, ... 48 | 'GradUnit','mT/m',... 49 | 'MaxSlew', seqParams.maxSlew, ... 50 | 'SlewUnit','T/m/s',... 51 | 'rfRingdownTime', 30e-6, ... 52 | 'rfDeadtime', 100e-6,... 53 | 'B0', specs.B0 ... 54 | ); 55 | 56 | %% Create a new sequence object 57 | obj.seq = mr.Sequence(obj.sys); 58 | 59 | %% Set parameters 60 | grad_amp_Hzm = mr.convert(obj.gradientAmplitude, 'mT/m', 'Hz/m', 'gamma', obj.sys.gamma); 61 | 62 | T_inter = 100e-3; % delay between event-blocks [s] 63 | 64 | %% Prepare event objects and combine to eventblocks 65 | area_flattop = grad_amp_Hzm * obj.flattopTime; 66 | mr_trig = mr.makeDigitalOutputPulse('ext1','duration', obj.sys.gradRasterTime, 'delay', T_trig_delay); 67 | mr_inter = mr.makeDelay(T_inter); 68 | 69 | % no-grad 70 | mr_G_ = mr.makeTrapezoid('x', 'FlatTime', obj.flattopTime, 'FlatArea', 0); % could be replaced by delay 71 | obj.seq.addBlock(mr_trig, mr_G_); 72 | obj.seq.addBlock(mr_inter); 73 | 74 | for ax = obj.axis 75 | mr_G_ = mr.makeTrapezoid(ax, 'FlatTime', obj.flattopTime, 'FlatArea', area_flattop); 76 | obj.seq.addBlock(mr_trig, mr_G_); 77 | obj.seq.addBlock(mr_inter) 78 | end 79 | 80 | % Required by PulSeq IDEA 81 | obj.seq.addBlock(PulseqBase.makeDummy); 82 | 83 | % Number of external triggers 84 | obj.nTrig = 4; 85 | 86 | %% Prepare sequence export 87 | obj.seq.setDefinition('Name', 'opc'); 88 | obj.seq.setDefinition('CameraNrDynamics', obj.nTrig); 89 | obj.seq.setDefinition('CameraNrSyncDynamics', 0); 90 | obj.seq.setDefinition('CameraAcqDuration', 0.1); 91 | obj.seq.setDefinition('CameraInterleaveTR', 0.4); 92 | obj.seq.setDefinition('CameraAqDelay', 0); 93 | 94 | %% Write to Pulseq file 95 | if not(isfolder('exports')) 96 | mkdir('exports') 97 | end 98 | obj.seq.write('exports/skope_offresAndPosCalib.seq') 99 | 100 | end 101 | end 102 | end -------------------------------------------------------------------------------- /sequences/skope_sweep.m: -------------------------------------------------------------------------------- 1 | classdef skope_sweep < PulseqBase 2 | % Sequence for off-resonance and position calibration 3 | 4 | % (c) 2024 Skope Magnetic Resonance Technologies AG 5 | 6 | properties 7 | axis = ['x', 'y', 'z']; % Axis with blips 8 | flattopTime = 1100e-3; % [s] 9 | gradientAmplitude = 2.5; % [mT/m] 10 | end 11 | 12 | methods 13 | 14 | function obj = skope_sweep(seqParams, sweepWaveform) 15 | 16 | %% Check input structure 17 | if not(isa(seqParams,'SequenceParams')) 18 | error('Input need to be a SequenceParams object.'); 19 | end 20 | 21 | %% Set base class properties 22 | obj.gradFreeTime = 0.5e-3; % Delay between trigger and blip-train [Unit: s] 23 | obj.nAve = seqParams.nAve; 24 | obj.TR = seqParams.TR; 25 | 26 | T_trig_delay = 1e-3; % trigger delay [s] 27 | 28 | %% Axes order 29 | [obj.axesOrder, obj.axesSign, readDir_SCT, phaseDir_SCT, sliceDir_SCT] ... 30 | = GetAxesOrderAndSign(obj.sliceOrientation,obj.phaseEncDir); 31 | 32 | %% Get system limits 33 | specs = GetMRSystemSpecs(seqParams.scannerType); 34 | 35 | if not(strcmpi(specs.maxGrad_unit,'mT/m')) 36 | error('Expected mT/m for maximum gradient.'); 37 | end 38 | 39 | if not(strcmpi(specs.maxSlew_unit,'T/m/s')) 40 | error('Expected T/m/s for slew rate.'); 41 | end 42 | 43 | %% Check specs 44 | if seqParams.maxGrad > specs.maxGrad 45 | error('Scanner does not support requested gradient amplitude.'); 46 | end 47 | if seqParams.maxSlew > specs.maxSlew 48 | error('Scanner does not support requested slew rate.'); 49 | end 50 | 51 | % Set system limits 52 | obj.sys = mr.opts('MaxGrad', seqParams.maxGrad, ... 53 | 'GradUnit','mT/m',... 54 | 'MaxSlew', seqParams.maxSlew, ... 55 | 'SlewUnit','T/m/s',... 56 | 'rfRingdownTime', 30e-6, ... 57 | 'rfDeadtime', 100e-6,... 58 | 'B0', specs.B0 ... 59 | ); 60 | 61 | %% Create a new sequence object 62 | obj.seq = mr.Sequence(obj.sys); 63 | 64 | %% Set parameters 65 | amp = max(sweepWaveform); 66 | grad_amp_Hzm = mr.convert(amp, 'mT/m', 'Hz/m', 'gamma', obj.sys.gamma); 67 | 68 | %% Prepare event objects and combine to eventblocks 69 | mr_trig = mr.makeDigitalOutputPulse('ext1','duration', obj.sys.gradRasterTime, 'delay', T_trig_delay); 70 | 71 | mr_gradfree = mr.makeDelay(2e-3); 72 | 73 | for ax = obj.axis 74 | for avg = 1:obj.nAve 75 | 76 | % Add trigger 77 | obj.addBlock(mr_trig); 78 | 79 | % Add delay after trigger 80 | obj.addBlock(mr_gradfree); 81 | 82 | % Play out waveform 83 | g = mr.makeArbitraryGrad(ax,sweepWaveform*grad_amp_Hzm); 84 | obj.addBlock(g); 85 | 86 | totalTime = mr.calcDuration(mr_trig) + ... 87 | mr.calcDuration(mr_gradfree) + ... 88 | mr.calcDuration(g); 89 | 90 | mr_inter = mr.makeDelay(obj.TR - totalTime); 91 | 92 | % Wait for next waveform 93 | obj.addBlock(mr_inter); 94 | end 95 | end 96 | 97 | % Required by PulSeq IDEA 98 | obj.addBlock(PulseqBase.makeDummy); 99 | 100 | % Number of external triggers 101 | obj.nTrig = 3*obj.nAve; 102 | 103 | %% Prepare sequence export 104 | obj.seq.setDefinition('Name', 'sweep'); 105 | obj.seq.setDefinition('CameraNrDynamics', obj.nTrig); 106 | obj.seq.setDefinition('CameraNrSyncDynamics', 0); 107 | obj.seq.setDefinition('CameraAcqDuration', 0.070); 108 | obj.seq.setDefinition('CameraInterleaveTR', 0.4); 109 | obj.seq.setDefinition('CameraAqDelay', 0); 110 | 111 | %% Write to Pulseq file 112 | if not(isfolder('exports')) 113 | mkdir('exports') 114 | end 115 | if obj.nAve == 1 116 | obj.seq.write('exports/skope_sweep_one_ave.seq') 117 | else 118 | obj.seq.write('exports/skope_sweep.seq') 119 | end 120 | 121 | end 122 | end 123 | end -------------------------------------------------------------------------------- /sequences/SequenceParams.m: -------------------------------------------------------------------------------- 1 | classdef SequenceParams 2 | %SEQUENCEPARAMS Default sequence parameters 3 | 4 | properties 5 | fov % Field of view [Unit: m] 6 | Nx % Number of readout samples 7 | Ny % Number of phase encoding steps 8 | Nz % Number of 3D phase encoding steps 9 | alpha % Flip angle [Unit: deg] 10 | thickness % Slice thickness [Unit: m] 11 | nSlices % Number of slices 12 | TE % Echo times [Unit: s] 13 | TR % Excitation repetition time [Unit: s] 14 | readoutTime % ADC duration [Unit: s] 15 | maxGrad % Used gradient amplitude by sequence 16 | maxSlew % Used slew rate by sequence 17 | 18 | % Defaults 19 | scannerType = 'Siemens 9.4T SC72CD'; 20 | nRep = 1; % Number of repetitions 21 | nAve = 1; % Number of averages 22 | mode = 'default'; % Allow to switch between different versions 23 | doPlayFatSat = false; % Play out fat-saturation pulse (for EPI) 24 | nDummy = 0; % Number of dummy pulses to reach steady state 25 | accFacPE = 1; % Acceleration factor [Phase] (only used for EPI at the moment) 26 | 27 | sliceOrientation = SliceOrientation.TRA; % Slice orientation 28 | phaseEncDir = PhaseEncodingDirection.AP; % Phase encoding direction 29 | end 30 | 31 | methods 32 | function obj = SequenceParams(seqName,mode) 33 | 34 | if not(exist('mode','var')) 35 | mode = 'default'; 36 | end 37 | 38 | switch lower(seqName) 39 | case 'gre2d' 40 | obj.fov = 200e-3; 41 | obj.Nx = 128; 42 | obj.Ny = obj.Nx; 43 | obj.alpha = 7; 44 | obj.thickness = 3e-3; 45 | obj.nSlices = 15; 46 | obj.TE = [6 12] * 1e-3; 47 | obj.TR = 25e-3; 48 | obj.readoutTime = 3.2e-3; 49 | obj.maxGrad = 28; 50 | obj.maxSlew = 150; 51 | obj.nDummy = 10; 52 | case 'epi2d' 53 | obj.TE = 33e-3; 54 | obj.TR = 200e-3; 55 | obj.readoutTime = 0.680e-3; 56 | obj.alpha = 90; 57 | obj.fov = 200e-3; 58 | obj.Nx = 80; 59 | obj.Ny = 80; 60 | obj.thickness = 3e-3; 61 | obj.nSlices = 15; 62 | obj.maxGrad = 32; 63 | obj.maxSlew = 130; 64 | obj.nDummy = 5; 65 | obj.nRep = 10; 66 | case 'gre3d' 67 | obj.fov = [0.56 0.56 0.56]*1e-2*40053000/42577481; 68 | obj.Nx = 56; 69 | obj.Ny = obj.Nx; 70 | obj.Nz = obj.Nx; 71 | obj.alpha = 1; 72 | obj.TE = [12.3 28.16] * 1e-3 + 1e-3; % one millisecond for phase estimation 73 | obj.TR = 100e-3; 74 | obj.readoutTime = 7.84e-3; 75 | obj.maxGrad = 35; 76 | obj.maxSlew = 150; 77 | obj.nDummy = 50; 78 | 79 | case 'spiral2d' 80 | % spiral-trajectory not adaptive to input params (hard coded) 81 | % don't change! 82 | obj.fov = 192e-3; 83 | obj.Nx = 192; 84 | obj.Ny = 16; 85 | obj.alpha = 15; 86 | obj.thickness = 3e-3; 87 | obj.nSlices = 15; 88 | obj.TE = 2.5 * 1e-3; 89 | obj.TR = 140e-3; 90 | obj.readoutTime = 8e-3; 91 | obj.maxGrad = 40; 92 | obj.maxSlew = 150; 93 | case 'gtf' 94 | if strcmpi(mode,'default') 95 | obj.TR = 1; 96 | obj.maxGrad = 40; 97 | obj.maxSlew = 200; 98 | obj.nAve = 5; 99 | obj.mode = mode; 100 | elseif strcmpi(mode,'linearityCheck') 101 | obj.TR = 1; 102 | obj.maxGrad = 40; 103 | obj.maxSlew = 200; 104 | obj.nAve = 1; 105 | obj.mode = mode; 106 | else 107 | error('Unknown sequence mode.') 108 | end 109 | case 'opc' 110 | obj.TR = 200e-3; 111 | obj.maxGrad = 40; 112 | obj.maxSlew = 200; 113 | case 'sweep' 114 | obj.TR = 1; 115 | obj.maxGrad = 40; 116 | obj.maxSlew = 200; 117 | obj.nAve = 100; 118 | case 'lec' 119 | obj.TR = 200e-3; 120 | obj.maxGrad = 40; 121 | obj.maxSlew = 200; 122 | otherwise 123 | error(['Unknown sequence: ', seqName]) 124 | end 125 | end 126 | end 127 | end -------------------------------------------------------------------------------- /methods/GetAxesOrderAndSign.m: -------------------------------------------------------------------------------- 1 | function [axesOrder, axesSign, readDir_SCT, phaseDir_SCT, sliceDir_SCT] = GetAxesOrderAndSign(sliceOrientation,phaseEncDir) 2 | % SIEMENS physical coordinate system: 3 | % X: (Your) Left -> (Your) Right | Y: Down -> Up | Z: Rear -> Front (Towards you) 4 | % 5 | % Vendor independent patient coordinte system: SAG/COR/TRA for HFS 6 | % S: (Your) Left -> (Your) Right | C: Up -> Down | T: Front -> Rear (Away from you) 7 | 8 | % (c) 2024 Skope Magnetic Resonance Technologies AG 9 | 10 | switch sliceOrientation 11 | case SliceOrientation.TRA 12 | switch phaseEncDir 13 | case PhaseEncodingDirection.AP 14 | axesOrder = {'x','y','z'}; % Read, phase, slice 15 | axesSign = [-1,1,1]; 16 | readDir_SCT = [-1,0,0]; 17 | phaseDir_SCT = [0,1,0]; 18 | sliceDir_SCT = [0,0,1]; 19 | case PhaseEncodingDirection.PA 20 | axesOrder = {'x','y','z'}; 21 | axesSign = [1,-1,1]; 22 | readDir_SCT = [1,0,0]; 23 | phaseDir_SCT = [0,-1,0]; 24 | sliceDir_SCT = [0,0,1]; 25 | case PhaseEncodingDirection.RL 26 | axesOrder = {'y','x','z'}; 27 | axesSign = [-1,-1,1]; 28 | readDir_SCT = [0,1,0]; 29 | phaseDir_SCT = [1,0,0]; 30 | sliceDir_SCT = [0,0,1]; 31 | case PhaseEncodingDirection.LR 32 | axesOrder = {'y','x','z'}; 33 | axesSign = [1,1,1]; 34 | readDir_SCT = [0,-1,0]; 35 | phaseDir_SCT = [-1,0,0]; 36 | sliceDir_SCT = [0,0,1]; 37 | otherwise 38 | error('Wrong phase encoding direction.'); 39 | end 40 | case SliceOrientation.SAG 41 | switch phaseEncDir 42 | case PhaseEncodingDirection.AP 43 | axesOrder = {'z','y','x'}; % Read, phase, slice 44 | axesSign = [-1,1,1]; 45 | readDir_SCT = [0,0,1]; 46 | phaseDir_SCT = [0,1,0]; 47 | sliceDir_SCT = [1,0,0]; 48 | case PhaseEncodingDirection.PA 49 | axesOrder = {'z','y','x'}; 50 | axesSign = [1,-1,1]; 51 | readDir_SCT = [0,0,-1]; 52 | phaseDir_SCT = [0,-1,0]; 53 | sliceDir_SCT = [1,0,0]; 54 | case PhaseEncodingDirection.HF 55 | axesOrder = {'y','z','x'}; 56 | axesSign = [-1,-1,1]; 57 | readDir_SCT = [0,1,0]; 58 | phaseDir_SCT = [0,0,-1]; 59 | sliceDir_SCT = [1,0,0]; 60 | case PhaseEncodingDirection.FH 61 | axesOrder = {'y','z','x'}; 62 | axesSign = [1,1,1]; 63 | readDir_SCT = [0,-1,0]; 64 | phaseDir_SCT = [0,0,1]; 65 | sliceDir_SCT = [1,0,0]; 66 | otherwise 67 | error('Wrong phase encoding direction.'); 68 | end 69 | case SliceOrientation.COR 70 | switch phaseEncDir 71 | case PhaseEncodingDirection.HF 72 | axesOrder = {'x','z','y'}; % Read, phase, slice 73 | axesSign = [-1,-1,1]; 74 | readDir_SCT = [-1,0,0]; 75 | phaseDir_SCT = [0,0,-1]; 76 | sliceDir_SCT = [0,1,0]; 77 | case PhaseEncodingDirection.FH 78 | axesOrder = {'x','z','y'}; 79 | axesSign = [1,1,1]; 80 | readDir_SCT = [1,0,0]; 81 | phaseDir_SCT = [0,0,1]; 82 | sliceDir_SCT = [0,1,0]; 83 | case PhaseEncodingDirection.RL 84 | axesOrder = {'z','x','y'}; 85 | axesSign = [1,-1,1]; 86 | readDir_SCT = [0,0,-1]; 87 | phaseDir_SCT = [1,0,0]; 88 | sliceDir_SCT = [0,1,0]; 89 | case PhaseEncodingDirection.LR 90 | axesOrder = {'z','x','y'}; 91 | axesSign = [-1,1,1]; 92 | readDir_SCT = [0,0,1]; 93 | phaseDir_SCT = [-1,0,0]; 94 | sliceDir_SCT = [0,1,0]; 95 | otherwise 96 | error('Wrong phase encoding direction.'); 97 | end 98 | otherwise 99 | error('Wrong slice orientation.'); 100 | end 101 | 102 | end -------------------------------------------------------------------------------- /docs/sequenceDiagramGtf.svg: -------------------------------------------------------------------------------- 1 | RF Trigger ADCGXGYGZGradient-free intervalTriggerSequence kernelCameraAcqDuration -------------------------------------------------------------------------------- /CreateSequences.m: -------------------------------------------------------------------------------- 1 | %% Pulseq example sequences 2 | % including triggers and synchronization pre-scans for field-monitoring 3 | 4 | % (c) 2024 Skope Magnetic Resonance Technologies AG 5 | 6 | %% Clean up 7 | clear all 8 | close all 9 | clc 10 | 11 | %% Check if Pulseq module has been added 12 | if not(isfolder('pulseq/matlab')) 13 | error("Please run 'git submodule init' and 'git submodule update' to get the latest Pulseq scripts.") 14 | end 15 | 16 | %% Add Pulseq, sequences and methods 17 | addpath('pulseq/matlab') 18 | addpath('methods') 19 | addpath('sequences') 20 | 21 | %% Create a 2D mono-polar dual-echo gradient-echo sequence for all directions 22 | % Get default sequence parameters 23 | paramsGre2d = SequenceParams('gre2d'); 24 | 25 | paramsGre2d.sliceOrientation = SliceOrientation.SAG; 26 | paramsGre2d.phaseEncDir = PhaseEncodingDirection.AP; 27 | gre2d = skope_gre_2d(paramsGre2d); 28 | 29 | paramsGre2d.sliceOrientation = SliceOrientation.SAG; 30 | paramsGre2d.phaseEncDir = PhaseEncodingDirection.HF; 31 | gre2d = skope_gre_2d(paramsGre2d); 32 | 33 | paramsGre2d.sliceOrientation = SliceOrientation.COR; 34 | paramsGre2d.phaseEncDir = PhaseEncodingDirection.RL; 35 | gre2d = skope_gre_2d(paramsGre2d); 36 | 37 | paramsGre2d.sliceOrientation = SliceOrientation.COR; 38 | paramsGre2d.phaseEncDir = PhaseEncodingDirection.HF; 39 | gre2d = skope_gre_2d(paramsGre2d); 40 | 41 | paramsGre2d.sliceOrientation = SliceOrientation.TRA; 42 | paramsGre2d.phaseEncDir = PhaseEncodingDirection.RL; 43 | gre2d = skope_gre_2d(paramsGre2d); 44 | 45 | paramsGre2d.sliceOrientation = SliceOrientation.TRA; 46 | paramsGre2d.phaseEncDir = PhaseEncodingDirection.AP; 47 | gre2d = skope_gre_2d(paramsGre2d); 48 | 49 | % Plot sequence information after sync 50 | timeRange = [4.25 4.27]; 51 | gre2d.plot(timeRange); 52 | 53 | %% Test sequence 54 | gre2d.test(); 55 | 56 | %% Create a 2D echo-planar imaging sequence for all directions 57 | paramsEpi2d = SequenceParams('epi2d'); 58 | 59 | paramsEpi2d.sliceOrientation = SliceOrientation.SAG; 60 | paramsEpi2d.phaseEncDir = PhaseEncodingDirection.AP; 61 | epi2d = skope_epi_2d(paramsEpi2d); 62 | 63 | paramsEpi2d.sliceOrientation = SliceOrientation.SAG; 64 | paramsEpi2d.phaseEncDir = PhaseEncodingDirection.HF; 65 | epi2d = skope_epi_2d(paramsEpi2d); 66 | 67 | paramsEpi2d.sliceOrientation = SliceOrientation.COR; 68 | paramsEpi2d.phaseEncDir = PhaseEncodingDirection.RL; 69 | epi2d = skope_epi_2d(paramsEpi2d); 70 | 71 | paramsEpi2d.sliceOrientation = SliceOrientation.COR; 72 | paramsEpi2d.phaseEncDir = PhaseEncodingDirection.HF; 73 | epi2d = skope_epi_2d(paramsEpi2d); 74 | 75 | paramsEpi2d.sliceOrientation = SliceOrientation.TRA; 76 | paramsEpi2d.phaseEncDir = PhaseEncodingDirection.RL; 77 | epi2d = skope_epi_2d(paramsEpi2d); 78 | 79 | paramsEpi2d.sliceOrientation = SliceOrientation.TRA; 80 | paramsEpi2d.phaseEncDir = PhaseEncodingDirection.AP; 81 | epi2d = skope_epi_2d(paramsEpi2d); 82 | 83 | % Plot sequence information 84 | timeRange = [5.999 6.070]; 85 | epi2d.plot(timeRange); 86 | 87 | % Test sequence 88 | epi2d.test(); 89 | 90 | %% EPI with acceleration factor 2 91 | paramsEpi2d.sliceOrientation = SliceOrientation.TRA; 92 | paramsEpi2d.phaseEncDir = PhaseEncodingDirection.AP; 93 | paramsEpi2d.accFacPE = 2; 94 | paramsEpi2d.TE = 18.1e-3; 95 | epi2d = skope_epi_2d(paramsEpi2d); 96 | 97 | % Plot sequence information 98 | timeRange = [5.999 6.070]; 99 | epi2d.plot(timeRange); 100 | 101 | % Test sequence 102 | epi2d.test(); 103 | 104 | %% EPI with acceleration factor 2 105 | paramsEpi2d.sliceOrientation = SliceOrientation.COR; 106 | paramsEpi2d.phaseEncDir = PhaseEncodingDirection.RL; 107 | paramsEpi2d.accFacPE = 2; 108 | paramsEpi2d.TE = 25.1e-3; 109 | 110 | paramsEpi2d.Nx = 120; 111 | paramsEpi2d.Ny = 120; 112 | paramsEpi2d.readoutTime = 650e-6; 113 | epi2d = skope_epi_2d(paramsEpi2d); 114 | 115 | % Plot sequence information 116 | timeRange = [5.999 6.070]; 117 | epi2d.plot(timeRange); 118 | 119 | % Test sequence 120 | epi2d.test(); 121 | 122 | %% Create off-resonance and position calibration sequence 123 | paramsOpc = SequenceParams('opc'); 124 | 125 | opc = skope_offresAndPosCalib(paramsOpc); 126 | 127 | % Plot sequence 128 | timeRange = [0 5]; 129 | opc.plot(timeRange); 130 | 131 | % Test sequence 132 | opc.test(); 133 | 134 | %% Create local eddy current calibration sequence 135 | paramsLec = SequenceParams('lec'); 136 | 137 | lec = skope_localEddyCalib(paramsLec); 138 | 139 | % Plot sequence 140 | timeRange = [0 22e-3]; 141 | lec.plot(timeRange); 142 | 143 | % Test sequence 144 | lec.test(); 145 | 146 | %% Create a series of blips 147 | paramsGtf = SequenceParams('gtf'); 148 | gtf = skope_gtf(paramsGtf); 149 | 150 | % Run as well with one average for nominal gradient simulation 151 | paramsGtf.nAve = 1; 152 | gtf = skope_gtf(paramsGtf); 153 | 154 | % Plot sequence information 155 | timeRange = [0 300]; 156 | gtf.plot(timeRange); 157 | 158 | % Test sequence 159 | gtf.test(); 160 | 161 | %% Create two interleaved series of blips with half and double amplitude 162 | paramsGtf = SequenceParams('gtf','linearityCheck'); 163 | gtf = skope_gtf(paramsGtf); 164 | 165 | % Plot sequence information 166 | timeRange = [0 300]; 167 | gtf.plot(timeRange); 168 | 169 | % Test sequence 170 | gtf.test(); 171 | 172 | %% Create a 3D monpolar dual-echo gradient-echo sequence 173 | paramsGre3d = SequenceParams('gre3d'); 174 | 175 | gre3d = skope_gre_3d(paramsGre3d); 176 | 177 | % Plot sequence information after sync 178 | timeRange = [0 100e-3]; 179 | gre3d.plot(timeRange); 180 | 181 | % Test sequence 182 | gre3d.test(); 183 | 184 | %% Create off-resonance and position calibration sequence 185 | load('./waveforms/sweepWaveform.mat') 186 | paramsSweep = SequenceParams('sweep'); 187 | sweep = skope_sweep(paramsSweep,sweepWaveform); 188 | 189 | % Run as well with one average for nominal gradient simulation 190 | paramsSweep.nAve = 1; 191 | sweep = skope_sweep(paramsSweep,sweepWaveform); 192 | 193 | % Plot sequence 194 | timeRange = [0 8]; 195 | sweep.plot(timeRange); 196 | 197 | % Test sequence 198 | sweep.test(); 199 | 200 | %% Create a 2D spiral gradient-echo sequence 201 | % Get default sequence parameters 202 | paramsSpiral2d = SequenceParams('spiral2d'); 203 | load('./waveforms/spiralGrad_FOV192_RES1mm_minRise6_maxAmp40_nitlv16.mat'); % [Hz/m] 204 | spiral2d = skope_spiral_2d(paramsSpiral2d,spiralWaveform); 205 | 206 | % Plot sequence information after sync 207 | timeRange = [5.4 5.42]; 208 | spiral2d.plot(timeRange); 209 | 210 | % Test sequence 211 | spiral2d.test(); -------------------------------------------------------------------------------- /docs/sequenceDiagramOffresAndPosCalibration.svg: -------------------------------------------------------------------------------- 1 | TriggerTriggerTriggerGradientGradientRF Trigger ADCGXGYGZGradientGradient-free intervalTriggerCameraAcqDurationDynamic 1Dynamic 2Dynamic 3Dynamic 4 -------------------------------------------------------------------------------- /sequences/skope_localEddyCalib.m: -------------------------------------------------------------------------------- 1 | classdef skope_localEddyCalib < PulseqBase 2 | % Sequence for local eddy current calibration 3 | 4 | % (c) 2024 Skope Magnetic Resonance Technologies AG 5 | 6 | properties 7 | axis = ['x', 'y', 'z'] % Axis with blips 8 | N_rep = 10 % Number of repetitions 9 | relax = 0.8 % Relax system limits 10 | nBlipsPerAxis = 32 % Number of blips per axis 11 | end 12 | 13 | methods 14 | 15 | function obj = skope_localEddyCalib(seqParams) 16 | 17 | %% Check input structure 18 | if not(isa(seqParams,'SequenceParams')) 19 | error('Input need to be a SequenceParams object.'); 20 | end 21 | 22 | %% Set base class properties 23 | obj.TR = seqParams.TR; % Repetition time [Unit: s] 24 | obj.gradFreeTime = 0.5e-3; % Delay between trigger and blip-train [Unit: s] 25 | 26 | %% Get system limits 27 | specs = GetMRSystemSpecs(seqParams.scannerType); 28 | 29 | if not(strcmpi(specs.maxGrad_unit,'mT/m')) 30 | error('Expected mT/m for maximum gradient.'); 31 | end 32 | 33 | if not(strcmpi(specs.maxSlew_unit,'T/m/s')) 34 | error('Expected T/m/s for slew rate.'); 35 | end 36 | 37 | %% Check specs 38 | if seqParams.maxGrad > specs.maxGrad 39 | error('Scanner does not support requested gradient amplitude.'); 40 | end 41 | if seqParams.maxSlew > specs.maxSlew 42 | error('Scanner does not support requested slew rate.'); 43 | end 44 | 45 | % Set system limits 46 | obj.sys = mr.opts('MaxGrad', seqParams.maxGrad, ... 47 | 'GradUnit','mT/m',... 48 | 'MaxSlew', seqParams.maxSlew, ... 49 | 'SlewUnit','T/m/s',... 50 | 'rfRingdownTime', 30e-6, ... 51 | 'rfDeadtime', 100e-6,... 52 | 'B0', specs.B0 ... 53 | ); 54 | 55 | %% Create a new sequence object 56 | obj.seq = mr.Sequence(obj.sys); 57 | 58 | %% Set parameters 59 | % Compliance to gradient raster time 60 | dur_base_ = 6e-5; % [s]; blip duration (factor must be even) 61 | dur_incr_ = 2e-5; % [s]; increment to adjust blip duration (factor must be even) 62 | dur_base = obj.roundUpToGRT(dur_base_); 63 | dur_inc = obj.roundUpToGRT(dur_incr_); 64 | assert(~mod((dur_base / 2), obj.sys.gradRasterTime)); 65 | assert(~mod((dur_inc / 2), obj.sys.gradRasterTime)); 66 | 67 | T_blips_limit = 40e-3; % time limit of blip-train per excitation 68 | 69 | if (mod(obj.nBlipsPerAxis, 2)) 70 | error("Choose even number of blips per axis 'nBlipsPerAxis'!"); % balancing condition 71 | end 72 | 73 | %% Prepare event objects 74 | % Trigger 75 | mr_trig = mr.makeDigitalOutputPulse('ext1','duration', obj.sys.gradRasterTime); 76 | 77 | % Delay 78 | mr_gradFreeTime = mr.makeDelay(obj.gradFreeTime); 79 | 80 | % Blips 81 | mr_blips = {}; 82 | for ax = obj.axis 83 | 84 | count = 0; 85 | for i=[0:(obj.nBlipsPerAxis-1)] 86 | 87 | sign = (-1)^i; 88 | dur = dur_base + count * dur_inc; 89 | 90 | if (mod(i,2)) 91 | count = count + 1; 92 | end 93 | 94 | % use relaxed slew-rate 95 | mr_blip_ = obj.makeBlip(ax, obj.sys.maxSlew * obj.relax, dur, sign); 96 | assert(dur - mr.calcDuration(mr_blip_) < 1e-15); % sanity check 97 | 98 | mr_blips = [mr_blips(:)', {mr_blip_}]; 99 | end 100 | end 101 | 102 | T_blips = PulseqBase.CalculateDuration(mr_blips); 103 | 104 | % Delay 105 | mr_delay = mr.makeDelay(obj.TR - (T_blips + mr_trig.duration)); 106 | 107 | 108 | assert(T_blips_limit - (T_blips + mr_trig.duration) > 0); 109 | assert(abs(obj.TR - (mr_trig.duration + T_blips + mr_delay.delay)) < 1e-8); 110 | 111 | % Required by PulSeq IDEA 112 | mr_dummy = PulseqBase.makeDummy(); 113 | 114 | %% Combines event objects to event blocks 115 | T_tot = 0; % counter: total time 116 | 117 | for i=[1:obj.N_rep] 118 | 119 | % trigger block 120 | obj.seq.addBlock(mr_trig); 121 | T_tot = T_tot + mr_trig.duration; 122 | 123 | % delay 124 | obj.seq.addBlock(mr_gradFreeTime); 125 | T_tot = T_tot + mr_gradFreeTime.delay; 126 | 127 | % blip-train 128 | for mr_blip_=mr_blips 129 | obj.seq.addBlock(mr_blip_); 130 | T_tot = T_tot + mr.calcDuration(mr_blip_); 131 | end 132 | 133 | % delay 134 | obj.seq.addBlock(mr_delay); 135 | T_tot = T_tot + mr_delay.delay; 136 | end 137 | 138 | assert(obj.N_rep * obj.TR - T_tot < 1e-15); 139 | 140 | obj.seq.addBlock(mr_dummy); 141 | 142 | %% Number of triggers 143 | obj.nTrig = obj.N_rep; 144 | 145 | %% Prepare sequence export 146 | obj.seq.setDefinition('Name', 'lec'); 147 | obj.seq.setDefinition('CameraNrDynamics', obj.nTrig); 148 | obj.seq.setDefinition('CameraNrSyncDynamics', 0); 149 | obj.seq.setDefinition('CameraAcqDuration', 0.021); 150 | obj.seq.setDefinition('CameraInterleaveTR', 0.15); 151 | obj.seq.setDefinition('CameraAqDelay', 0); 152 | 153 | %% Write to Pulseq file 154 | if not(isfolder('exports')) 155 | mkdir('exports') 156 | end 157 | obj.seq.write('exports/skope_localEddyCalib.seq') 158 | 159 | end 160 | 161 | end 162 | 163 | methods (Access = private) 164 | function blip = makeBlip(obj, axis, slew, duration, sign) 165 | % Prepare a blip gradient 166 | 167 | rise_time = duration / 2.; 168 | grad = slew * rise_time; 169 | 170 | if (abs(grad) >= obj.sys.maxGrad * obj.relax) 171 | err_msg = ['Gradient strength exceeds maximum.' , ... 172 | ' Try:' , ... 173 | ' (1) decreasing number of blips per axis "nBlipsPerAxis", or ', ... 174 | ' (2) decreasing duration increment "dur_incr_".']; 175 | error(err_msg); 176 | end 177 | 178 | % gradient moment 179 | area = sign * (grad * rise_time); 180 | area = round(area, 12, 'decimal'); % to prevent pulseq from strange rounding 181 | % in makeTrapezoid.m line:113 182 | 183 | blip = mr.makeTrapezoid( axis, ..., 184 | 'maxSlew', slew, ... 185 | 'maxGrad', obj.sys.maxGrad * obj.relax, ... 186 | 'Area', area, ... 187 | 'system', obj.sys); 188 | end 189 | end 190 | end -------------------------------------------------------------------------------- /sequences/skope_gtf.m: -------------------------------------------------------------------------------- 1 | classdef skope_gtf < PulseqBase 2 | % Sequence for impulse response measurements using gradient blips 3 | 4 | % (c) 2024 Skope Magnetic Resonance Technologies AG 5 | 6 | properties 7 | axis = ['x', 'y', 'z'] % Axis with blips 8 | relax = 0.9 % Relax system limits 9 | nBlipsPerAxis = 19 % Number of blips per axis (one 10 | % measurement without a gradient blib 11 | % will be added per axis) 12 | end 13 | 14 | methods 15 | 16 | function obj = skope_gtf(seqParams) 17 | 18 | %% Check input structure 19 | if not(isa(seqParams,'SequenceParams')) 20 | error('Input need to be a SequenceParams object.'); 21 | end 22 | 23 | if not(strcmpi(seqParams.mode,'linearityCheck')) && ... 24 | not(strcmpi(seqParams.mode,'default')) 25 | error('Unknown sequence mode.') 26 | end 27 | 28 | %% Get system limits 29 | specs = GetMRSystemSpecs(seqParams.scannerType); 30 | 31 | if not(strcmpi(specs.maxGrad_unit,'mT/m')) 32 | error('Expected mT/m for maximum gradient.'); 33 | end 34 | 35 | if not(strcmpi(specs.maxSlew_unit,'T/m/s')) 36 | error('Expected T/m/s for slew rate.'); 37 | end 38 | 39 | obj.TR = seqParams.TR; 40 | obj.gradFreeTime = 2e-3; % Delay between trigger and blip [Unit: s] 41 | obj.nAve = seqParams.nAve; 42 | obj.nRep = seqParams.nRep; 43 | 44 | %% Check specs 45 | if seqParams.maxGrad > specs.maxGrad 46 | error('Scanner does not support requested gradient amplitude.'); 47 | end 48 | if seqParams.maxSlew > specs.maxSlew 49 | error('Scanner does not support requested slew rate.'); 50 | end 51 | 52 | % Set system limits 53 | obj.sys = mr.opts('MaxGrad', seqParams.maxGrad, ... 54 | 'GradUnit','mT/m',... 55 | 'MaxSlew', seqParams.maxSlew, ... 56 | 'SlewUnit','T/m/s',... 57 | 'rfRingdownTime', 30e-6, ... 58 | 'rfDeadtime', 100e-6,... 59 | 'B0', specs.B0 ... 60 | ); 61 | 62 | %% Create a new sequence object 63 | obj.seq = mr.Sequence(obj.sys); 64 | 65 | %% Set parameters 66 | % Compliance to gradient raster time 67 | dur_base_ = 2e-5; % [s]; blip duration (factor must be even) 68 | dur_incr_ = 2e-5; % [s]; increment to adjust blip duration (factor must be even) 69 | dur_base = obj.roundUpToGRT(dur_base_); 70 | dur_inc = obj.roundUpToGRT(dur_incr_); 71 | assert(~mod((dur_base / 2), obj.sys.gradRasterTime)); 72 | assert(~mod((dur_inc / 2), obj.sys.gradRasterTime)); 73 | 74 | %% Prepare event objects 75 | % Trigger 76 | mr_trig = mr.makeDigitalOutputPulse('ext1','duration', obj.sys.gradRasterTime); 77 | 78 | % Delay 79 | mr_gradFreeTime = mr.makeDelay(obj.gradFreeTime); 80 | 81 | % Blips 82 | mr_blips = {}; 83 | for ax = 1:3 84 | index = 0; 85 | for count=[0:(obj.nBlipsPerAxis-1)] 86 | 87 | sign = 1; 88 | dur = dur_base + count * dur_inc; 89 | 90 | % use relaxed slew-rate 91 | mr_blip_ = obj.makeBlip(obj.axis(ax), obj.sys.maxSlew * obj.relax, dur, sign); 92 | assert(dur - mr.calcDuration(mr_blip_) < 1e-15); % sanity check 93 | 94 | if strcmpi(seqParams.mode,'linearityCheck') 95 | % Play out same blip with half the amplitude 96 | mr_blips{ax,index+1} = mr.scaleGrad(mr_blip_,0.5); 97 | mr_blips{ax,index+2} = mr_blip_; 98 | index = index+2; 99 | else 100 | mr_blips{ax,index+1} = mr_blip_; 101 | index = index+1; 102 | end 103 | end 104 | end 105 | 106 | % Required by PulSeq IDEA 107 | mr_dummy = PulseqBase.makeDummy(); 108 | 109 | %% Combines event objects to event blocks 110 | if strcmpi(seqParams.mode,'linearityCheck') 111 | % Play out the 'blip' with zero amplitude twice as well 112 | startVal = -1; 113 | else 114 | startVal = 0; 115 | end 116 | 117 | for r = 1:obj.nRep 118 | for ax = 1:3 119 | % blip-train 120 | for n=startVal:size(mr_blips(),2) 121 | for i=1:obj.nAve 122 | % trigger block 123 | obj.seq.addBlock(mr_trig); 124 | T_tot = mr_trig.duration; 125 | 126 | if n>0 127 | % delay 128 | obj.seq.addBlock(mr_gradFreeTime); 129 | T_tot = T_tot + mr_gradFreeTime.delay; 130 | 131 | obj.seq.addBlock(mr_blips{ax,n}); 132 | T_tot = T_tot + mr.calcDuration(mr_blips{ax,n}); 133 | end 134 | 135 | % Delay 136 | assert(obj.TR - T_tot > 0) 137 | mr_delay = mr.makeDelay(obj.TR - T_tot); 138 | 139 | % delay 140 | obj.seq.addBlock(mr_delay); 141 | end 142 | end 143 | end 144 | end 145 | 146 | obj.seq.addBlock(mr_dummy); 147 | 148 | %% Number of triggers 149 | obj.nTrig = obj.nAve*obj.nRep*3*(obj.nBlipsPerAxis+1); 150 | if strcmpi(seqParams.mode,'linearityCheck') 151 | obj.nTrig = obj.nTrig * 2; 152 | end 153 | 154 | %% Prepare sequence export 155 | if strcmpi(seqParams.mode,'linearityCheck') 156 | obj.seq.setDefinition('Name', 'gtf_linCheck'); 157 | else 158 | obj.seq.setDefinition('Name', 'gtf'); 159 | end 160 | obj.seq.setDefinition('CameraNrDynamics', obj.nTrig); 161 | obj.seq.setDefinition('CameraNrSyncDynamics', 0); 162 | obj.seq.setDefinition('CameraAcqDuration', 0.070); 163 | obj.seq.setDefinition('CameraInterleaveTR', 0.400); 164 | obj.seq.setDefinition('CameraAqDelay', 0); 165 | 166 | %% Write to Pulseq file 167 | if not(isfolder('exports')) 168 | mkdir('exports') 169 | end 170 | if strcmpi(seqParams.mode,'linearityCheck') 171 | obj.seq.write('exports/skope_gtf_linearityCheck.seq') 172 | else 173 | if obj.nAve == 1 174 | obj.seq.write('exports/skope_gtf_one_ave.seq') 175 | else 176 | obj.seq.write('exports/skope_gtf.seq') 177 | end 178 | end 179 | 180 | end 181 | end 182 | 183 | methods (Access = private) 184 | function blip = makeBlip(obj, axis, slew, duration, sign) 185 | % Prepare a blip gradient 186 | 187 | rise_time = duration / 2.; 188 | grad = slew * rise_time; 189 | 190 | if (abs(grad) >= obj.sys.maxGrad * obj.relax) 191 | err_msg = ['Gradient strength exceeds maximum.' , ... 192 | ' Try:' , ... 193 | ' (1) decreasing number of blips per axis "nBlipsPerAxis", or ', ... 194 | ' (2) decreasing duration increment "dur_incr_".']; 195 | error(err_msg); 196 | end 197 | 198 | % gradient moment 199 | area = sign * (grad * rise_time); 200 | area = round(area, 12, 'decimal'); % to prevent pulseq from strange rounding 201 | % in makeTrapezoid.m line:113 202 | 203 | blip = mr.makeTrapezoid( axis, ..., 204 | 'maxSlew', slew, ... 205 | 'maxGrad', obj.sys.maxGrad * obj.relax, ... 206 | 'Area', area, ... 207 | 'system', obj.sys); 208 | end 209 | end 210 | end -------------------------------------------------------------------------------- /sequences/PulseqBase.m: -------------------------------------------------------------------------------- 1 | classdef (Abstract) PulseqBase < handle 2 | % Base class for Pulseq sequences that support field monitoring 3 | 4 | % (c) 2022 Skope Magnetic Resonance Technologies AG 5 | 6 | properties 7 | 8 | % System properties 9 | sys 10 | 11 | % Array of echo times (1x2) [Unit: s] 12 | TE 13 | 14 | % Excitation repetition time [Unit: s] 15 | TR 16 | 17 | % Duration of scanner readout event [Unit: s] 18 | readoutTime 19 | 20 | % Field of view (1x2) [Unit: m] 21 | fov 22 | 23 | % Number of readout samples 24 | Nx 25 | 26 | % Number of phase encoding lines 27 | Ny 28 | 29 | % Number of partition encoding steps 30 | Nz 31 | 32 | % Slice thickness 33 | thickness 34 | 35 | % Number of slices 36 | nSlices 37 | 38 | % Number of synchronization triggers 39 | nSyncDynamics = 10 40 | 41 | % Flip angle 42 | alpha = 90 43 | 44 | % Number of repetitions 45 | nRep = 1; 46 | 47 | % Number of averages 48 | nAve = 1; 49 | 50 | % Slice orientation 51 | sliceOrientation = SliceOrientation.TRA; 52 | 53 | % Phase encoding direction 54 | phaseEncDir = PhaseEncodingDirection.AP; 55 | 56 | % Order of axes according to slice orientation and phase encoding 57 | % direction 58 | axesOrder; 59 | 60 | % Sign flip of axis 61 | axesSign; 62 | 63 | % Number of dummy pulses to reach steady state 64 | nDummy = 0; 65 | 66 | end 67 | 68 | properties(SetAccess=protected, GetAccess=public) 69 | 70 | % Pause between synchronization and actual imaging scans (default 4s) 71 | preScanPause = 4 72 | 73 | % Minimal camera TR [Unit: s] 74 | minCameraTR = 115e-3 75 | 76 | % Number of imaging triggers 77 | nTrig = 0 78 | 79 | % Maximal duty cycle of Acquisition system 80 | maxDutyCycleAQSys = 0.25 81 | 82 | % Time from rising edge of trigger to first scanner ADC sample [Unit: s] 83 | triggerToScannerAcqDelay 84 | 85 | % Duration of camera acquisition [Unit: s] 86 | cameraAcqDuration 87 | 88 | % Number of trigger to skip by AQ system 89 | skipFactor = 1; 90 | 91 | % Camera interleave TR or blank time [Unit: s] 92 | % Additional external triggers from the scanner are ignored by the 93 | % Camera Acquisition System during this period 94 | cameraInterleaveTR = 0 95 | 96 | % Pulseq object 97 | seq 98 | end 99 | 100 | properties (Access=protected) 101 | 102 | % Gradient-free time for probe excitation [Unit: s] 103 | gradFreeTime = 200e-6 104 | 105 | % Trigger trigger object 106 | extTrigger 107 | 108 | % Fill time for echo times [Unit: s] 109 | fillTE 110 | 111 | % Fill time for repetition time [Unit: s] 112 | fillTR 113 | end 114 | 115 | methods 116 | 117 | function obj = plot(obj, timeRange) 118 | % Plot label information and k-space trajectory data 119 | 120 | if not(exist('timeRange','var')) 121 | timeRange = [0 25]*obj.TR; 122 | end 123 | 124 | %% plot sequence and k-space diagrams 125 | % Check if LIN is a used label 126 | labelLINPresent = false; 127 | for b=1:length(obj.seq.blockEvents) 128 | block = obj.seq.getBlock(b); 129 | if isfield(block,'label') && any(cellfun(@(x)strcmp(x,'LIN'),{block.label.label})) 130 | labelLINPresent = true; 131 | break; 132 | end 133 | end 134 | if labelLINPresent 135 | obj.seq.plot('timeRange', timeRange, 'TimeDisp', 'ms', 'label', 'lin'); 136 | else 137 | obj.seq.plot('timeRange', timeRange, 'TimeDisp', 'ms'); 138 | end 139 | 140 | if false 141 | obj.seq.plot('timeRange', timeRange, 'TimeDisp', 'ms', 'label', 'eco'); 142 | obj.seq.plot('timeRange', timeRange, 'TimeDisp', 'ms', 'label', 'slc'); 143 | obj.seq.plot('timeRange', timeRange, 'TimeDisp', 'ms', 'label', 'nav'); 144 | obj.seq.plot('timeRange', timeRange, 'TimeDisp', 'ms', 'label', 'avg'); 145 | end 146 | 147 | if not(isempty(obj.seq.labelincLibrary.data)) || not(isempty(obj.seq.labelsetLibrary.data)) 148 | 149 | % k-space trajectory calculation 150 | [ktraj_adc, t_adc, ktraj, t_ktraj, t_excitation, t_refocusing] = obj.seq.calculateKspacePP(); 151 | 152 | % plot k-spaces 153 | figure; 154 | subplot(2,1,1); 155 | plot(t_ktraj, ktraj'); % plot the entire k-space trajectory 156 | hold; plot(t_adc,ktraj_adc(1,:),'.'); % and sampling points on the kx-axis 157 | title('k-space components as functions of time'); 158 | drawnow 159 | subplot(2,1,2); 160 | plot(ktraj(1,:),ktraj(2,:),'b'); % a 2D plot 161 | axis('equal'); % enforce aspect ratio for the correct trajectory display 162 | hold;plot(ktraj_adc(1,:),ktraj_adc(2,:),'r.'); % plot the sampling points 163 | title('2D k-space'); 164 | drawnow 165 | end 166 | end 167 | 168 | function obj = test(obj) 169 | % Very optional slow step but useful for testing during 170 | % development e.g. for the real TE, TR or for staying within 171 | % slew rate limits 172 | rep = obj.seq.testReport; 173 | fprintf([rep{:}]); 174 | 175 | end 176 | 177 | function obj = CalculateInterleaveTR(obj, triggerTR) 178 | % Calculate blank time for Camera Acquisition System. The field 179 | % probes can perform an acquisition circa every 110 ms. If the 180 | % triggering is faster, several triggers need to be skipped. The 181 | % interleaveTR/blank time allows to set the period in which triggers 182 | % should be ignored by the Camera Acquisition System. 183 | 184 | % Default values 185 | eps = 2e-3; 186 | 187 | %% Calculate minimal interleave TR 188 | if triggerTR < obj.minCameraTR + eps 189 | obj.skipFactor = max(2, ceil(obj.minCameraTR / triggerTR)); 190 | obj.cameraInterleaveTR = obj.skipFactor * triggerTR - eps; 191 | else 192 | obj.skipFactor = 1; 193 | obj.cameraInterleaveTR = triggerTR - eps; 194 | end 195 | 196 | %% Calculate duty cycle 197 | dutyCycle = obj.cameraAcqDuration / obj.cameraInterleaveTR; 198 | if dutyCycle > obj.maxDutyCycleAQSys 199 | 200 | % Minimal TR that Skope AQ system will allow 201 | minTR = obj.cameraAcqDuration / obj.maxDutyCycleAQSys; 202 | 203 | % Time to add to reach minTR 204 | diff = minTR - obj.cameraInterleaveTR; 205 | 206 | % Number of additional triggers to skip 207 | addTrig2Skip = ceil(diff / triggerTR); 208 | 209 | % Add a multiple of trigger TR 210 | obj.cameraInterleaveTR = obj.cameraInterleaveTR + addTrig2Skip * triggerTR; 211 | obj.skipFactor = obj.skipFactor + addTrig2Skip; 212 | end 213 | end 214 | 215 | function t = roundUpToGRT(obj,t) 216 | % Round time to gradient raster time 217 | 218 | % First round to microseconds to avoid errors due to number representation 219 | t_us = round(t*1e6); 220 | gdt_us = obj.sys.gradRasterTime*1e6; 221 | 222 | % Round up to GRT 223 | t = ceil(t_us/gdt_us)*obj.sys.gradRasterTime; 224 | end 225 | 226 | function obj = addBlock(obj,varargin) 227 | for i=1:numel(varargin) 228 | switch varargin{i}.type 229 | case {'trap','grad'} 230 | ind = find(cellfun(@(x)strcmp(x,varargin{i}.channel),obj.axesOrder)); 231 | if isfield(varargin{i},'amplitude') 232 | varargin{i}.amplitude = obj.axesSign(ind)*varargin{i}.amplitude; 233 | elseif isfield(varargin{i},'waveform') 234 | varargin{i}.waveform = obj.axesSign(ind)*varargin{i}.waveform; 235 | varargin{i}.first = varargin{i}.waveform(1); 236 | varargin{i}.last = varargin{i}.waveform(end); 237 | end 238 | otherwise 239 | % Nothing to do 240 | end 241 | 242 | end 243 | obj.seq.addBlock(varargin); 244 | end 245 | end 246 | 247 | methods (Static) 248 | function t = CalculateDuration(set) 249 | % Calculate the total duration of all event objects contained in a set 250 | t = 0; 251 | for i = set 252 | t = t + mr.calcDuration(i); 253 | end 254 | end 255 | 256 | function dummy = makeDummy() 257 | % Prepare a gradient with 0 amplitude 258 | % to create a [SHAPES] paragraph required by PulSeq IDEA 259 | dummyWaveForm = [0 0]; 260 | dummy = mr.makeArbitraryGrad('x', dummyWaveForm); 261 | end 262 | end 263 | end -------------------------------------------------------------------------------- /exports/skope_gtf_one_ave.seq: -------------------------------------------------------------------------------- 1 | # Pulseq sequence file 2 | # Created by MATLAB mr toolbox 3 | 4 | [VERSION] 5 | major 1 6 | minor 4 7 | revision 1 8 | 9 | [DEFINITIONS] 10 | AdcRasterTime 1e-07 11 | BlockDurationRaster 1e-05 12 | CameraAcqDuration 0.07 13 | CameraAqDelay 0 14 | CameraInterleaveTR 0.4 15 | CameraNrDynamics 60 16 | CameraNrSyncDynamics 0 17 | GradientRasterTime 1e-05 18 | Name gtf 19 | RadiofrequencyRasterTime 1e-06 20 | 21 | # Format of blocks: 22 | # NUM DUR RF GX GY GZ ADC EXT 23 | [BLOCKS] 24 | 1 1 0 0 0 0 0 1 25 | 2 99999 0 0 0 0 0 0 26 | 3 1 0 0 0 0 0 1 27 | 4 200 0 0 0 0 0 0 28 | 5 2 0 1 0 0 0 0 29 | 6 99797 0 0 0 0 0 0 30 | 7 1 0 0 0 0 0 1 31 | 8 200 0 0 0 0 0 0 32 | 9 4 0 2 0 0 0 0 33 | 10 99795 0 0 0 0 0 0 34 | 11 1 0 0 0 0 0 1 35 | 12 200 0 0 0 0 0 0 36 | 13 6 0 3 0 0 0 0 37 | 14 99793 0 0 0 0 0 0 38 | 15 1 0 0 0 0 0 1 39 | 16 200 0 0 0 0 0 0 40 | 17 8 0 4 0 0 0 0 41 | 18 99791 0 0 0 0 0 0 42 | 19 1 0 0 0 0 0 1 43 | 20 200 0 0 0 0 0 0 44 | 21 10 0 5 0 0 0 0 45 | 22 99789 0 0 0 0 0 0 46 | 23 1 0 0 0 0 0 1 47 | 24 200 0 0 0 0 0 0 48 | 25 12 0 6 0 0 0 0 49 | 26 99787 0 0 0 0 0 0 50 | 27 1 0 0 0 0 0 1 51 | 28 200 0 0 0 0 0 0 52 | 29 14 0 7 0 0 0 0 53 | 30 99785 0 0 0 0 0 0 54 | 31 1 0 0 0 0 0 1 55 | 32 200 0 0 0 0 0 0 56 | 33 16 0 8 0 0 0 0 57 | 34 99783 0 0 0 0 0 0 58 | 35 1 0 0 0 0 0 1 59 | 36 200 0 0 0 0 0 0 60 | 37 18 0 9 0 0 0 0 61 | 38 99781 0 0 0 0 0 0 62 | 39 1 0 0 0 0 0 1 63 | 40 200 0 0 0 0 0 0 64 | 41 20 0 10 0 0 0 0 65 | 42 99779 0 0 0 0 0 0 66 | 43 1 0 0 0 0 0 1 67 | 44 200 0 0 0 0 0 0 68 | 45 22 0 11 0 0 0 0 69 | 46 99777 0 0 0 0 0 0 70 | 47 1 0 0 0 0 0 1 71 | 48 200 0 0 0 0 0 0 72 | 49 24 0 12 0 0 0 0 73 | 50 99775 0 0 0 0 0 0 74 | 51 1 0 0 0 0 0 1 75 | 52 200 0 0 0 0 0 0 76 | 53 26 0 13 0 0 0 0 77 | 54 99773 0 0 0 0 0 0 78 | 55 1 0 0 0 0 0 1 79 | 56 200 0 0 0 0 0 0 80 | 57 28 0 14 0 0 0 0 81 | 58 99771 0 0 0 0 0 0 82 | 59 1 0 0 0 0 0 1 83 | 60 200 0 0 0 0 0 0 84 | 61 30 0 15 0 0 0 0 85 | 62 99769 0 0 0 0 0 0 86 | 63 1 0 0 0 0 0 1 87 | 64 200 0 0 0 0 0 0 88 | 65 32 0 16 0 0 0 0 89 | 66 99767 0 0 0 0 0 0 90 | 67 1 0 0 0 0 0 1 91 | 68 200 0 0 0 0 0 0 92 | 69 34 0 17 0 0 0 0 93 | 70 99765 0 0 0 0 0 0 94 | 71 1 0 0 0 0 0 1 95 | 72 200 0 0 0 0 0 0 96 | 73 36 0 18 0 0 0 0 97 | 74 99763 0 0 0 0 0 0 98 | 75 1 0 0 0 0 0 1 99 | 76 200 0 0 0 0 0 0 100 | 77 38 0 19 0 0 0 0 101 | 78 99761 0 0 0 0 0 0 102 | 79 1 0 0 0 0 0 1 103 | 80 99999 0 0 0 0 0 0 104 | 81 1 0 0 0 0 0 1 105 | 82 200 0 0 0 0 0 0 106 | 83 2 0 0 1 0 0 0 107 | 84 99797 0 0 0 0 0 0 108 | 85 1 0 0 0 0 0 1 109 | 86 200 0 0 0 0 0 0 110 | 87 4 0 0 2 0 0 0 111 | 88 99795 0 0 0 0 0 0 112 | 89 1 0 0 0 0 0 1 113 | 90 200 0 0 0 0 0 0 114 | 91 6 0 0 3 0 0 0 115 | 92 99793 0 0 0 0 0 0 116 | 93 1 0 0 0 0 0 1 117 | 94 200 0 0 0 0 0 0 118 | 95 8 0 0 4 0 0 0 119 | 96 99791 0 0 0 0 0 0 120 | 97 1 0 0 0 0 0 1 121 | 98 200 0 0 0 0 0 0 122 | 99 10 0 0 5 0 0 0 123 | 100 99789 0 0 0 0 0 0 124 | 101 1 0 0 0 0 0 1 125 | 102 200 0 0 0 0 0 0 126 | 103 12 0 0 6 0 0 0 127 | 104 99787 0 0 0 0 0 0 128 | 105 1 0 0 0 0 0 1 129 | 106 200 0 0 0 0 0 0 130 | 107 14 0 0 7 0 0 0 131 | 108 99785 0 0 0 0 0 0 132 | 109 1 0 0 0 0 0 1 133 | 110 200 0 0 0 0 0 0 134 | 111 16 0 0 8 0 0 0 135 | 112 99783 0 0 0 0 0 0 136 | 113 1 0 0 0 0 0 1 137 | 114 200 0 0 0 0 0 0 138 | 115 18 0 0 9 0 0 0 139 | 116 99781 0 0 0 0 0 0 140 | 117 1 0 0 0 0 0 1 141 | 118 200 0 0 0 0 0 0 142 | 119 20 0 0 10 0 0 0 143 | 120 99779 0 0 0 0 0 0 144 | 121 1 0 0 0 0 0 1 145 | 122 200 0 0 0 0 0 0 146 | 123 22 0 0 11 0 0 0 147 | 124 99777 0 0 0 0 0 0 148 | 125 1 0 0 0 0 0 1 149 | 126 200 0 0 0 0 0 0 150 | 127 24 0 0 12 0 0 0 151 | 128 99775 0 0 0 0 0 0 152 | 129 1 0 0 0 0 0 1 153 | 130 200 0 0 0 0 0 0 154 | 131 26 0 0 13 0 0 0 155 | 132 99773 0 0 0 0 0 0 156 | 133 1 0 0 0 0 0 1 157 | 134 200 0 0 0 0 0 0 158 | 135 28 0 0 14 0 0 0 159 | 136 99771 0 0 0 0 0 0 160 | 137 1 0 0 0 0 0 1 161 | 138 200 0 0 0 0 0 0 162 | 139 30 0 0 15 0 0 0 163 | 140 99769 0 0 0 0 0 0 164 | 141 1 0 0 0 0 0 1 165 | 142 200 0 0 0 0 0 0 166 | 143 32 0 0 16 0 0 0 167 | 144 99767 0 0 0 0 0 0 168 | 145 1 0 0 0 0 0 1 169 | 146 200 0 0 0 0 0 0 170 | 147 34 0 0 17 0 0 0 171 | 148 99765 0 0 0 0 0 0 172 | 149 1 0 0 0 0 0 1 173 | 150 200 0 0 0 0 0 0 174 | 151 36 0 0 18 0 0 0 175 | 152 99763 0 0 0 0 0 0 176 | 153 1 0 0 0 0 0 1 177 | 154 200 0 0 0 0 0 0 178 | 155 38 0 0 19 0 0 0 179 | 156 99761 0 0 0 0 0 0 180 | 157 1 0 0 0 0 0 1 181 | 158 99999 0 0 0 0 0 0 182 | 159 1 0 0 0 0 0 1 183 | 160 200 0 0 0 0 0 0 184 | 161 2 0 0 0 1 0 0 185 | 162 99797 0 0 0 0 0 0 186 | 163 1 0 0 0 0 0 1 187 | 164 200 0 0 0 0 0 0 188 | 165 4 0 0 0 2 0 0 189 | 166 99795 0 0 0 0 0 0 190 | 167 1 0 0 0 0 0 1 191 | 168 200 0 0 0 0 0 0 192 | 169 6 0 0 0 3 0 0 193 | 170 99793 0 0 0 0 0 0 194 | 171 1 0 0 0 0 0 1 195 | 172 200 0 0 0 0 0 0 196 | 173 8 0 0 0 4 0 0 197 | 174 99791 0 0 0 0 0 0 198 | 175 1 0 0 0 0 0 1 199 | 176 200 0 0 0 0 0 0 200 | 177 10 0 0 0 5 0 0 201 | 178 99789 0 0 0 0 0 0 202 | 179 1 0 0 0 0 0 1 203 | 180 200 0 0 0 0 0 0 204 | 181 12 0 0 0 6 0 0 205 | 182 99787 0 0 0 0 0 0 206 | 183 1 0 0 0 0 0 1 207 | 184 200 0 0 0 0 0 0 208 | 185 14 0 0 0 7 0 0 209 | 186 99785 0 0 0 0 0 0 210 | 187 1 0 0 0 0 0 1 211 | 188 200 0 0 0 0 0 0 212 | 189 16 0 0 0 8 0 0 213 | 190 99783 0 0 0 0 0 0 214 | 191 1 0 0 0 0 0 1 215 | 192 200 0 0 0 0 0 0 216 | 193 18 0 0 0 9 0 0 217 | 194 99781 0 0 0 0 0 0 218 | 195 1 0 0 0 0 0 1 219 | 196 200 0 0 0 0 0 0 220 | 197 20 0 0 0 10 0 0 221 | 198 99779 0 0 0 0 0 0 222 | 199 1 0 0 0 0 0 1 223 | 200 200 0 0 0 0 0 0 224 | 201 22 0 0 0 11 0 0 225 | 202 99777 0 0 0 0 0 0 226 | 203 1 0 0 0 0 0 1 227 | 204 200 0 0 0 0 0 0 228 | 205 24 0 0 0 12 0 0 229 | 206 99775 0 0 0 0 0 0 230 | 207 1 0 0 0 0 0 1 231 | 208 200 0 0 0 0 0 0 232 | 209 26 0 0 0 13 0 0 233 | 210 99773 0 0 0 0 0 0 234 | 211 1 0 0 0 0 0 1 235 | 212 200 0 0 0 0 0 0 236 | 213 28 0 0 0 14 0 0 237 | 214 99771 0 0 0 0 0 0 238 | 215 1 0 0 0 0 0 1 239 | 216 200 0 0 0 0 0 0 240 | 217 30 0 0 0 15 0 0 241 | 218 99769 0 0 0 0 0 0 242 | 219 1 0 0 0 0 0 1 243 | 220 200 0 0 0 0 0 0 244 | 221 32 0 0 0 16 0 0 245 | 222 99767 0 0 0 0 0 0 246 | 223 1 0 0 0 0 0 1 247 | 224 200 0 0 0 0 0 0 248 | 225 34 0 0 0 17 0 0 249 | 226 99765 0 0 0 0 0 0 250 | 227 1 0 0 0 0 0 1 251 | 228 200 0 0 0 0 0 0 252 | 229 36 0 0 0 18 0 0 253 | 230 99763 0 0 0 0 0 0 254 | 231 1 0 0 0 0 0 1 255 | 232 200 0 0 0 0 0 0 256 | 233 38 0 0 0 19 0 0 257 | 234 99761 0 0 0 0 0 0 258 | 235 2 0 20 0 0 0 0 259 | 260 | # Format of arbitrary gradients: 261 | # time_shape_id of 0 means default timing (stepping with grad_raster starting at 1/2 of grad_raster) 262 | # id amplitude amp_shape_id time_shape_id delay 263 | # .. Hz/m .. .. us 264 | [GRADIENTS] 265 | 20 0 1 2 0 266 | 267 | # Format of trapezoid gradients: 268 | # id amplitude rise flat fall delay 269 | # .. Hz/m us us us us 270 | [TRAP] 271 | 1 76636.8 10 0 10 0 272 | 2 153274 20 0 20 0 273 | 3 229910 30 0 30 0 274 | 4 306547 40 0 40 0 275 | 5 383184 50 0 50 0 276 | 6 459821 60 0 60 0 277 | 7 536458 70 0 70 0 278 | 8 613094 80 0 80 0 279 | 9 689731 90 0 90 0 280 | 10 766368 100 0 100 0 281 | 11 843005 110 0 110 0 282 | 12 919642 120 0 120 0 283 | 13 996278 130 0 130 0 284 | 14 1.07292e+06 140 0 140 0 285 | 15 1.14955e+06 150 0 150 0 286 | 16 1.22619e+06 160 0 160 0 287 | 17 1.30283e+06 170 0 170 0 288 | 18 1.37946e+06 180 0 180 0 289 | 19 1.4561e+06 190 0 190 0 290 | 291 | # Format of extension lists: 292 | # id type ref next_id 293 | # next_id of 0 terminates the list 294 | # Extension list is followed by extension specifications 295 | [EXTENSIONS] 296 | 1 1 1 0 297 | 298 | # Extension specification for digital output and input triggers: 299 | # id type channel delay (us) duration (us) 300 | extension TRIGGERS 1 301 | 1 1 3 0 10 302 | 303 | # Sequence Shapes 304 | [SHAPES] 305 | 306 | shape_id 1 307 | num_samples 2 308 | 0 309 | 0 310 | 311 | shape_id 2 312 | num_samples 2 313 | 0.5 314 | 1.5 315 | 316 | 317 | [SIGNATURE] 318 | # This is the hash of the Pulseq file, calculated right before the [SIGNATURE] section was added 319 | # It can be reproduced/verified with md5sum if the file trimmed to the position right above [SIGNATURE] 320 | # The new line character preceding [SIGNATURE] BELONGS to the signature (and needs to be sripped away for recalculating/verification) 321 | Type md5 322 | Hash 4adee16db3beb12a821415ccf69070c3 323 | -------------------------------------------------------------------------------- /docs/sequenceDiagramEPI2D.svg: -------------------------------------------------------------------------------- 1 | fillTERF Trigger ADCGXGYGZADC Gradient-free intervalSequence kernelTriggerADC ADC ADC ADC ADC fillTRTETRCameraAcqDurationechoSpacingTriggerToScannerAcqDelay -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pulseq sequences for field-monitoring 2 | 3 | This repository contains sequences for calibrating field-monitoring experiments and imaging sequences for demonstration purposes on Siemens scanners. Pulse sequences for Skope Field Camera installation and use on GE scanners can be found under https://github.com/toppeMRI/Skope. 4 | 5 | Calibration sequence for all users 6 | - Off-resonance and position calibration - This sequence is run at the start of each scanning session and after each change of the scanner shim settings, it provides the data to determine the resonance frequency and the position of the probes within the scanner bore. 7 | 8 | For Clip-On Camera "Cranberry" Edition users 9 | - Local eddy current calibration - This sequence provides the data needed to quantify eddy currents near individual probes as part of the Local Eddy Current correction provided in the Clip-On Camera "Cranberry" Edition. 10 | 11 | Imaging sequences for demonstration 12 | - Mono-polar 2D dual-echo gradient-echo sequence in different orientations 13 | - 2D echo-planar imaging sequence in different orientations 14 | 15 | Please visit https://github.com/pulseq/pulseq for further information about Pulseq and make sure to use the correct version of the Pulseq interpreter on the scanner. 16 | 17 | ## Getting started 18 | 19 | ### Clone this repository 20 | 21 | git clone https://github.com/SkopeMagneticResonanceTechnologies/Pulseq-Sequences.git 22 | 23 | Add Pulseq as a submodule 24 | 25 | git submodule init 26 | git submodule update 27 | 28 | ### Creating the Pulseq sequence files in MATLAB 29 | 30 | - Open MATLAB 31 | - Run CreateSequences.m to create Pulseq sequence files for all sequences 32 | 33 | ### NOTES 34 | - The Siemens Pulseq interpreter 1.4.2 must be used for these sequences. 35 | - If the sequences do not run on the scanner, try closing the protocol and opening it again. 36 | 37 | ## Acquisition parameters for field-monitoring 38 | 39 | The sequence defines the timing of all events for the scanner. Some timing information also needs to be transferred to the Camera Acquisition System. The required values have been added as DEFINITIONS to the header of the Pulseq files. The following tables relates the values in the Pulseq file to the parameters on the graphical user interface of skope-fx. 40 | 41 | | Parameter on GUI of skope-fx | Pulseq definition | Explanation | 42 | |------------------------------|----------------------|-------------| 43 | | NrDynamics | CameraNrDynamics | Maximum number of acquisitions performed by the Camera Acquisition System. The actual number of acquired dynamics will be smaller if triggers are being skipped.| 44 | | InterleaveTR | CameraInterleaveTR | If the time between subsequent triggers is smaller than the CameraInterleaveTR (blank time), the subsequent triggers will be ignored for the duration of the CameraInterleaveTR. This parameter can be used to skip triggers in case of rapid imaging. | 45 | | Aq Duration | CameraAcqDuration | Duration of the acquisition performed on the Camera Acquisition System.| 46 | | Aq Delay | CameraAqDelay | Delay between the reception of the trigger and the start of the acquisition on the Camera Acquisition System. | 47 | | Nr Sync Dynamics | CameraNrSyncDynamics | Number of acquisitions to be performed for data synchronization. | 48 | 49 | Note that the time unit is seconds in the Pulseq file and milliseconds on the graphical user interface of skope-fx. 50 | 51 | ## Acquisition parameters for image reconstruction 52 | 53 | Some additional timing values are required to merge camera and scanner data and to perform image reconstruction. 54 | 55 | A rough estimate of the delay between the start of the trigger and the first scanner ADC sample is needed for the synchronization of the camera and scanner data. This value has been added to the DEFINITIONS structure of the Pulseq file. 56 | 57 | TriggerToScannerAcqDelay - Time from TTL trigger to first sample of ADC event 58 | 59 | Additional timing information is required for data merging and image reconstruction. 60 | 61 | **General parameters** 62 | 63 | SliceShifts - Slice shift in millimeter along the slice direction 64 | TE - Echo time(s) in seconds 65 | TR - Repetition time in seconds 66 | phaseDir_SCT - Phase encoding direction in the patient coordinate system (Sag, Cor, Tra) 67 | readDir_SCT - Read direction in the patient coordinate system (Sag, Cor, Tra) 68 | sliceDir_SCT - Slice direction in the patient coordinate system (Sag, Cor, Tra) 69 | 70 | **EPI** 71 | 72 | EchoSpacing - Echo spacing in seconds 73 | EchoTrainLength - Echo train length 74 | 75 | ## Sequences 76 | 77 | ### Off-resonance and position calibration (OPC) 78 | 79 | At the start of each scanning session and after each change of the scanner shim settings, this calibration sequence is used to determine the resonance frequency and the position of the probes within the scanner bore. It consists of four acquisitions. The first acquisition is performed in the absence of any applied gradient. The other three acquisitions are performed while a gradient of 2.5 mT/m is played out on either of the three physical axes. 80 | 81 | ![Off-resonance and position calibration](docs/sequenceDiagramOffresAndPosCalibration.svg "Off-resonance and position calibration") 82 | 83 | Please do not change the timings used by this sequence. The calibration wizard in skope-fx uses the set values as default values. 84 | 85 | ### Local eddy current calibration (LEC) 86 | 87 | Local eddy currents can disturb the fields measured by individual probes. The implemented sequence allows to quantify these eddy currents by playing out a series of gradient blips on each physical axis of the gradient system. 88 | 89 | Please do not change the timing or the number of blips used by this sequence. The calibration wizard in skope-fx relies on correct timing of these events in the sequence. 90 | 91 | ![Local eddy current calibration](docs/sequenceDiagramLocalEddyCurrentCalibration.svg "Local eddy current calibration") 92 | 93 | ### 2D gradient echo (GRE) sequence 94 | 95 | The implemented sequence acquires two mono-polar echoes for 15 slices. The data is acquired in an interleaved fashion meaning that the innermost loop of the sequence is the slice index. A repetition time (TR) of 25 ms has been selected as default value. With an interleave TR of 123 ms, four triggers will be skipped and every fifth scanner acquisition will be monitored. Hence, the first, sixth and eleventh slice will be fully monitored. The k-space trajectories for the other slices can be interpolated based on the acquired data. 96 | 97 | The figure below shows the kernel of the GRE sequence. It is repeated 1920 (= 128 [lines] * 15 [slices]) times to acquire the entire image. 98 | 99 | The same kernel is repeated 10 times without RF at the start of the scan to acquire synchronization data. During this pre-scan, the Camera Acquisition System transmits amplitude-modulated pulses at the proton frequency. The data can be used to calculate the delay between scanner and camera data. A pause of 4s separates the synchronization pre-scan from the actual imaging experiment. 100 | 101 | ![GRE sequence kernel](docs/sequenceDiagramGRE2D.svg "GRE sequence kernel") 102 | 103 | Set the following values on the Camera Acquisition System: 104 | 105 | CameraNrDynamics 384 106 | CameraInterleaveTR 123 ms 107 | CameraAcqDuration 13 ms 108 | CameraAqDelay 0 ms 109 | CameraNrSyncDynamics 10 110 | 111 | Due to trigger skipping, the Camera Acquisition System will not acquire all 1920 dynamics but it will still register them. If you typed in the correct number of monitored dynamics, the AQ system should stop automatically at the end of the scan. Otherwise you need to click Stop Scan on the graphical user interface of skope-fx once the scanner has finished. 112 | 113 | The figure below shows the effect of trigger skipping for an acquistion of 5 slices. 114 | 115 | Trigger skipping 116 | 117 | ### 2D echo planar (EPI) sequence 118 | 119 | The figure below shows the kernel for the EPI sequence. It is repeated 150 times to acquire all slices and repetitions. As for the GRE sequence, 10 synchronization pre-scans are played out before the actual imaging scan. 120 | 121 | ![EPI sequence kernel](docs/sequenceDiagramEPI2D.svg "EPI sequence kernel" ) 122 | 123 | Set the following values on the Camera Acquisition System: 124 | 125 | CameraNrDynamics 75 126 | CameraInterleaveTR 398 ms 127 | CameraAcqDuration 63 ms 128 | CameraAqDelay 0 ms 129 | CameraNrSyncDynamics 10 130 | 131 | Due to trigger skipping, the Camera Acquisition System will not acquire all 150 dynamics. 132 | 133 | ### Blip sequence for gradient transfer function estimation 134 | 135 | The figure below shows the kernel for the GTF sequence. Three sets of gradient blips with different amplitudes and maximum slew rate are played out on the three physical gradient axes. 136 | 137 | ![GTF sequence kernel](docs/sequenceDiagramGtf.svg "GTF sequence kernel" ) 138 | 139 | Set the following values on the Camera Acquisition System: 140 | 141 | CameraNrDynamics 240 142 | CameraInterleaveTR 400 143 | CameraAcqDuration 40 ms 144 | CameraAqDelay 0 ms 145 | CameraNrSyncDynamics 0 146 | 147 | 148 | ## Pulseq files 149 | 150 | The Pulseq files included in this repository have been created for a 7T Siemens whole-body system. **Please check that the selected echo-spacings do not fall within the forbidden frequencies of your gradient system.** 151 | 152 | ## Warranties 153 | 154 | THE SEQUENCES ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND EXPRESS OR IMPLIED, AND TO THE MAXIMUM EXTENT PERMITTED BY THE APPLICABLE LAW. EXCEPT AS EXPRESSLY PROVIDED IN THIS CLAUSE, NO WARRANTY, CONDITION, UNDERTAKING, LIABILITY OR TERM, EXPRESS OR IMPLIED, STATUTORY OR OTHERWISE, AS TO CONDITION, QUALITY, PERFORMANCE, FUNCTIONALITY, INFRINGEMENT, MERCHANTABILITY, DURABILITY OR FITNESS FOR PURPOSE, IS GIVEN OR ASSUMED BY SKOPE AG, LICENSOR OR ITS LICENSORS AND ALL SUCH WARRANTIES, CONDITIONS, UNDERTAKINGS AND TERMS ARE HEREBY EXCLUDED. 155 | 156 | ## Disclaimer 157 | 158 | The sequences *are not* medical device applications. The sequences and any resulting data are to be used for research purposes only and *not* to diagnose, treat, cure or prevent any disease. Skope Magnetic Resonance Technologies makes no representation and assumes no responsibility for the misuse of these sequences. 159 | Skope Magnetic Resonance Technologies does not take any responsibility or liability for damages, harm, loss of data or similar incidents that are in direct or indirect relation to the usage or presence of any of its devices. MR sequences or particular implementations thereof can be subject to intellectual property of one or several parties. 160 | 161 | The information available in this repository, including text, graphics, images, and information contained on or in said repository is not intended as medical advice or a substitute for obtaining medical advice. Skope Magnetic Resonance Technologies does not recommend or endorse any specific tests, products, procedures, or other information that may be mentioned in this manual, especially in place of physician recommended care. You are encouraged to confirm any and all information obtained from or through this repository without your physician or other medical professional. Never disregard professional medical advice or delay seeking medical treatment because of something you have access through Skope Magnetic Resonance Technologies. 162 | 163 | The statements made regarding the sequences *have not* been evaluated by the Food and Drug Administration or like authorities. The safety and efficacy of the sequences *has not* been confirmed by FDA-approved or likewise approved research. The sequences *are not* intended to diagnose, treat, cure or prevent any disease. 164 | The content of this repository can change without notice. 165 | 166 | © COPYRIGHT 2024 by Skope Magnetic Resonance Technologies AG 167 | -------------------------------------------------------------------------------- /docs/sequenceDiagramGRE2D.svg: -------------------------------------------------------------------------------- 1 | fillTE1fillTE2TE 1TE 2RF Trigger ADCGXGYGZADC 1TriggerToScannerAcqDelayADC 2Gradient-free intervalCameraAcqDurationTRfillTRSequence kernelTrigger -------------------------------------------------------------------------------- /docs/sequenceDiagramLocalEddyCurrentCalibration.svg: -------------------------------------------------------------------------------- 1 | RF Trigger ADCGXGYGZGradient-free intervalTriggerSequence kernelCameraAcqDuration -------------------------------------------------------------------------------- /docs/triggerSkipping.svg: -------------------------------------------------------------------------------- 1 | 262728293012345678910s1s2s3s4s5111213141516171819202122232425L1L2L3L4L5L…Scan CounterScan CounterMonitoredNot monitored -------------------------------------------------------------------------------- /sequences/skope_spiral_2d.m: -------------------------------------------------------------------------------- 1 | classdef skope_spiral_2d < PulseqBase 2 | % This is a demo spiral gradient-echo sequence, which includes 3 | % synchronization scans for field-monitoring with a Skope Field Camera and 4 | % uses the LABEL extension. The member method plot() can be used to display 5 | % the generated sequence. 6 | % 7 | % Notes: 8 | % - The sequence is not adaptive, i.e. the parameters can't be changed, as 9 | % the spiral waveform is loaded from file 10 | % - The k-space trajectory during the synchronization scans will not be 11 | % correctly shown by the member method plot(). 12 | % 13 | % Example: 14 | % spiral2d = skope_spiral_2d(sequenceParams, waveform); 15 | % spiral2d.plot(); 16 | % spiral2d.test(); 17 | % 18 | % See also PulseqBase 19 | 20 | % (c) 2024 Skope Magnetic Resonance Technologies AG 21 | 22 | properties (Access=private) 23 | 24 | % Pulseq transmit object 25 | rf 26 | 27 | % Pulseq ADC event 28 | adc 29 | 30 | % Spiral gradient (loaded) 31 | gspiral 32 | 33 | % Pulseq spiral readout gradient (x) 34 | gx 35 | 36 | % Pulseq spiral readout gradient (y) 37 | gy 38 | 39 | % Pulseq slice selection gradient 40 | gz 41 | 42 | % Pulseq slice refocusing gradient 43 | gzReph 44 | 45 | % Refocussing gradients for spiral 46 | gxRefoc 47 | gyRefoc 48 | 49 | % Pulseq slice spoiling gradient 50 | gzSpoil 51 | 52 | % ADC dwelltime 53 | adcDwelltime 54 | 55 | % Phase encoding moments 56 | phaseAreas 57 | 58 | % RF phase 59 | rf_phase 60 | 61 | % RF phase increment 62 | rf_inc 63 | 64 | % Phase increment for RF spoiling 65 | rfSpoilingInc = 117 66 | 67 | distanceFactorPercentage = 250; 68 | 69 | end 70 | 71 | methods 72 | 73 | function obj = skope_spiral_2d(seqParams, waveform) 74 | 75 | %% Check input structure 76 | if not(isa(seqParams,'SequenceParams')) 77 | error('Input need to be a SequenceParams object.'); 78 | end 79 | 80 | %% Get system limits 81 | specs = GetMRSystemSpecs(seqParams.scannerType); 82 | 83 | if not(strcmpi(specs.maxGrad_unit,'mT/m')) 84 | error('Expected mT/m for maximum gradient.'); 85 | end 86 | 87 | if not(strcmpi(specs.maxSlew_unit,'T/m/s')) 88 | error('Expected T/m/s for slew rate.'); 89 | end 90 | 91 | %% Check specs 92 | if seqParams.maxGrad > specs.maxGrad 93 | error('Scanner does not support requested gradient amplitude.'); 94 | end 95 | if seqParams.maxSlew > specs.maxSlew 96 | error('Scanner does not support requested slew rate.'); 97 | end 98 | 99 | %% Create object 100 | obj.sys = mr.opts( 'MaxGrad', seqParams.maxGrad, ... 101 | 'GradUnit', 'mT/m', ... 102 | 'MaxSlew', seqParams.maxSlew, ... 103 | 'SlewUnit', 'T/m/s', ... 104 | 'rfRingdownTime', 20e-6, ... 105 | 'rfDeadTime', 100e-6, ... 106 | 'adcDeadTime', 10e-6); 107 | 108 | % ADC dwelltime 109 | obj.adcDwelltime = 2e-6; 110 | 111 | % Field of view [Unit: m] 112 | obj.fov = seqParams.fov; 113 | 114 | % Number of readout samples 115 | obj.Nx = seqParams.Nx; 116 | 117 | % Number of spiral arms 118 | obj.Ny = seqParams.Ny; 119 | 120 | % Flip angle [Unit: deg] 121 | obj.alpha = seqParams.alpha; 122 | 123 | % Slice thickness [Unit: m] 124 | obj.thickness = seqParams.thickness; 125 | 126 | % Number of slices 127 | obj.nSlices = seqParams.nSlices; 128 | 129 | % Echo times [Unit: s] 130 | obj.TE = seqParams.TE; 131 | 132 | % Excitation repetition time [Unit: s] 133 | obj.TR = seqParams.TR; 134 | 135 | % ADC duration [Unit: s] 136 | obj.readoutTime = seqParams.readoutTime; 137 | 138 | obj.sliceOrientation = seqParams.sliceOrientation; 139 | obj.phaseEncDir = seqParams.phaseEncDir; 140 | 141 | %% Axes order 142 | [obj.axesOrder, obj.axesSign, readDir_SCT, phaseDir_SCT, sliceDir_SCT] ... 143 | = GetAxesOrderAndSign(obj.sliceOrientation,obj.phaseEncDir); 144 | 145 | 146 | %% Create a new sequence object 147 | obj.seq = mr.Sequence(obj.sys); 148 | 149 | %% Time for probe excitation 150 | obj.gradFreeTime = obj.roundUpToGRT(200e-6); 151 | 152 | %% Create alpha-degree slice selection pulse and gradient 153 | [obj.rf, obj.gz] = mr.makeSincPulse( obj.alpha*pi/180, ... 154 | 'Duration', 1.5e-3, ... 155 | 'SliceThickness', obj.thickness, ... 156 | 'apodization', 0.5, ... 157 | 'timeBwProduct', 4, ... 158 | 'system',obj.sys); 159 | 160 | %% Define other gradients and ADC events (Not that X gradient has been flipped here) 161 | obj.gzReph = mr.makeTrapezoid(obj.axesOrder{3},'Area',-obj.gz.area/2,'Duration',1e-3,'system',obj.sys); 162 | 163 | % Create spiral waveform 164 | obj.gspiral = waveform.'; 165 | obj.gx = mr.makeArbitraryGrad(obj.axesOrder{1},obj.gspiral(1,:)); 166 | obj.gy = mr.makeArbitraryGrad(obj.axesOrder{2},obj.gspiral(2,:)); 167 | durADC = mr.calcDuration(obj.gx); 168 | 169 | % Let's make the number of samples divisible by 10 and 8 170 | nSamplesADC = floor(durADC/obj.adcDwelltime/80)*80; 171 | % Update duration 172 | durADC = nSamplesADC*obj.adcDwelltime; 173 | 174 | obj.adc = mr.makeAdc(nSamplesADC, ... 175 | 'Duration', durADC, ... 176 | 'system', obj.sys); 177 | 178 | % Compute moment 179 | mx = -trapz(obj.gspiral(1,:))*obj.sys.gradRasterTime; 180 | my = -trapz(obj.gspiral(2,:))*obj.sys.gradRasterTime; 181 | 182 | % gradient spoiling and refocussing 183 | obj.gzSpoil = mr.makeTrapezoid(obj.axesOrder{3},'Area',4/obj.thickness,'system', obj.sys); 184 | obj.gxRefoc = mr.makeTrapezoid(obj.axesOrder{1},'Area',mx,'system', obj.sys); 185 | obj.gyRefoc = mr.makeTrapezoid(obj.axesOrder{2},'Area',my,'system', obj.sys); 186 | refocTime = max([mr.calcDuration(obj.gzSpoil),mr.calcDuration(obj.gxRefoc),mr.calcDuration(obj.gyRefoc)]); 187 | 188 | % Stretch gradients 189 | obj.gzSpoil = mr.makeTrapezoid(obj.axesOrder{3},'Area',4/obj.thickness,'system', obj.sys, 'Duration',refocTime); 190 | obj.gxRefoc = mr.makeTrapezoid(obj.axesOrder{1},'Area',mx,'system', obj.sys, 'Duration',refocTime); 191 | obj.gyRefoc = mr.makeTrapezoid(obj.axesOrder{2},'Area',my,'system', obj.sys, 'Duration',refocTime); 192 | 193 | %% Calculate minimal TEs 194 | % First echo 195 | minTE1 = obj.gz.flatTime/2 + obj.gz.fallTime + mr.calcDuration(obj.gzReph); 196 | disp(['Minimal TE1 is ' num2str((minTE1 + obj.gradFreeTime)*1000) ' ms']) 197 | 198 | obj.fillTE(1) = obj.roundUpToGRT(obj.TE(1) - minTE1); 199 | assert(obj.fillTE(1) >= obj.gradFreeTime, 'Assertion for TE1 failed'); 200 | 201 | %% Time from trigger to scanner acquisition 202 | obj.triggerToScannerAcqDelay = obj.fillTE(1) + obj.adc.delay; 203 | 204 | %% Prepare trigger 205 | obj.extTrigger = mr.makeDigitalOutputPulse('ext1','duration', obj.sys.gradRasterTime); 206 | 207 | %% Calculate minimal TR 208 | minTR = mr.calcDuration(obj.gz) ... 209 | + mr.calcDuration(obj.gzReph) ... 210 | + obj.fillTE(1) ... 211 | + mr.calcDuration(obj.gx) ... 212 | + mr.calcDuration(obj.gzSpoil); 213 | disp(['Minimal TR is ' num2str(minTR*1000) ' ms']) 214 | 215 | obj.fillTR = obj.roundUpToGRT(obj.TR - minTR); 216 | assert(obj.fillTR >= 0, 'Assertion for TR failed.'); 217 | 218 | %% Calculate required camera acquisition duration 219 | obj.cameraAcqDuration = obj.fillTE(1) ... 220 | + mr.calcDuration(obj.gx) ... 221 | + 1e-3; % To be safe 222 | obj.cameraAcqDuration = ceil(obj.cameraAcqDuration*1000)/1000; 223 | 224 | %% Phase settings 225 | obj.rf_phase = 0; 226 | obj.rf_inc = 0; 227 | 228 | %% Synchronization 229 | if obj.nSyncDynamics > 0 230 | for avg = 1:obj.nSyncDynamics 231 | slc = 1; 232 | lin = 1; 233 | obj = runKernel(obj, lin, slc, avg, false); 234 | end 235 | 236 | %% Add pause and reset flags 237 | if obj.preScanPause < 4 238 | warning('The pause between the synchronization and imaging scans should be equal or larger than 4 seconds. The current value is okay for simulation purposes.'); 239 | end 240 | 241 | obj.addBlock(mr.makeDelay(obj.preScanPause), mr.makeLabel('SET','LIN', 0), mr.makeLabel('SET','SLC', 0), mr.makeLabel('SET','AVG', 0)); 242 | end 243 | 244 | %% Actual imaging sequence 245 | % loop over phase encodes and define sequence blocks 246 | for lin = 1:obj.Ny 247 | % loop over slices 248 | for slc = 1:obj.nSlices 249 | avg = 1; 250 | obj = runKernel(obj, lin, slc, avg); 251 | end 252 | end 253 | 254 | % Set number of expected external triggers 255 | obj.nTrig = obj.Ny * obj.nSlices; 256 | 257 | %% check whether the timing of the sequence is correct 258 | [ok, error_report] = obj.seq.checkTiming; 259 | 260 | if (ok) 261 | fprintf('Timing check passed successfully\n'); 262 | else 263 | fprintf('Timing check failed! Error listing follows:\n'); 264 | fprintf([error_report{:}]); 265 | fprintf('\n'); 266 | end 267 | 268 | %% Calculate Camera Interleave TR (blank time) 269 | obj.CalculateInterleaveTR(obj.TR); 270 | 271 | %% Prepare sequence export 272 | obj.seq.setDefinition('Name', 'spir2d'); 273 | obj.seq.setDefinition('FOV', [obj.fov obj.fov obj.thickness*obj.nSlices*(1+obj.distanceFactorPercentage/100)]); 274 | obj.seq.setDefinition('TR', obj.TR); 275 | obj.seq.setDefinition('TE', obj.TE); 276 | 277 | %% Parameters needed be added to the scanner data header for trajectory merging 278 | obj.seq.setDefinition('TriggerToScannerAcqDelay', obj.triggerToScannerAcqDelay); 279 | 280 | %% Parameters to be set on the user interface of the Field Camera 281 | % The number of actually acquired dynamics depends on the cameraInterleaveTR. 282 | obj.seq.setDefinition('CameraNrDynamics', obj.nTrig); 283 | obj.seq.setDefinition('CameraNrSyncDynamics', obj.nSyncDynamics); 284 | obj.seq.setDefinition('CameraAcqDuration', obj.cameraAcqDuration); 285 | obj.seq.setDefinition('CameraInterleaveTR', obj.cameraInterleaveTR); 286 | obj.seq.setDefinition('CameraAqDelay', 0); 287 | obj.seq.setDefinition('AdcSampleTime', obj.adc.dwell); 288 | obj.seq.setDefinition('Matrix', [obj.Nx obj.Ny]); 289 | obj.seq.setDefinition('SliceShifts', [obj.thickness*([1:obj.nSlices]-1-(obj.nSlices-1)/2)]*(1+obj.distanceFactorPercentage/100)); 290 | obj.seq.setDefinition('readDir_SCT', readDir_SCT); 291 | obj.seq.setDefinition('phaseDir_SCT', phaseDir_SCT); 292 | obj.seq.setDefinition('sliceDir_SCT', sliceDir_SCT); 293 | 294 | %% Write to Pulseq file 295 | if not(isfolder('exports')) 296 | mkdir('exports') 297 | end 298 | obj.seq.write(strcat('exports/skope_spiral_2d','_',string(obj.sliceOrientation),'_',string(obj.phaseEncDir),'.seq')); 299 | 300 | end 301 | end 302 | 303 | methods (Access=private) 304 | 305 | function [Gx_rot, Gy_rot] = rotate_spiralArm(obj, Gx, Gy, phi) 306 | Gx_rot = cos(phi) * Gx - sin(phi) * Gy; 307 | Gy_rot = sin(phi) * Gx + cos(phi) * Gy; 308 | end 309 | 310 | function obj = runKernel(obj, lin, slc, avg, doPlayRF) 311 | 312 | %% Input check 313 | if not(exist('doPlayRF','var')) 314 | doPlayRF = true; 315 | end 316 | 317 | %% RF and ADC settings 318 | if doPlayRF 319 | obj.rf.freqOffset = obj.gz.amplitude * obj.thickness*(slc-1-(obj.nSlices-1)/2)*(1+obj.distanceFactorPercentage/100); 320 | obj.rf.phaseOffset = obj.rf_phase/180*pi; 321 | obj.adc.phaseOffset = obj.rf_phase/180*pi; 322 | obj.rf_inc = mod(obj.rf_inc + obj.rfSpoilingInc, 360.0); 323 | obj.rf_phase = mod(obj.rf_phase + obj.rf_inc, 360.0); 324 | obj.addBlock(obj.rf, obj.gz, mr.makeLabel('SET','PMC',false), mr.makeLabel('SET','AVG',avg-1)); 325 | else 326 | obj.rf.freqOffset = 0; 327 | obj.rf.phaseOffset = 0; 328 | obj.addBlock(obj.gz, mr.makeLabel('SET','PMC',true), mr.makeLabel('SET','AVG',avg-1)); 329 | end 330 | 331 | %% Slice refocusing gradient 332 | obj.addBlock(obj.gzReph); 333 | 334 | %% External trigger and gradient-free interval a 335 | obj.addBlock(obj.extTrigger, mr.makeDelay(obj.fillTE(1))); 336 | 337 | %% All LABELS / counters an flags are automatically initialized to 0 in the beginning, no need to define initial 0's 338 | % so we will just increment LIN after the ADC event (e.g. during the spoiler) 339 | %seq.addBlock(mr.makeDelay(1)); % older scanners like Trio may need this 340 | % dummy delay to keep up with timing 341 | 342 | 343 | %% Set labels 344 | labels = [ {mr.makeLabel('SET','LIN', lin-1)}, ... 345 | {mr.makeLabel('SET','SLC', slc-1)}, ... 346 | {mr.makeLabel('SET','AVG', avg-1)}]; 347 | 348 | %% Rotate and add readout 349 | phi = (2 * pi * lin / obj.Ny); 350 | [gx, gy] = obj.rotate_spiralArm(obj.gspiral(1,:), obj.gspiral(2,:), phi); 351 | obj.gx = mr.makeArbitraryGrad(obj.axesOrder{1},gx); 352 | obj.gy = mr.makeArbitraryGrad(obj.axesOrder{2},gy); 353 | 354 | obj.addBlock(obj.gx, obj.gy, obj.adc, mr.makeLabel('SET','ECO', 0), labels{:}); 355 | 356 | %% Spoiling 357 | [gxRefoc, gyRefoc] = mr.rotate(obj.axesOrder{3},phi,{obj.gxRefoc, obj.gyRefoc}); 358 | spoilBlockContents = {gxRefoc, gyRefoc, obj.gzSpoil}; 359 | obj.addBlock(spoilBlockContents{:}); 360 | 361 | %% Add delay 362 | obj.addBlock(mr.makeDelay(obj.fillTR)); 363 | 364 | end 365 | end 366 | end 367 | -------------------------------------------------------------------------------- /sequences/skope_gre_3d.m: -------------------------------------------------------------------------------- 1 | classdef skope_gre_3d < PulseqBase 2 | % This is a demo monopolar dual-echo gradient-echo sequence, which includes 3 | % synchronization scans for field-monitoring with a Skope Field Camera and 4 | % uses the LABEL extension. The member method plot() can be used to display 5 | % the generated sequence. 6 | % 7 | % Notes: 8 | % - The sequence file is written into the current folder. 9 | % - The TR refers to the excitation repetition time in this example and not 10 | % the slice TR. 11 | % - The k-space trajectory during the synchronization scans will not be 12 | % correctly shown by the member method plot(). 13 | % - The x-axis is flipped because of a bug in the Siemens Pulseq 14 | % interpreter 1.4.0. 15 | % 16 | % Example: 17 | % gre = skope_gre_3d(sequenceParams); 18 | % gre.plot(); 19 | % gre.test(); 20 | % 21 | % See also PulseqBase 22 | 23 | % (c) 2024 Skope Magnetic Resonance Technologies AG 24 | 25 | properties (Access=private) 26 | 27 | % Pulseq transmit object 28 | rf 29 | 30 | % Pulseq ADC event 31 | adc 32 | 33 | % Pulseq prewinding gradient 34 | gxPre 35 | 36 | % Pulseq readout gradient 37 | gx 38 | 39 | % Pulseq slice selection gradient 40 | gz 41 | 42 | % Pulseq slice refocusing gradient 43 | gzReph 44 | 45 | % Pulseq read rewinding gradient 46 | gxFlyBack 47 | 48 | % Pulseq read spoiling gradient 49 | gxSpoil 50 | 51 | % Pulseq slice spoiling gradient 52 | gzSpoil 53 | 54 | % Phase encoding moments 55 | phaseAreaY 56 | 57 | % Partition encoding moments 58 | phaseAreaZ 59 | 60 | % RF phase 61 | rf_phase 62 | 63 | % RF phase increment 64 | rf_inc 65 | 66 | % Phase increment for RF spoiling 67 | rfSpoilingInc = 117 68 | 69 | end 70 | 71 | methods 72 | 73 | function obj = skope_gre_3d(seqParams) 74 | 75 | %% Check input structure 76 | if not(isa(seqParams,'SequenceParams')) 77 | error('Input need to be a SequenceParams object.'); 78 | end 79 | 80 | %% Get system limits 81 | specs = GetMRSystemSpecs(seqParams.scannerType); 82 | 83 | if not(strcmpi(specs.maxGrad_unit,'mT/m')) 84 | error('Expected mT/m for maximum gradient.'); 85 | end 86 | 87 | if not(strcmpi(specs.maxSlew_unit,'T/m/s')) 88 | error('Expected T/m/s for slew rate.'); 89 | end 90 | 91 | %% Used gradient amplitude and slew rate by this sequence 92 | 93 | 94 | %% Check specs 95 | if seqParams.maxGrad > specs.maxGrad 96 | error('Scanner does not support requested gradient amplitude.'); 97 | end 98 | if seqParams.maxSlew > specs.maxSlew 99 | error('Scanner does not support requested slew rate.'); 100 | end 101 | 102 | %% Create object 103 | obj.sys = mr.opts( 'MaxGrad', seqParams.maxGrad, ... 104 | 'GradUnit', 'mT/m', ... 105 | 'MaxSlew', seqParams.maxSlew, ... 106 | 'SlewUnit', 'T/m/s', ... 107 | 'rfRingdownTime', 20e-6, ... 108 | 'rfDeadTime', 100e-6, ... 109 | 'adcDeadTime', 10e-6); 110 | 111 | % Field of view [Unit: m] 112 | obj.fov = seqParams.fov; 113 | 114 | % % Number of readout samples 115 | obj.Nx = seqParams.Nx; 116 | 117 | % Number of phase encoding steps 118 | obj.Ny = obj.Nx; 119 | 120 | % Number of phase encoding steps 121 | obj.Nz = obj.Nx; 122 | 123 | % Flip angle [Unit: deg] 124 | obj.alpha = seqParams.alpha; 125 | 126 | % Echo times [Unit: s] + one millisecond for phase estimation 127 | obj.TE = seqParams.TE; 128 | 129 | % Excitation repetition time [Unit: s] 130 | obj.TR = seqParams.TR; 131 | 132 | % ADC duration [Unit: s] 133 | obj.readoutTime = seqParams.readoutTime; 134 | 135 | obj.sliceOrientation = seqParams.sliceOrientation; 136 | obj.phaseEncDir = seqParams.phaseEncDir; 137 | 138 | % Number of dummy shots for steady state 139 | obj.nDummy = seqParams.nDummy; 140 | 141 | %% Axes order 142 | [obj.axesOrder, obj.axesSign, readDir_SCT, phaseDir_SCT, sliceDir_SCT] ... 143 | = GetAxesOrderAndSign(obj.sliceOrientation,obj.phaseEncDir); 144 | 145 | %% Sync scans 146 | obj.nSyncDynamics = 0; 147 | 148 | Tpre = obj.readoutTime; 149 | 150 | %% Create a new sequence object 151 | obj.seq = mr.Sequence(obj.sys); 152 | 153 | %% Create non-selective pulse 154 | obj.rf = mr.makeBlockPulse(obj.alpha*pi/180,obj.sys,'Duration',0.2e-3); 155 | obj.rf_phase = 0; 156 | obj.rf_inc = 0; 157 | 158 | %% Time for probe excitation 159 | obj.gradFreeTime = obj.roundUpToGRT(200e-6); 160 | 161 | %% Define other gradients and ADC events (Not that X gradient has been flipped here) 162 | deltak = 1./obj.fov; 163 | obj.gx = mr.makeTrapezoid( obj.axesOrder{1}, ... 164 | 'FlatArea', obj.Nx*deltak(1), ... 165 | 'FlatTime', obj.readoutTime, ... 166 | 'system',obj.sys); 167 | obj.adc = mr.makeAdc(obj.Nx, ... 168 | 'Duration', obj.gx.flatTime, ... 169 | 'Delay', obj.gx.riseTime, ... 170 | 'system', obj.sys); 171 | obj.gxPre = mr.makeTrapezoid(obj.axesOrder{1},obj.sys,'Area',-obj.gx.area/2,'Duration',Tpre); 172 | obj.gxFlyBack = mr.makeTrapezoid(obj.axesOrder{1},'Area',-obj.gx.area,'system',obj.sys); 173 | obj.gxSpoil = mr.makeTrapezoid(obj.axesOrder{1},obj.sys,'Area',obj.gx.area,'Duration',Tpre*2); 174 | obj.phaseAreaY = ([(obj.Ny-1):-1:0]-obj.Ny/2)*deltak(2); 175 | obj.phaseAreaZ = ([(obj.Nz-1):-1:0]-obj.Nz/2)*deltak(3); 176 | 177 | %% Calculate minimal TEs 178 | % First echo 179 | minTE1 = mr.calcDuration(obj.rf) - mr.calcRfCenter(obj.rf) - obj.rf.delay ... 180 | + obj.gradFreeTime ... 181 | + mr.calcDuration(obj.gxPre) ... 182 | + mr.calcDuration(obj.gx)/2; 183 | 184 | disp(['Minimal TE1 is ' num2str(minTE1*1000) ' ms']) 185 | 186 | obj.fillTE(1) = obj.roundUpToGRT(obj.TE(1) - minTE1); 187 | assert(obj.fillTE(1) >= 0, 'Assertion for TE1 failed'); 188 | 189 | % Second echo 190 | minTE2 = minTE1 + mr.calcDuration(obj.gx)/2 ... 191 | + mr.calcDuration(obj.gxFlyBack) ... 192 | + mr.calcDuration(obj.gx)/2 ... 193 | + obj.fillTE(1); 194 | disp(['Minimal TE2 is ' num2str(minTE2*1000) ' ms']) 195 | 196 | obj.fillTE(2) = obj.roundUpToGRT(obj.TE(2) - minTE2); 197 | assert(obj.fillTE(2) >= 0, 'Assertion for TE2 failed.'); 198 | 199 | %% Increase duration of fly-back gradient by TE2 fill time 200 | if obj.fillTE(2) > 0 201 | obj.gxFlyBack = mr.makeTrapezoid(obj.axesOrder{1},'Area',-obj.gx.area,'system',obj.sys,'Duration',mr.calcDuration(obj.gxFlyBack) + obj.fillTE(2)); 202 | obj.fillTE(2) = 0; 203 | end 204 | 205 | %% Calculate minimal TR 206 | minTR = mr.calcDuration(obj.rf) ... 207 | + mr.calcDuration(obj.gradFreeTime) ... 208 | + obj.fillTE(1) ... 209 | + mr.calcDuration(obj.gxPre) ... 210 | + mr.calcDuration(obj.gx) ... 211 | + mr.calcDuration(obj.gxFlyBack) ... 212 | + mr.calcDuration(obj.gx) ... 213 | + mr.calcDuration(obj.gxSpoil); 214 | disp(['Minimal TR is ' num2str(minTR*1000) ' ms']) 215 | 216 | obj.fillTR = obj.roundUpToGRT(obj.TR - minTR); 217 | assert(obj.fillTR >= 0, 'Assertion for TR failed.'); 218 | 219 | %% Time from trigger to scanner acquisition 220 | obj.triggerToScannerAcqDelay = obj.fillTE(1) + mr.calcDuration(obj.gradFreeTime) ... 221 | + mr.calcDuration(obj.gxPre) ... 222 | + obj.adc.delay; 223 | 224 | %% Prepare trigger 225 | obj.extTrigger = mr.makeDigitalOutputPulse('ext1','duration', obj.sys.gradRasterTime); 226 | 227 | %% Drive magnetization to steady state 228 | for i=1:obj.nDummy 229 | runKernel(obj, floor(obj.Ny/2), floor(obj.Nz/2), 1, KernelMode.Dummy); 230 | end 231 | 232 | %% Calculate required camera acquisition duration 233 | obj.cameraAcqDuration = obj.fillTE(1) + mr.calcDuration(obj.gradFreeTime) ... 234 | + mr.calcDuration(obj.gxPre) ... 235 | + mr.calcDuration(obj.gx) ... 236 | + mr.calcDuration(obj.gxFlyBack) ... 237 | + mr.calcDuration(obj.gx) ... 238 | + 1e-3; % To be safe 239 | 240 | %% Synchronization 241 | % if obj.nSyncDynamics > 0 242 | % for avg = 1:obj.nSyncDynamics 243 | % slc = 1; 244 | % lin = 1; 245 | % obj = runKernel(obj, lin, slc, avg, false); 246 | % end 247 | % 248 | % %% Add pause and reset flags 249 | % if obj.preScanPause < 4 250 | % warning('The pause between the synchronization and imaging scans should be equal or larger than 4 seconds. The current value is okay for simulation purposes.'); 251 | % end 252 | % 253 | % obj.addBlock({mr.makeDelay(obj.preScanPause), mr.makeLabel('SET','LIN', 0), mr.makeLabel('SET','SLC', 0), mr.makeLabel('SET','AVG', 0)}); 254 | % end 255 | 256 | %% Actual imaging sequence 257 | % loop over phase encodes and define sequence blocks 258 | for par = 1:obj.Nz 259 | for lin = 1:obj.Ny 260 | % loop over slices 261 | avg = 1; 262 | obj = runKernel(obj, lin, par, avg, KernelMode.Imaging); 263 | end 264 | end 265 | 266 | % Set number of expected external triggers 267 | obj.nTrig = obj.nDummy + obj.Ny * obj.Nz; 268 | 269 | %% check whether the timing of the sequence is correct 270 | [ok, error_report] = obj.seq.checkTiming; 271 | 272 | if (ok) 273 | fprintf('Timing check passed successfully\n'); 274 | else 275 | fprintf('Timing check failed! Error listing follows:\n'); 276 | fprintf([error_report{:}]); 277 | fprintf('\n'); 278 | end 279 | 280 | %% Calculate Camera Interleave TR (blank time) 281 | obj.CalculateInterleaveTR(obj.TR); 282 | 283 | %% Prepare sequence export 284 | obj.seq.setDefinition('Name', 'gre3d'); 285 | obj.seq.setDefinition('FOV', obj.fov); 286 | obj.seq.setDefinition('TR', obj.TR); 287 | obj.seq.setDefinition('TE', obj.TE); 288 | 289 | %% Parameters needed be added to the scanner data header for trajectory merging 290 | obj.seq.setDefinition('TriggerToScannerAcqDelay', obj.triggerToScannerAcqDelay); 291 | 292 | %% Parameters to be set on the user interface of the Field Camera 293 | % The number of actually acquired dynamics depends on the cameraInterleaveTR. 294 | obj.seq.setDefinition('CameraNrDynamics', obj.nTrig); 295 | obj.seq.setDefinition('CameraNrSyncDynamics', obj.nSyncDynamics); 296 | obj.seq.setDefinition('CameraAcqDuration', obj.cameraAcqDuration); 297 | obj.seq.setDefinition('CameraInterleaveTR', obj.cameraInterleaveTR); 298 | obj.seq.setDefinition('CameraAqDelay', 0); 299 | obj.seq.setDefinition('AdcSampleTime', obj.adc.dwell); 300 | obj.seq.setDefinition('Matrix', [obj.Nx obj.Ny obj.Nz]); 301 | obj.seq.setDefinition('readDir_SCT', readDir_SCT); 302 | obj.seq.setDefinition('phaseDir_SCT', phaseDir_SCT); 303 | obj.seq.setDefinition('sliceDir_SCT', sliceDir_SCT); 304 | 305 | %% Write to Pulseq file 306 | if not(isfolder('exports')) 307 | mkdir('exports') 308 | end 309 | obj.seq.write(strcat('exports/skope_gre_3d','_',string(obj.sliceOrientation),'_',string(obj.phaseEncDir),'.seq')); 310 | 311 | 312 | end 313 | end 314 | 315 | methods (Access=private) 316 | function obj = runKernel(obj, lin, par, avg, mode) 317 | 318 | if not(isa(mode, 'KernelMode')) 319 | error('Expected a kernel mode argument') 320 | end 321 | 322 | %% RF and ADC settings 323 | if mode==KernelMode.Dummy || mode==KernelMode.Imaging 324 | obj.rf.phaseOffset = mod(117*(lin^2+lin+2)*pi/180,2*pi); 325 | obj.adc.phaseOffset = obj.rf.phaseOffset; 326 | obj.addBlock(obj.rf); 327 | else 328 | obj.rf.phaseOffset = 0; 329 | obj.adc.phaseOffset = 0; 330 | obj.addBlock(mr.makeDelay(mr.calcDuration(obj.rf))); 331 | end 332 | 333 | %% External trigger and gradient-free interval 334 | % We send the trigger here always for the dummies to get a 335 | % steady state field probe signal 336 | obj.addBlock(obj.extTrigger, mr.makeDelay(obj.gradFreeTime + obj.fillTE(1))); 337 | 338 | %% Read-prewinding and phase encoding gradients 339 | gyPre = mr.makeTrapezoid(obj.axesOrder{2}, ... 340 | 'Area', obj.phaseAreaY(lin), ... 341 | 'Duration', mr.calcDuration(obj.gxPre), ... 342 | 'system',obj.sys); 343 | gzPre = mr.makeTrapezoid(obj.axesOrder{3}, ... 344 | 'Area', obj.phaseAreaZ(par), ... 345 | 'Duration', mr.calcDuration(obj.gxPre), ... 346 | 'system',obj.sys); 347 | obj.addBlock(obj.gxPre,gyPre,gzPre); 348 | 349 | %% All LABELS / counters an flags are automatically initialized to 0 in the beginning, no need to define initial 0's 350 | % so we will just increment LIN after the ADC event (e.g. during the spoiler) 351 | %seq.addBlock(mr.makeDelay(1)); % older scanners like Trio may need this 352 | % dummy delay to keep up with timing 353 | 354 | %% Set labels 355 | labels = [ {mr.makeLabel('SET','LIN', lin-1)}, ... 356 | {mr.makeLabel('SET','PAR', par-1)}, ... 357 | {mr.makeLabel('SET','AVG', avg-1)}]; 358 | 359 | %% First readout gradient 360 | obj.addBlock(obj.gx, obj.adc, mr.makeLabel('SET','ECO', 0), labels{:}); 361 | 362 | %% Fly back 363 | obj.addBlock(obj.gxFlyBack); % Fill time has been absorbed in gradient duration 364 | 365 | %% Second readout gradient 366 | obj.addBlock(obj.gx, obj.adc, mr.makeLabel('SET','ECO', 1), labels{:}); 367 | 368 | %% Negative Phase encoding 369 | gyPre.amplitude = -gyPre.amplitude; 370 | gzPre.amplitude = -gzPre.amplitude; 371 | 372 | %% Spoiling 373 | spoilBlockContents = {obj.gxSpoil, gyPre, gzPre}; 374 | obj.addBlock(spoilBlockContents{:}); 375 | 376 | %% Add delay 377 | obj.addBlock(mr.makeDelay(obj.fillTR)); 378 | 379 | end 380 | end 381 | end -------------------------------------------------------------------------------- /sequences/skope_gre_2d.m: -------------------------------------------------------------------------------- 1 | classdef skope_gre_2d < PulseqBase 2 | % This is a demo mono-polar dual-echo gradient-echo sequence, which includes 3 | % synchronization scans for field-monitoring with a Skope Field Camera and 4 | % uses the LABEL extension. The member method plot() can be used to display 5 | % the generated sequence. 6 | % 7 | % Notes: 8 | % - The sequence file is written into the current folder. 9 | % - The TR refers to the excitation repetition time in this example and not 10 | % the slice TR. 11 | % - The k-space trajectory during the synchronization scans will not be 12 | % correctly shown by the member method plot(). 13 | % - The x-axis is flipped because of a bug in the Siemens Pulseq 14 | % interpreter 1.4.0. 15 | % 16 | % Example: 17 | % gre = skope_gre_2d(sequenceParams); 18 | % gre.plot(); 19 | % gre.test(); 20 | % 21 | % See also PulseqBase 22 | 23 | % (c) 2024 Skope Magnetic Resonance Technologies AG 24 | 25 | properties (Access=private) 26 | 27 | % Pulseq transmit object 28 | rf 29 | 30 | % Pulseq ADC event 31 | adc 32 | 33 | % Pulseq prewinding gradient 34 | gxPre 35 | 36 | % Pulseq readout gradient 37 | gx 38 | 39 | % Pulseq slice selection gradient 40 | gz 41 | 42 | % Pulseq slice refocusing gradient 43 | gzReph 44 | 45 | % Pulseq read rewinding gradient 46 | gxFlyBack 47 | 48 | % Pulseq read spoiling gradient 49 | gxSpoil 50 | 51 | % Pulseq slice spoiling gradient 52 | gzSpoil 53 | 54 | % Phase encoding moments 55 | phaseAreas 56 | 57 | % RF phase 58 | rf_phase 59 | 60 | % RF phase increment 61 | rf_inc 62 | 63 | % Phase increment for RF spoiling 64 | rfSpoilingInc = 117 65 | 66 | distanceFactorPercentage = 250; 67 | 68 | end 69 | 70 | methods 71 | 72 | function obj = skope_gre_2d(seqParams) 73 | 74 | %% Check input structure 75 | if not(isa(seqParams,'SequenceParams')) 76 | error('Input need to be a SequenceParams object.'); 77 | end 78 | 79 | %% Get system limits 80 | specs = GetMRSystemSpecs(seqParams.scannerType); 81 | 82 | if not(strcmpi(specs.maxGrad_unit,'mT/m')) 83 | error('Expected mT/m for maximum gradient.'); 84 | end 85 | 86 | if not(strcmpi(specs.maxSlew_unit,'T/m/s')) 87 | error('Expected T/m/s for slew rate.'); 88 | end 89 | 90 | %% Check specs 91 | if seqParams.maxGrad > specs.maxGrad 92 | error('Scanner does not support requested gradient amplitude.'); 93 | end 94 | if seqParams.maxSlew > specs.maxSlew 95 | error('Scanner does not support requested slew rate.'); 96 | end 97 | 98 | %% Create object 99 | obj.sys = mr.opts( 'MaxGrad', seqParams.maxGrad, ... 100 | 'GradUnit', 'mT/m', ... 101 | 'MaxSlew', seqParams.maxSlew, ... 102 | 'SlewUnit', 'T/m/s', ... 103 | 'rfRingdownTime', 20e-6, ... 104 | 'rfDeadTime', 100e-6, ... 105 | 'adcDeadTime', 10e-6); 106 | 107 | % Field of view [Unit: m] 108 | obj.fov = seqParams.fov; 109 | 110 | % Number of readout samples 111 | obj.Nx = seqParams.Nx; 112 | 113 | % Number of phase encoding steps 114 | obj.Ny = seqParams.Ny; 115 | 116 | % Flip angle [Unit: deg] 117 | obj.alpha = seqParams.alpha; 118 | 119 | % Slice thickness [Unit: m] 120 | obj.thickness = seqParams.thickness; 121 | 122 | % Number of slices 123 | obj.nSlices = seqParams.nSlices; 124 | 125 | % Echo times [Unit: s] 126 | obj.TE = seqParams.TE; 127 | 128 | % Excitation repetition time [Unit: s] 129 | obj.TR = seqParams.TR; 130 | 131 | % ADC duration [Unit: s] 132 | obj.readoutTime = seqParams.readoutTime; 133 | 134 | obj.sliceOrientation = seqParams.sliceOrientation; 135 | obj.phaseEncDir = seqParams.phaseEncDir; 136 | 137 | obj.nDummy = seqParams.nDummy; 138 | 139 | %% Axes order 140 | [obj.axesOrder, obj.axesSign, readDir_SCT, phaseDir_SCT, sliceDir_SCT] ... 141 | = GetAxesOrderAndSign(obj.sliceOrientation,obj.phaseEncDir); 142 | 143 | %% Create a new sequence object 144 | obj.seq = mr.Sequence(obj.sys); 145 | 146 | %% Time for probe excitation 147 | obj.gradFreeTime = obj.roundUpToGRT(200e-6); 148 | 149 | %% Create alpha-degree slice selection pulse and gradient 150 | [obj.rf, obj.gz] = mr.makeSincPulse( obj.alpha*pi/180, ... 151 | 'Duration', 1.5e-3, ... 152 | 'SliceThickness', obj.thickness, ... 153 | 'apodization', 0.5, ... 154 | 'timeBwProduct', 4, ... 155 | 'system',obj.sys); 156 | % Set correct axis 157 | obj.gz.channel = obj.axesOrder{3}; 158 | 159 | %% Define other gradients and ADC events (Not that X gradient has been flipped here) 160 | deltak = 1/obj.fov; 161 | obj.gx = mr.makeTrapezoid( obj.axesOrder{1}, ... 162 | 'FlatArea', obj.Nx*deltak, ... 163 | 'FlatTime', obj.readoutTime, ... 164 | 'system',obj.sys); 165 | obj.adc = mr.makeAdc(obj.Nx, ... 166 | 'Duration', obj.gx.flatTime, ... 167 | 'Delay', obj.gx.riseTime, ... 168 | 'system', obj.sys); 169 | obj.gxPre = mr.makeTrapezoid(obj.axesOrder{1},'Area',-obj.gx.area/2,'Duration',1e-3,'system',obj.sys); 170 | obj.gzReph = mr.makeTrapezoid(obj.axesOrder{3},'Area',-obj.gz.area/2,'Duration',1e-3,'system',obj.sys); 171 | obj.gxFlyBack = mr.makeTrapezoid(obj.axesOrder{1},'Area',-obj.gx.area,'system',obj.sys); 172 | obj.phaseAreas = -((0:obj.Ny-1)-obj.Ny/2)*deltak; % phase area should be Kmax for clin=0 and -Kmax for clin=Ny... strange 173 | 174 | % gradient spoiling 175 | obj.gxSpoil = mr.makeTrapezoid(obj.axesOrder{1},'Area', 2*obj.Nx*deltak,'system', obj.sys); 176 | obj.gzSpoil = mr.makeTrapezoid(obj.axesOrder{3},'Area',4/obj.thickness,'system', obj.sys); 177 | 178 | %% Calculate minimal TEs 179 | % First echo 180 | minTE1 = obj.gz.flatTime/2 + obj.gz.fallTime + mr.calcDuration(obj.gzReph) ... 181 | + mr.calcDuration(obj.gxPre) ... 182 | + mr.calcDuration(obj.gx)/2; 183 | disp(['Minimal TE1 is ' num2str((minTE1 + obj.gradFreeTime)*1000) ' ms']) 184 | 185 | obj.fillTE(1) = obj.roundUpToGRT(obj.TE(1) - minTE1); 186 | assert(obj.fillTE(1) >= obj.gradFreeTime, 'Assertion for TE1 failed'); 187 | 188 | % Second echo 189 | minTE2 = minTE1 + mr.calcDuration(obj.gx)/2 ... 190 | + mr.calcDuration(obj.gxFlyBack) ... 191 | + mr.calcDuration(obj.gx)/2 ... 192 | + obj.fillTE(1); 193 | disp(['Minimal TE2 is ' num2str(minTE2*1000) ' ms']) 194 | 195 | obj.fillTE(2) = obj.roundUpToGRT(obj.TE(2) - minTE2); 196 | assert(obj.fillTE(2) >= 0, 'Assertion for TE2 failed.'); 197 | 198 | %% Time from trigger to scanner acquisition 199 | obj.triggerToScannerAcqDelay = obj.fillTE(1) ... 200 | + mr.calcDuration(obj.gxPre) ... 201 | + obj.adc.delay; 202 | 203 | %% Absorb delayTE2 in gradient 204 | obj.gxFlyBack = mr.makeTrapezoid(obj.axesOrder{1},'Area',-obj.gx.area, ... 205 | 'Duration', mr.calcDuration(obj.gxFlyBack) + obj.fillTE(2), ... 206 | 'system', obj.sys); 207 | obj.fillTE(2) = 0; 208 | 209 | %% Prepare trigger 210 | obj.extTrigger = mr.makeDigitalOutputPulse('ext1','duration', obj.sys.gradRasterTime); 211 | 212 | %% Calculate minimal TR 213 | minTR = mr.calcDuration(obj.gz) ... 214 | + mr.calcDuration(obj.gzReph) ... 215 | + obj.fillTE(1) ... 216 | + mr.calcDuration(obj.gxPre) ... 217 | + mr.calcDuration(obj.gx) ... 218 | + mr.calcDuration(obj.gxFlyBack) ... 219 | + mr.calcDuration(obj.gx) ... 220 | + mr.calcDuration(obj.gxSpoil, obj.gzSpoil); 221 | disp(['Minimal TR is ' num2str(minTR*1000) ' ms']) 222 | 223 | obj.fillTR = obj.roundUpToGRT(obj.TR - minTR); 224 | assert(obj.fillTR >= 0, 'Assertion for TR failed.'); 225 | 226 | %% Calculate required camera acquisition duration 227 | obj.cameraAcqDuration = obj.fillTE(1) ... 228 | + mr.calcDuration(obj.gxPre) ... 229 | + mr.calcDuration(obj.gx) ... 230 | + mr.calcDuration(obj.gxFlyBack) ... 231 | + mr.calcDuration(obj.gx) ... 232 | + 1e-3; % To be safe 233 | obj.cameraAcqDuration = ceil(obj.cameraAcqDuration*1000)/1000; 234 | 235 | %% Phase settings 236 | obj.rf_phase = 0; 237 | obj.rf_inc = 0; 238 | 239 | %% Synchronization 240 | if obj.nSyncDynamics > 0 241 | for avg = 1:obj.nSyncDynamics 242 | slc = 1; 243 | lin = 1; 244 | obj = runKernel(obj, lin, slc, avg, KernelMode.Sync); 245 | end 246 | 247 | %% Add pause and reset flags 248 | if obj.preScanPause < 4 249 | warning('The pause between the synchronization and imaging scans should be equal or larger than 4 seconds. The current value is okay for simulation purposes.'); 250 | end 251 | 252 | obj.addBlock(mr.makeDelay(obj.preScanPause), mr.makeLabel('SET','LIN', 0), mr.makeLabel('SET','SLC', 0), mr.makeLabel('SET','AVG', 0)); 253 | end 254 | 255 | %% Dummies 256 | for lin = 1:min(obj.Ny,obj.nDummy) 257 | % loop over slices 258 | for slc = 1:obj.nSlices 259 | avg = 1; 260 | obj = runKernel(obj, lin, slc, avg, KernelMode.Dummy); 261 | end 262 | end 263 | 264 | %% Actual imaging sequence 265 | % loop over phase encodes and define sequence blocks 266 | for lin = 1:obj.Ny 267 | % loop over slices 268 | for slc = 1:obj.nSlices 269 | avg = 1; 270 | obj = runKernel(obj, lin, slc, avg, KernelMode.Imaging); 271 | end 272 | end 273 | 274 | % Set number of expected external triggers 275 | obj.nTrig = obj.Ny * obj.nSlices; 276 | 277 | %% check whether the timing of the sequence is correct 278 | [ok, error_report] = obj.seq.checkTiming; 279 | 280 | if (ok) 281 | fprintf('Timing check passed successfully\n'); 282 | else 283 | fprintf('Timing check failed! Error listing follows:\n'); 284 | fprintf([error_report{:}]); 285 | fprintf('\n'); 286 | end 287 | 288 | %% Calculate Camera Interleave TR (blank time) 289 | obj.CalculateInterleaveTR(obj.TR); 290 | 291 | %% Prepare sequence export 292 | obj.seq.setDefinition('Name', 'gre2d'); 293 | obj.seq.setDefinition('FOV', [obj.fov obj.fov obj.thickness*obj.nSlices*(1+obj.distanceFactorPercentage/100)]); 294 | obj.seq.setDefinition('TR', obj.TR); 295 | obj.seq.setDefinition('TE', obj.TE); 296 | 297 | %% Parameters needed be added to the scanner data header for trajectory merging 298 | obj.seq.setDefinition('TriggerToScannerAcqDelay', obj.triggerToScannerAcqDelay); 299 | 300 | %% Parameters to be set on the user interface of the Field Camera 301 | % The number of actually acquired dynamics depends on the cameraInterleaveTR. 302 | obj.seq.setDefinition('CameraNrDynamics', ceil(obj.nTrig/obj.skipFactor)); 303 | obj.seq.setDefinition('CameraNrSyncDynamics', obj.nSyncDynamics); 304 | obj.seq.setDefinition('CameraAcqDuration', obj.cameraAcqDuration); 305 | obj.seq.setDefinition('CameraInterleaveTR', obj.cameraInterleaveTR); 306 | obj.seq.setDefinition('CameraAqDelay', 0); 307 | obj.seq.setDefinition('AdcSampleTime', obj.adc.dwell); 308 | obj.seq.setDefinition('Matrix', [obj.Nx obj.Ny]); 309 | obj.seq.setDefinition('SliceShifts', [obj.thickness*([1:obj.nSlices]-1-(obj.nSlices-1)/2)]*(1+obj.distanceFactorPercentage/100)); 310 | obj.seq.setDefinition('readDir_SCT', readDir_SCT); 311 | obj.seq.setDefinition('phaseDir_SCT', phaseDir_SCT); 312 | obj.seq.setDefinition('sliceDir_SCT', sliceDir_SCT); 313 | 314 | %% Write to Pulseq file 315 | if not(isfolder('exports')) 316 | mkdir('exports') 317 | end 318 | obj.seq.write(strcat('exports/skope_gre_2d','_',string(obj.sliceOrientation),'_',string(obj.phaseEncDir),'.seq')); 319 | 320 | end 321 | end 322 | 323 | methods (Access=private) 324 | function obj = runKernel(obj, lin, slc, avg, mode) 325 | 326 | if not(isa(mode, 'KernelMode')) 327 | error('Expected a kernel mode argument') 328 | end 329 | 330 | 331 | %% RF and ADC settings 332 | if mode==KernelMode.Dummy || mode==KernelMode.Imaging 333 | obj.rf.freqOffset = obj.gz.amplitude * obj.thickness * (slc-1-(obj.nSlices-1)/2)*(1+obj.distanceFactorPercentage/100); 334 | obj.rf.phaseOffset = obj.rf_phase/180*pi; 335 | obj.adc.phaseOffset = obj.rf_phase/180*pi; 336 | obj.rf_inc = mod(obj.rf_inc + obj.rfSpoilingInc, 360.0); 337 | obj.rf_phase = mod(obj.rf_phase + obj.rf_inc, 360.0); 338 | obj.addBlock(obj.rf, obj.gz, mr.makeLabel('SET','PMC',false), mr.makeLabel('SET','AVG',avg-1)); 339 | else 340 | obj.rf.freqOffset = 0; 341 | obj.rf.phaseOffset = 0; 342 | obj.addBlock(obj.gz, mr.makeLabel('SET','PMC',true), mr.makeLabel('SET','AVG',avg-1)); 343 | end 344 | 345 | %% Slice refocusing gradient 346 | obj.addBlock(obj.gzReph); 347 | 348 | %% External trigger and gradient-free interval a 349 | if mode==KernelMode.Sync || mode==KernelMode.Imaging 350 | obj.addBlock(obj.extTrigger, mr.makeDelay(obj.fillTE(1))); 351 | else 352 | obj.addBlock(mr.makeDelay(obj.fillTE(1))); 353 | end 354 | 355 | %% Read-prewinding and phase encoding gradients 356 | gyPre = mr.makeTrapezoid(obj.axesOrder{2}, ... 357 | 'Area', obj.phaseAreas(lin), ... 358 | 'Duration', mr.calcDuration(obj.gxPre), ... 359 | 'system',obj.sys); 360 | obj.addBlock(obj.gxPre,gyPre); 361 | 362 | %% All LABELS / counters an flags are automatically initialized to 0 in the beginning, no need to define initial 0's 363 | % so we will just increment LIN after the ADC event (e.g. during the spoiler) 364 | %seq.addBlock(mr.makeDelay(1)); % older scanners like Trio may need this 365 | % dummy delay to keep up with timing 366 | 367 | %% Set labels 368 | labels = [ {mr.makeLabel('SET','LIN', lin-1)}, ... 369 | {mr.makeLabel('SET','SLC', slc-1)}, ... 370 | {mr.makeLabel('SET','AVG', avg-1)}]; 371 | 372 | %% First readout gradient 373 | if mode==KernelMode.Sync || mode==KernelMode.Imaging 374 | obj.addBlock(obj.gx, obj.adc, mr.makeLabel('SET','ECO', 0), labels{:}); 375 | else 376 | obj.addBlock(obj.gx, mr.makeLabel('SET','ECO', 0), labels{:}); 377 | end 378 | 379 | %% Fly back 380 | obj.addBlock(obj.gxFlyBack); 381 | 382 | %% Second readout gradient 383 | if mode==KernelMode.Sync || mode==KernelMode.Imaging 384 | obj.addBlock(obj.gx, obj.adc, mr.makeLabel('SET','ECO', 1)); 385 | else 386 | obj.addBlock(obj.gx, mr.makeLabel('SET','ECO', 1)); 387 | end 388 | 389 | %% Negative Phase encoding 390 | gyPre.amplitude = -gyPre.amplitude; 391 | 392 | %% Spoiling 393 | spoilBlockContents = {obj.gxSpoil, gyPre, obj.gzSpoil}; 394 | obj.addBlock(spoilBlockContents{:}); 395 | 396 | %% Add delay 397 | obj.addBlock(mr.makeDelay(obj.fillTR)); 398 | 399 | end 400 | end 401 | end -------------------------------------------------------------------------------- /exports/skope_gtf_linearityCheck.seq: -------------------------------------------------------------------------------- 1 | # Pulseq sequence file 2 | # Created by MATLAB mr toolbox 3 | 4 | [VERSION] 5 | major 1 6 | minor 4 7 | revision 1 8 | 9 | [DEFINITIONS] 10 | AdcRasterTime 1e-07 11 | BlockDurationRaster 1e-05 12 | CameraAcqDuration 0.07 13 | CameraAqDelay 0 14 | CameraInterleaveTR 0.4 15 | CameraNrDynamics 120 16 | CameraNrSyncDynamics 0 17 | GradientRasterTime 1e-05 18 | Name gtf_linCheck 19 | RadiofrequencyRasterTime 1e-06 20 | 21 | # Format of blocks: 22 | # NUM DUR RF GX GY GZ ADC EXT 23 | [BLOCKS] 24 | 1 1 0 0 0 0 0 1 25 | 2 99999 0 0 0 0 0 0 26 | 3 1 0 0 0 0 0 1 27 | 4 99999 0 0 0 0 0 0 28 | 5 1 0 0 0 0 0 1 29 | 6 200 0 0 0 0 0 0 30 | 7 2 0 1 0 0 0 0 31 | 8 99797 0 0 0 0 0 0 32 | 9 1 0 0 0 0 0 1 33 | 10 200 0 0 0 0 0 0 34 | 11 2 0 2 0 0 0 0 35 | 12 99797 0 0 0 0 0 0 36 | 13 1 0 0 0 0 0 1 37 | 14 200 0 0 0 0 0 0 38 | 15 4 0 3 0 0 0 0 39 | 16 99795 0 0 0 0 0 0 40 | 17 1 0 0 0 0 0 1 41 | 18 200 0 0 0 0 0 0 42 | 19 4 0 4 0 0 0 0 43 | 20 99795 0 0 0 0 0 0 44 | 21 1 0 0 0 0 0 1 45 | 22 200 0 0 0 0 0 0 46 | 23 6 0 5 0 0 0 0 47 | 24 99793 0 0 0 0 0 0 48 | 25 1 0 0 0 0 0 1 49 | 26 200 0 0 0 0 0 0 50 | 27 6 0 6 0 0 0 0 51 | 28 99793 0 0 0 0 0 0 52 | 29 1 0 0 0 0 0 1 53 | 30 200 0 0 0 0 0 0 54 | 31 8 0 7 0 0 0 0 55 | 32 99791 0 0 0 0 0 0 56 | 33 1 0 0 0 0 0 1 57 | 34 200 0 0 0 0 0 0 58 | 35 8 0 8 0 0 0 0 59 | 36 99791 0 0 0 0 0 0 60 | 37 1 0 0 0 0 0 1 61 | 38 200 0 0 0 0 0 0 62 | 39 10 0 9 0 0 0 0 63 | 40 99789 0 0 0 0 0 0 64 | 41 1 0 0 0 0 0 1 65 | 42 200 0 0 0 0 0 0 66 | 43 10 0 10 0 0 0 0 67 | 44 99789 0 0 0 0 0 0 68 | 45 1 0 0 0 0 0 1 69 | 46 200 0 0 0 0 0 0 70 | 47 12 0 11 0 0 0 0 71 | 48 99787 0 0 0 0 0 0 72 | 49 1 0 0 0 0 0 1 73 | 50 200 0 0 0 0 0 0 74 | 51 12 0 12 0 0 0 0 75 | 52 99787 0 0 0 0 0 0 76 | 53 1 0 0 0 0 0 1 77 | 54 200 0 0 0 0 0 0 78 | 55 14 0 13 0 0 0 0 79 | 56 99785 0 0 0 0 0 0 80 | 57 1 0 0 0 0 0 1 81 | 58 200 0 0 0 0 0 0 82 | 59 14 0 14 0 0 0 0 83 | 60 99785 0 0 0 0 0 0 84 | 61 1 0 0 0 0 0 1 85 | 62 200 0 0 0 0 0 0 86 | 63 16 0 15 0 0 0 0 87 | 64 99783 0 0 0 0 0 0 88 | 65 1 0 0 0 0 0 1 89 | 66 200 0 0 0 0 0 0 90 | 67 16 0 16 0 0 0 0 91 | 68 99783 0 0 0 0 0 0 92 | 69 1 0 0 0 0 0 1 93 | 70 200 0 0 0 0 0 0 94 | 71 18 0 17 0 0 0 0 95 | 72 99781 0 0 0 0 0 0 96 | 73 1 0 0 0 0 0 1 97 | 74 200 0 0 0 0 0 0 98 | 75 18 0 18 0 0 0 0 99 | 76 99781 0 0 0 0 0 0 100 | 77 1 0 0 0 0 0 1 101 | 78 200 0 0 0 0 0 0 102 | 79 20 0 19 0 0 0 0 103 | 80 99779 0 0 0 0 0 0 104 | 81 1 0 0 0 0 0 1 105 | 82 200 0 0 0 0 0 0 106 | 83 20 0 20 0 0 0 0 107 | 84 99779 0 0 0 0 0 0 108 | 85 1 0 0 0 0 0 1 109 | 86 200 0 0 0 0 0 0 110 | 87 22 0 21 0 0 0 0 111 | 88 99777 0 0 0 0 0 0 112 | 89 1 0 0 0 0 0 1 113 | 90 200 0 0 0 0 0 0 114 | 91 22 0 22 0 0 0 0 115 | 92 99777 0 0 0 0 0 0 116 | 93 1 0 0 0 0 0 1 117 | 94 200 0 0 0 0 0 0 118 | 95 24 0 23 0 0 0 0 119 | 96 99775 0 0 0 0 0 0 120 | 97 1 0 0 0 0 0 1 121 | 98 200 0 0 0 0 0 0 122 | 99 24 0 24 0 0 0 0 123 | 100 99775 0 0 0 0 0 0 124 | 101 1 0 0 0 0 0 1 125 | 102 200 0 0 0 0 0 0 126 | 103 26 0 25 0 0 0 0 127 | 104 99773 0 0 0 0 0 0 128 | 105 1 0 0 0 0 0 1 129 | 106 200 0 0 0 0 0 0 130 | 107 26 0 26 0 0 0 0 131 | 108 99773 0 0 0 0 0 0 132 | 109 1 0 0 0 0 0 1 133 | 110 200 0 0 0 0 0 0 134 | 111 28 0 27 0 0 0 0 135 | 112 99771 0 0 0 0 0 0 136 | 113 1 0 0 0 0 0 1 137 | 114 200 0 0 0 0 0 0 138 | 115 28 0 28 0 0 0 0 139 | 116 99771 0 0 0 0 0 0 140 | 117 1 0 0 0 0 0 1 141 | 118 200 0 0 0 0 0 0 142 | 119 30 0 29 0 0 0 0 143 | 120 99769 0 0 0 0 0 0 144 | 121 1 0 0 0 0 0 1 145 | 122 200 0 0 0 0 0 0 146 | 123 30 0 30 0 0 0 0 147 | 124 99769 0 0 0 0 0 0 148 | 125 1 0 0 0 0 0 1 149 | 126 200 0 0 0 0 0 0 150 | 127 32 0 31 0 0 0 0 151 | 128 99767 0 0 0 0 0 0 152 | 129 1 0 0 0 0 0 1 153 | 130 200 0 0 0 0 0 0 154 | 131 32 0 32 0 0 0 0 155 | 132 99767 0 0 0 0 0 0 156 | 133 1 0 0 0 0 0 1 157 | 134 200 0 0 0 0 0 0 158 | 135 34 0 33 0 0 0 0 159 | 136 99765 0 0 0 0 0 0 160 | 137 1 0 0 0 0 0 1 161 | 138 200 0 0 0 0 0 0 162 | 139 34 0 34 0 0 0 0 163 | 140 99765 0 0 0 0 0 0 164 | 141 1 0 0 0 0 0 1 165 | 142 200 0 0 0 0 0 0 166 | 143 36 0 35 0 0 0 0 167 | 144 99763 0 0 0 0 0 0 168 | 145 1 0 0 0 0 0 1 169 | 146 200 0 0 0 0 0 0 170 | 147 36 0 36 0 0 0 0 171 | 148 99763 0 0 0 0 0 0 172 | 149 1 0 0 0 0 0 1 173 | 150 200 0 0 0 0 0 0 174 | 151 38 0 37 0 0 0 0 175 | 152 99761 0 0 0 0 0 0 176 | 153 1 0 0 0 0 0 1 177 | 154 200 0 0 0 0 0 0 178 | 155 38 0 38 0 0 0 0 179 | 156 99761 0 0 0 0 0 0 180 | 157 1 0 0 0 0 0 1 181 | 158 99999 0 0 0 0 0 0 182 | 159 1 0 0 0 0 0 1 183 | 160 99999 0 0 0 0 0 0 184 | 161 1 0 0 0 0 0 1 185 | 162 200 0 0 0 0 0 0 186 | 163 2 0 0 1 0 0 0 187 | 164 99797 0 0 0 0 0 0 188 | 165 1 0 0 0 0 0 1 189 | 166 200 0 0 0 0 0 0 190 | 167 2 0 0 2 0 0 0 191 | 168 99797 0 0 0 0 0 0 192 | 169 1 0 0 0 0 0 1 193 | 170 200 0 0 0 0 0 0 194 | 171 4 0 0 3 0 0 0 195 | 172 99795 0 0 0 0 0 0 196 | 173 1 0 0 0 0 0 1 197 | 174 200 0 0 0 0 0 0 198 | 175 4 0 0 4 0 0 0 199 | 176 99795 0 0 0 0 0 0 200 | 177 1 0 0 0 0 0 1 201 | 178 200 0 0 0 0 0 0 202 | 179 6 0 0 5 0 0 0 203 | 180 99793 0 0 0 0 0 0 204 | 181 1 0 0 0 0 0 1 205 | 182 200 0 0 0 0 0 0 206 | 183 6 0 0 6 0 0 0 207 | 184 99793 0 0 0 0 0 0 208 | 185 1 0 0 0 0 0 1 209 | 186 200 0 0 0 0 0 0 210 | 187 8 0 0 7 0 0 0 211 | 188 99791 0 0 0 0 0 0 212 | 189 1 0 0 0 0 0 1 213 | 190 200 0 0 0 0 0 0 214 | 191 8 0 0 8 0 0 0 215 | 192 99791 0 0 0 0 0 0 216 | 193 1 0 0 0 0 0 1 217 | 194 200 0 0 0 0 0 0 218 | 195 10 0 0 9 0 0 0 219 | 196 99789 0 0 0 0 0 0 220 | 197 1 0 0 0 0 0 1 221 | 198 200 0 0 0 0 0 0 222 | 199 10 0 0 10 0 0 0 223 | 200 99789 0 0 0 0 0 0 224 | 201 1 0 0 0 0 0 1 225 | 202 200 0 0 0 0 0 0 226 | 203 12 0 0 11 0 0 0 227 | 204 99787 0 0 0 0 0 0 228 | 205 1 0 0 0 0 0 1 229 | 206 200 0 0 0 0 0 0 230 | 207 12 0 0 12 0 0 0 231 | 208 99787 0 0 0 0 0 0 232 | 209 1 0 0 0 0 0 1 233 | 210 200 0 0 0 0 0 0 234 | 211 14 0 0 13 0 0 0 235 | 212 99785 0 0 0 0 0 0 236 | 213 1 0 0 0 0 0 1 237 | 214 200 0 0 0 0 0 0 238 | 215 14 0 0 14 0 0 0 239 | 216 99785 0 0 0 0 0 0 240 | 217 1 0 0 0 0 0 1 241 | 218 200 0 0 0 0 0 0 242 | 219 16 0 0 15 0 0 0 243 | 220 99783 0 0 0 0 0 0 244 | 221 1 0 0 0 0 0 1 245 | 222 200 0 0 0 0 0 0 246 | 223 16 0 0 16 0 0 0 247 | 224 99783 0 0 0 0 0 0 248 | 225 1 0 0 0 0 0 1 249 | 226 200 0 0 0 0 0 0 250 | 227 18 0 0 17 0 0 0 251 | 228 99781 0 0 0 0 0 0 252 | 229 1 0 0 0 0 0 1 253 | 230 200 0 0 0 0 0 0 254 | 231 18 0 0 18 0 0 0 255 | 232 99781 0 0 0 0 0 0 256 | 233 1 0 0 0 0 0 1 257 | 234 200 0 0 0 0 0 0 258 | 235 20 0 0 19 0 0 0 259 | 236 99779 0 0 0 0 0 0 260 | 237 1 0 0 0 0 0 1 261 | 238 200 0 0 0 0 0 0 262 | 239 20 0 0 20 0 0 0 263 | 240 99779 0 0 0 0 0 0 264 | 241 1 0 0 0 0 0 1 265 | 242 200 0 0 0 0 0 0 266 | 243 22 0 0 21 0 0 0 267 | 244 99777 0 0 0 0 0 0 268 | 245 1 0 0 0 0 0 1 269 | 246 200 0 0 0 0 0 0 270 | 247 22 0 0 22 0 0 0 271 | 248 99777 0 0 0 0 0 0 272 | 249 1 0 0 0 0 0 1 273 | 250 200 0 0 0 0 0 0 274 | 251 24 0 0 23 0 0 0 275 | 252 99775 0 0 0 0 0 0 276 | 253 1 0 0 0 0 0 1 277 | 254 200 0 0 0 0 0 0 278 | 255 24 0 0 24 0 0 0 279 | 256 99775 0 0 0 0 0 0 280 | 257 1 0 0 0 0 0 1 281 | 258 200 0 0 0 0 0 0 282 | 259 26 0 0 25 0 0 0 283 | 260 99773 0 0 0 0 0 0 284 | 261 1 0 0 0 0 0 1 285 | 262 200 0 0 0 0 0 0 286 | 263 26 0 0 26 0 0 0 287 | 264 99773 0 0 0 0 0 0 288 | 265 1 0 0 0 0 0 1 289 | 266 200 0 0 0 0 0 0 290 | 267 28 0 0 27 0 0 0 291 | 268 99771 0 0 0 0 0 0 292 | 269 1 0 0 0 0 0 1 293 | 270 200 0 0 0 0 0 0 294 | 271 28 0 0 28 0 0 0 295 | 272 99771 0 0 0 0 0 0 296 | 273 1 0 0 0 0 0 1 297 | 274 200 0 0 0 0 0 0 298 | 275 30 0 0 29 0 0 0 299 | 276 99769 0 0 0 0 0 0 300 | 277 1 0 0 0 0 0 1 301 | 278 200 0 0 0 0 0 0 302 | 279 30 0 0 30 0 0 0 303 | 280 99769 0 0 0 0 0 0 304 | 281 1 0 0 0 0 0 1 305 | 282 200 0 0 0 0 0 0 306 | 283 32 0 0 31 0 0 0 307 | 284 99767 0 0 0 0 0 0 308 | 285 1 0 0 0 0 0 1 309 | 286 200 0 0 0 0 0 0 310 | 287 32 0 0 32 0 0 0 311 | 288 99767 0 0 0 0 0 0 312 | 289 1 0 0 0 0 0 1 313 | 290 200 0 0 0 0 0 0 314 | 291 34 0 0 33 0 0 0 315 | 292 99765 0 0 0 0 0 0 316 | 293 1 0 0 0 0 0 1 317 | 294 200 0 0 0 0 0 0 318 | 295 34 0 0 34 0 0 0 319 | 296 99765 0 0 0 0 0 0 320 | 297 1 0 0 0 0 0 1 321 | 298 200 0 0 0 0 0 0 322 | 299 36 0 0 35 0 0 0 323 | 300 99763 0 0 0 0 0 0 324 | 301 1 0 0 0 0 0 1 325 | 302 200 0 0 0 0 0 0 326 | 303 36 0 0 36 0 0 0 327 | 304 99763 0 0 0 0 0 0 328 | 305 1 0 0 0 0 0 1 329 | 306 200 0 0 0 0 0 0 330 | 307 38 0 0 37 0 0 0 331 | 308 99761 0 0 0 0 0 0 332 | 309 1 0 0 0 0 0 1 333 | 310 200 0 0 0 0 0 0 334 | 311 38 0 0 38 0 0 0 335 | 312 99761 0 0 0 0 0 0 336 | 313 1 0 0 0 0 0 1 337 | 314 99999 0 0 0 0 0 0 338 | 315 1 0 0 0 0 0 1 339 | 316 99999 0 0 0 0 0 0 340 | 317 1 0 0 0 0 0 1 341 | 318 200 0 0 0 0 0 0 342 | 319 2 0 0 0 1 0 0 343 | 320 99797 0 0 0 0 0 0 344 | 321 1 0 0 0 0 0 1 345 | 322 200 0 0 0 0 0 0 346 | 323 2 0 0 0 2 0 0 347 | 324 99797 0 0 0 0 0 0 348 | 325 1 0 0 0 0 0 1 349 | 326 200 0 0 0 0 0 0 350 | 327 4 0 0 0 3 0 0 351 | 328 99795 0 0 0 0 0 0 352 | 329 1 0 0 0 0 0 1 353 | 330 200 0 0 0 0 0 0 354 | 331 4 0 0 0 4 0 0 355 | 332 99795 0 0 0 0 0 0 356 | 333 1 0 0 0 0 0 1 357 | 334 200 0 0 0 0 0 0 358 | 335 6 0 0 0 5 0 0 359 | 336 99793 0 0 0 0 0 0 360 | 337 1 0 0 0 0 0 1 361 | 338 200 0 0 0 0 0 0 362 | 339 6 0 0 0 6 0 0 363 | 340 99793 0 0 0 0 0 0 364 | 341 1 0 0 0 0 0 1 365 | 342 200 0 0 0 0 0 0 366 | 343 8 0 0 0 7 0 0 367 | 344 99791 0 0 0 0 0 0 368 | 345 1 0 0 0 0 0 1 369 | 346 200 0 0 0 0 0 0 370 | 347 8 0 0 0 8 0 0 371 | 348 99791 0 0 0 0 0 0 372 | 349 1 0 0 0 0 0 1 373 | 350 200 0 0 0 0 0 0 374 | 351 10 0 0 0 9 0 0 375 | 352 99789 0 0 0 0 0 0 376 | 353 1 0 0 0 0 0 1 377 | 354 200 0 0 0 0 0 0 378 | 355 10 0 0 0 10 0 0 379 | 356 99789 0 0 0 0 0 0 380 | 357 1 0 0 0 0 0 1 381 | 358 200 0 0 0 0 0 0 382 | 359 12 0 0 0 11 0 0 383 | 360 99787 0 0 0 0 0 0 384 | 361 1 0 0 0 0 0 1 385 | 362 200 0 0 0 0 0 0 386 | 363 12 0 0 0 12 0 0 387 | 364 99787 0 0 0 0 0 0 388 | 365 1 0 0 0 0 0 1 389 | 366 200 0 0 0 0 0 0 390 | 367 14 0 0 0 13 0 0 391 | 368 99785 0 0 0 0 0 0 392 | 369 1 0 0 0 0 0 1 393 | 370 200 0 0 0 0 0 0 394 | 371 14 0 0 0 14 0 0 395 | 372 99785 0 0 0 0 0 0 396 | 373 1 0 0 0 0 0 1 397 | 374 200 0 0 0 0 0 0 398 | 375 16 0 0 0 15 0 0 399 | 376 99783 0 0 0 0 0 0 400 | 377 1 0 0 0 0 0 1 401 | 378 200 0 0 0 0 0 0 402 | 379 16 0 0 0 16 0 0 403 | 380 99783 0 0 0 0 0 0 404 | 381 1 0 0 0 0 0 1 405 | 382 200 0 0 0 0 0 0 406 | 383 18 0 0 0 17 0 0 407 | 384 99781 0 0 0 0 0 0 408 | 385 1 0 0 0 0 0 1 409 | 386 200 0 0 0 0 0 0 410 | 387 18 0 0 0 18 0 0 411 | 388 99781 0 0 0 0 0 0 412 | 389 1 0 0 0 0 0 1 413 | 390 200 0 0 0 0 0 0 414 | 391 20 0 0 0 19 0 0 415 | 392 99779 0 0 0 0 0 0 416 | 393 1 0 0 0 0 0 1 417 | 394 200 0 0 0 0 0 0 418 | 395 20 0 0 0 20 0 0 419 | 396 99779 0 0 0 0 0 0 420 | 397 1 0 0 0 0 0 1 421 | 398 200 0 0 0 0 0 0 422 | 399 22 0 0 0 21 0 0 423 | 400 99777 0 0 0 0 0 0 424 | 401 1 0 0 0 0 0 1 425 | 402 200 0 0 0 0 0 0 426 | 403 22 0 0 0 22 0 0 427 | 404 99777 0 0 0 0 0 0 428 | 405 1 0 0 0 0 0 1 429 | 406 200 0 0 0 0 0 0 430 | 407 24 0 0 0 23 0 0 431 | 408 99775 0 0 0 0 0 0 432 | 409 1 0 0 0 0 0 1 433 | 410 200 0 0 0 0 0 0 434 | 411 24 0 0 0 24 0 0 435 | 412 99775 0 0 0 0 0 0 436 | 413 1 0 0 0 0 0 1 437 | 414 200 0 0 0 0 0 0 438 | 415 26 0 0 0 25 0 0 439 | 416 99773 0 0 0 0 0 0 440 | 417 1 0 0 0 0 0 1 441 | 418 200 0 0 0 0 0 0 442 | 419 26 0 0 0 26 0 0 443 | 420 99773 0 0 0 0 0 0 444 | 421 1 0 0 0 0 0 1 445 | 422 200 0 0 0 0 0 0 446 | 423 28 0 0 0 27 0 0 447 | 424 99771 0 0 0 0 0 0 448 | 425 1 0 0 0 0 0 1 449 | 426 200 0 0 0 0 0 0 450 | 427 28 0 0 0 28 0 0 451 | 428 99771 0 0 0 0 0 0 452 | 429 1 0 0 0 0 0 1 453 | 430 200 0 0 0 0 0 0 454 | 431 30 0 0 0 29 0 0 455 | 432 99769 0 0 0 0 0 0 456 | 433 1 0 0 0 0 0 1 457 | 434 200 0 0 0 0 0 0 458 | 435 30 0 0 0 30 0 0 459 | 436 99769 0 0 0 0 0 0 460 | 437 1 0 0 0 0 0 1 461 | 438 200 0 0 0 0 0 0 462 | 439 32 0 0 0 31 0 0 463 | 440 99767 0 0 0 0 0 0 464 | 441 1 0 0 0 0 0 1 465 | 442 200 0 0 0 0 0 0 466 | 443 32 0 0 0 32 0 0 467 | 444 99767 0 0 0 0 0 0 468 | 445 1 0 0 0 0 0 1 469 | 446 200 0 0 0 0 0 0 470 | 447 34 0 0 0 33 0 0 471 | 448 99765 0 0 0 0 0 0 472 | 449 1 0 0 0 0 0 1 473 | 450 200 0 0 0 0 0 0 474 | 451 34 0 0 0 34 0 0 475 | 452 99765 0 0 0 0 0 0 476 | 453 1 0 0 0 0 0 1 477 | 454 200 0 0 0 0 0 0 478 | 455 36 0 0 0 35 0 0 479 | 456 99763 0 0 0 0 0 0 480 | 457 1 0 0 0 0 0 1 481 | 458 200 0 0 0 0 0 0 482 | 459 36 0 0 0 36 0 0 483 | 460 99763 0 0 0 0 0 0 484 | 461 1 0 0 0 0 0 1 485 | 462 200 0 0 0 0 0 0 486 | 463 38 0 0 0 37 0 0 487 | 464 99761 0 0 0 0 0 0 488 | 465 1 0 0 0 0 0 1 489 | 466 200 0 0 0 0 0 0 490 | 467 38 0 0 0 38 0 0 491 | 468 99761 0 0 0 0 0 0 492 | 469 2 0 39 0 0 0 0 493 | 494 | # Format of arbitrary gradients: 495 | # time_shape_id of 0 means default timing (stepping with grad_raster starting at 1/2 of grad_raster) 496 | # id amplitude amp_shape_id time_shape_id delay 497 | # .. Hz/m .. .. us 498 | [GRADIENTS] 499 | 39 0 1 2 0 500 | 501 | # Format of trapezoid gradients: 502 | # id amplitude rise flat fall delay 503 | # .. Hz/m us us us us 504 | [TRAP] 505 | 1 38318.4 10 0 10 0 506 | 2 76636.8 10 0 10 0 507 | 3 76636.8 20 0 20 0 508 | 4 153274 20 0 20 0 509 | 5 114955 30 0 30 0 510 | 6 229910 30 0 30 0 511 | 7 153274 40 0 40 0 512 | 8 306547 40 0 40 0 513 | 9 191592 50 0 50 0 514 | 10 383184 50 0 50 0 515 | 11 229910 60 0 60 0 516 | 12 459821 60 0 60 0 517 | 13 268229 70 0 70 0 518 | 14 536458 70 0 70 0 519 | 15 306547 80 0 80 0 520 | 16 613094 80 0 80 0 521 | 17 344866 90 0 90 0 522 | 18 689731 90 0 90 0 523 | 19 383184 100 0 100 0 524 | 20 766368 100 0 100 0 525 | 21 421502 110 0 110 0 526 | 22 843005 110 0 110 0 527 | 23 459821 120 0 120 0 528 | 24 919642 120 0 120 0 529 | 25 498139 130 0 130 0 530 | 26 996278 130 0 130 0 531 | 27 536458 140 0 140 0 532 | 28 1.07292e+06 140 0 140 0 533 | 29 574776 150 0 150 0 534 | 30 1.14955e+06 150 0 150 0 535 | 31 613094 160 0 160 0 536 | 32 1.22619e+06 160 0 160 0 537 | 33 651413 170 0 170 0 538 | 34 1.30283e+06 170 0 170 0 539 | 35 689731 180 0 180 0 540 | 36 1.37946e+06 180 0 180 0 541 | 37 728050 190 0 190 0 542 | 38 1.4561e+06 190 0 190 0 543 | 544 | # Format of extension lists: 545 | # id type ref next_id 546 | # next_id of 0 terminates the list 547 | # Extension list is followed by extension specifications 548 | [EXTENSIONS] 549 | 1 1 1 0 550 | 551 | # Extension specification for digital output and input triggers: 552 | # id type channel delay (us) duration (us) 553 | extension TRIGGERS 1 554 | 1 1 3 0 10 555 | 556 | # Sequence Shapes 557 | [SHAPES] 558 | 559 | shape_id 1 560 | num_samples 2 561 | 0 562 | 0 563 | 564 | shape_id 2 565 | num_samples 2 566 | 0.5 567 | 1.5 568 | 569 | 570 | [SIGNATURE] 571 | # This is the hash of the Pulseq file, calculated right before the [SIGNATURE] section was added 572 | # It can be reproduced/verified with md5sum if the file trimmed to the position right above [SIGNATURE] 573 | # The new line character preceding [SIGNATURE] BELONGS to the signature (and needs to be sripped away for recalculating/verification) 574 | Type md5 575 | Hash 37bc6a079eabf7a92f049a566c8a80ac 576 | --------------------------------------------------------------------------------