diff --git a/examples/matRad_example8_photonsVMAT.m b/examples/matRad_example8_photonsVMAT.m new file mode 100644 index 000000000..9835232b7 --- /dev/null +++ b/examples/matRad_example8_photonsVMAT.m @@ -0,0 +1,125 @@ +%% Example Photon Treatment Plan with VMAT direct aperture optimization +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2017 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSES.txt. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%% +% In this example we will show +% (i) how to load patient data into matRad +% (ii) how to input necessary parameters in the pln structure +% (iii) how to setup a photon dose calculation +% (iv) how to inversely optimize fluence directly from command window in MatLab. +% (v) how to apply a sequencing algorithm +% (vi) how to run a VMAT direct aperture optimization +% (vii) how to visually and quantitatively evaluate the result + +%% Patient Data Import +% Let's begin with a clear Matlab environment and import the TG119 patient +% into your workspace +matRad_rc + +load TG119.mat + +%% Treatment Plan +% The next step is to define your treatment plan labeled as 'pln'. This +% structure requires input from the treatment planner and defines +% the most important cornerstones of your treatment plan. + +% meta information for treatment plan +pln.numOfFractions = 30; +pln.radiationMode = 'photons'; % either photons / protons / helium / carbon / brachy +pln.machine = 'Generic'; % generic for RT / LDR or HDR for BT + +pln.bioModel = 'none'; % none: for photons, protons, carbon, brachy % constRBE: constant RBE for photons and protons + % MCN: McNamara-variable RBE model for protons % WED: Wedenberg-variable RBE model for protons + % LEM: Local Effect Model for carbon ions % HEL: data-driven RBE parametrization for helium + +pln.multScen = 'nomScen'; % scenario creation type 'nomScen' 'wcScen' 'impScen' 'rndScen' + +% beam geometry settings +pln.propStf.bixelWidth = 5; % [mm] / also corresponds to lateral spot spacing for particles +pln.propStf.maxGantryAngleSpacing = 30; % [°] / max gantry angle spacing for dose calculation +pln.propStf.maxDAOGantryAngleSpacing = 60; % [°] / max gantry angle spacing for DAO +pln.propStf.maxFMOGantryAngleSpacing = 180; % [°] / max gantry angle spacing for FMO +pln.propStf.startingAngle = -180; % [°] / starting angle for VMAT +pln.propStf.finishingAngle = 180; % [°] / finishing angle for VMAT +pln.propStf.couchAngle = 0; % [°] +pln.propStf.isoCenter = matRad_getIsoCenter(cst,ct,0); +pln.propStf.generator = 'PhotonVMAT'; +pln.propStf.continuousAperture = false; + +% dose calculation settings +pln.propDoseCalc.doseGrid.resolution.x = 5; % [mm] +pln.propDoseCalc.doseGrid.resolution.y = 5; % [mm] +pln.propDoseCalc.doseGrid.resolution.z = 5; % [mm] + +% sequencing settings +pln.propSeq.runSequencing = true; % true: run sequencing, false: don't / will be ignored for particles and also triggered by runDAO below +pln.propSeq.sequencer = 'siochi'; +pln.propSeq.numLevels = 7; + +% optimization settings +pln.propOpt.quantityOpt = 'physicalDose'; % Quantity to optimizer (could also be RBExDose, BED, effect) +pln.propOpt.optimizer = 'IPOPT'; % We can also utilize 'fmincon' from Matlab's optimization toolbox +pln.propOpt.runDAO = true; % 1/true: run DAO, 0/false: don't / will be ignored for particles +pln.propOpt.runVMAT = true; +pln.propOpt.preconditioner = true; + +%pln.propOpt.VMAToptions.machineConstraintFile = [pln.radiationMode '_' pln.machine]; + +%% Generate Beam Geometry STF +stf = matRad_generateStf(ct,cst,pln); + +%% Dose Calculation +% Lets generate dosimetric information by pre-computing dose influence +% matrices for unit beamlet intensities. Having dose influences available +% allows for subsequent inverse optimization. +dij = matRad_calcDoseInfluence(ct, cst, stf, pln); + +%% Inverse Planning for IMRT +% The goal of the fluence optimization is to find a set of beamlet weights +% which yield the best possible dose distribution according to the +% predefined clinical objectives and constraints underlying the radiation +% treatment. In VMAT, FMO is done only at the angles in the +% FMOGantryAngles set. Once the optimization has finished, trigger once the GUI to +% visualize the optimized dose cubes. +resultGUI = matRad_fluenceOptimization(dij,cst,pln,stf); +matRadGUI; + +%% Sequencing +% This is a multileaf collimator leaf sequencing algorithm that is used in +% order to modulate the intensity of the beams with multiple static +% segments, so that translates each intensity map into a set of deliverable +% aperture shapes. The fluence map at each angle in the initGantryAngles +% set is sequenced, with the resulting apertures spread to neighbouring +% angles from the optGantryAngles set. +resultGUI = matRad_sequencing(resultGUI,stf,dij,pln); + +%% DAO - Direct Aperture Optimization +% The Direct Aperture Optimization is an optimization approach where we +% directly optimize aperture shapes and weights at the angles in the +% optGantryAngles set. The gantry angle speed, leaf speed, and MU rate are +% constrained by the min and max values specified by the user. +resultGUI = matRad_directApertureOptimization(dij,cst,resultGUI.apertureInfo,resultGUI,pln); + +%% Aperture visualization +% Use a matrad function to visualize the resulting aperture shapes +matRad_visApertureInfo(resultGUI.apertureInfo); + +%% Indicator Calculation and display of DVH and QI +resultGUI = matRad_planAnalysis(resultGUI,ct,cst,stf,pln); + +%% Calculate delivery metrics + +resultGUI = matRad_calcDeliveryMetrics(resultGUI,pln,stf); + diff --git a/matRad/MatRad_Config.m b/matRad/MatRad_Config.m index 06e12316b..bbf9ae9b9 100644 --- a/matRad/MatRad_Config.m +++ b/matRad/MatRad_Config.m @@ -249,6 +249,7 @@ function setDefaultProperties(obj) %Sequencing Options obj.defaults.propSeq.sequencer = 'siochi'; + obj.defaults.propSeq.numLevels = 5; @@ -324,12 +325,12 @@ function setDefaultGUIProperties(obj) if ispc light = logical(winqueryreg('HKEY_CURRENT_USER','Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize','AppsUseLightTheme')); elseif ismac - out = system('defaults read -g AppleInterfaceStyle'); - if ~strcmp(out,'Dark') + [~,out] = system('defaults read -g AppleInterfaceStyle'); + if ~strcmp(out(1:end-1),'Dark') light = true; end else - out = system('gsettings get org.gnome.desktop.interface color-scheme'); + [~,out] = system('gsettings get org.gnome.desktop.interface color-scheme'); if strcmp(out,'prefer-light') light = true; end diff --git a/matRad/basedata/photons_Generic.mat b/matRad/basedata/photons_Generic.mat index 7f0b7ee35..79b003550 100644 Binary files a/matRad/basedata/photons_Generic.mat and b/matRad/basedata/photons_Generic.mat differ diff --git a/matRad/dicom/@matRad_DicomImporter/matRad_importDicomSteeringPhotons.m b/matRad/dicom/@matRad_DicomImporter/matRad_importDicomSteeringPhotons.m index c591699f2..88a3d8b20 100644 --- a/matRad/dicom/@matRad_DicomImporter/matRad_importDicomSteeringPhotons.m +++ b/matRad/dicom/@matRad_DicomImporter/matRad_importDicomSteeringPhotons.m @@ -47,7 +47,16 @@ % set necessary steering information obj.stf(i).gantryAngle = UniqueComb(i,1); obj.stf(i).couchAngle = UniqueComb(i,2); - obj.stf(i).isoCenter = obj.pln.propStf.isoCenter(i,:); + + %Handle possibility of multiple isocenters + if size(obj.pln.propStf.isoCenter,1) == 1 + obj.stf(i).isoCenter = obj.pln.propStf.isoCenter; + elseif size(pln.propStf.isoCenter,1) == obj.pln.propStf.numOfBeams + obj.stf(i).isoCenter = obj.pln.propStf.isoCenter(i,:); + else + matRad_cfg = MatRad_Config.instance(); + matRad_cfg.dispError('Invalid number of isocenters - should either be one or as many as beams!'); + end % bixelWidth = 'field' as keyword for whole field dose calc obj.stf(i).bixelWidth = 'field'; diff --git a/matRad/doseCalc/+DoseEngines/matRad_PhotonPencilBeamSVDEngine.m b/matRad/doseCalc/+DoseEngines/matRad_PhotonPencilBeamSVDEngine.m index dd273c235..adb8fde38 100644 --- a/matRad/doseCalc/+DoseEngines/matRad_PhotonPencilBeamSVDEngine.m +++ b/matRad/doseCalc/+DoseEngines/matRad_PhotonPencilBeamSVDEngine.m @@ -159,6 +159,17 @@ function setDefaults(this) matRad_cfg.dispWarning('Kernel Cut-Off ''%f mm'' cannot be smaller than geometric lateral cutoff ''%f mm''. Using ''%f mm''!',this.kernelCutOff,this.geometricLateralCutOff,this.geometricLateralCutOff); this.kernelCutOff = this.geometricLateralCutOff; end + + % TODO: calculate and add weightToMU for the generic photon + % machine. Typical calibration: 100 cGy/100 MU in a 10x10 cm^2 + % field, 100 cm SSD, depth of dose maximum for the given beam + % quality. + if isfield(this.machine.data,'weightToMU') + dij.weightToMU = this.machine.data.weightToMU; + else + dij.weightToMU = 100; + matRad_cfg.dispWarning('photon machine file does not contain weight to MU scaling factor. Assuming %.1f.',dij.weightToMU); + end %% kernel convolution % set up convolution grid diff --git a/matRad/doseCalc/matRad_calcPhotonDoseVmc.m b/matRad/doseCalc/matRad_calcPhotonDoseVmc.m new file mode 100644 index 000000000..65543d8ba --- /dev/null +++ b/matRad/doseCalc/matRad_calcPhotonDoseVmc.m @@ -0,0 +1,343 @@ +function dij = matRad_calcPhotonDoseVmc(ct,stf,pln,cst,calcDoseDirect) +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% matRad vmc++ photon dose calculation wrapper +% +% call +% dij = matRad_calcPhotonDoseVmc(ct,stf,pln,cst,calcDoseDirect) +% +% input +% ct: matRad ct struct +% stf: matRad steering information struct +% pln: matRad plan meta information struct +% cst: matRad cst struct +% calcDoseDirect: boolian switch to bypass dose influence matrix +% computation and directly calculate dose; only makes +% sense in combination with matRad_calcDoseDirect.m% +% output +% dij: matRad dij struct +% +% References +% +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +dij.radiationMode = pln.radiationMode; + +% default: dose influence matrix computation +if ~exist('calcDoseDirect','var') + calcDoseDirect = false; +end + +% set output level. 0 = no vmc specific output. 1 = print to matlab cmd. +% 2 = open in terminal(s) +verbose = 0; + +if ~isdeployed % only if _not_ running as standalone + % add path for optimization functions + matRadRootDir = fileparts(mfilename('fullpath')); + addpath(fullfile(matRadRootDir,'vmc++')) +end + +% meta information for dij +dij.numOfBeams = pln.propStf.numOfBeams; +dij.numOfVoxels = prod(ct.cubeDim); +dij.resolution = ct.resolution; +dij.dimensions = ct.cubeDim; +dij.numOfScenarios = 1; +dij.numOfRaysPerBeam = [stf(:).numOfRays]; +dij.weightToMU = 100; +dij.scaleFactor = 1; +dij.totalNumOfBixels = sum([stf(:).totalNumOfBixels]); +dij.totalNumOfRays = sum(dij.numOfRaysPerBeam); + +% check if full dose influence data is required +if calcDoseDirect + numOfColumnsDij = length(stf); + numOfBixelsContainer = 1; +else + numOfColumnsDij = dij.totalNumOfBixels; + numOfBixelsContainer = ceil(dij.totalNumOfBixels/10); +end + +% set up arrays for book keeping +dij.bixelNum = NaN*ones(numOfColumnsDij,1); +dij.rayNum = NaN*ones(numOfColumnsDij,1); +dij.beamNum = NaN*ones(numOfColumnsDij,1); + +bixelNum = NaN*ones(dij.totalNumOfBixels,1); +rayNum = NaN*ones(dij.totalNumOfBixels,1); +beamNum = NaN*ones(dij.totalNumOfBixels,1); + +doseTmpContainer = cell(numOfBixelsContainer,dij.numOfScenarios); +doseTmpContainerError = cell(numOfBixelsContainer,dij.numOfScenarios); + +% Allocate space for dij.physicalDose sparse matrix +for i = 1:dij.numOfScenarios + dij.physicalDose{i} = spalloc(prod(ct.cubeDim),numOfColumnsDij,1); + dij.physicalDoseError{i} = spalloc(prod(ct.cubeDim),numOfColumnsDij,1); +end + +% set environment variables for vmc++ +cd(fileparts(mfilename('fullpath'))) + +if exist(['vmc++' filesep 'bin'],'dir') ~= 7 + error(['Could not locate vmc++ environment. ' ... + 'Please provide the files in the correct folder structure at matRadroot' filesep 'vmc++.']); +else + VMCPath = fullfile(pwd , 'vmc++'); + switch pln.propDoseCalc.vmcOptions.version + case 'Carleton' + runsPath = fullfile(VMCPath, 'run'); + case 'dkfz' + runsPath = fullfile(VMCPath, 'runs'); + end + phantomPath = fullfile(runsPath, 'phantoms'); + + setenv('vmc_home',VMCPath); + setenv('vmc_dir',runsPath); + setenv('xvmc_dir',VMCPath); + + if isunix + system(['chmod a+x ' VMCPath filesep 'bin' filesep 'vmc_Linux.exe']); + end + +end + +% set consistent random seed (enables reproducibility) +rng(0); + +% get default vmc options +VmcOptions = matRad_vmcOptions(pln,ct); + +% export CT cube as binary file for vmc++ +matRad_exportCtVmc(ct, fullfile(phantomPath, 'matRad_CT.ct')); + +% take only voxels inside patient +V = [cst{:,4}]; +V = unique(vertcat(V{:})); + +writeCounter = 0; +readCounter = 0; +maxNumOfParMCSim = 0; + +% initialize waitbar +figureWait = waitbar(0,'calculate dose influence matrix for photons (vmc++)...'); +% show busy state +set(figureWait,'pointer','watch'); + +fprintf('matRad: VMC++ photon dose calculation...\n'); +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +for i = 1:dij.numOfBeams % loop over all beams + + fprintf('Beam %d of %d ...',i,dij.numOfBeams); + + % remember beam and bixel number + if calcDoseDirect + dij.beamNum(i) = i; + dij.rayNum(i) = i; + dij.bixelNum(i) = i; + end + + if strcmp(pln.propDoseCalc.vmcOptions.source,'phsp') + % set angle-specific vmc++ parameters + + % phsp starts off pointed in the +z direction, with source at -z + % phsp source gets translated, then rotated (-z, +y, -x) around + % 0, then pushed to isocenter + + % correct for the source to collimator distance and change units mm -> cm + translation = stf(i).isoCenter/10+[0 0 pln.propDoseCalc.vmcOptions.SCD + stf(i).sourcePoint_bev(2)]/10; + + % enter in isocentre + isocenter = stf(i).isoCenter/10; + + % determine vmc++ rotation angles from gantry and couch + % angles + angles = matRad_matRad2vmcSourceAngles(stf(i).gantryAngle,stf(i).couchAngle); + + % set vmc++ parameters + VmcOptions.source.translation = translation; + VmcOptions.source.isocenter = isocenter; + VmcOptions.source.angles = angles; + end + + % use beam-specific CT name + VmcOptions.geometry.XyzGeometry.CtFile = strrep(fullfile(runsPath,'phantoms','matRad_CT.ct'),'\','/'); % path of density matrix (only needed if input method is 'CT-PHANTOM') + + for j = 1:stf(i).numOfRays % loop over all rays / for photons we only have one bixel per ray! + + writeCounter = writeCounter + 1; + + % create different seeds for every bixel + VmcOptions.McControl.rngSeeds = [randi(30000),randi(30000)]; + + % remember beam and bixel number + if ~calcDoseDirect + dij.beamNum(writeCounter) = i; + dij.rayNum(writeCounter) = j; + dij.bixelNum(writeCounter) = j; + end + beamNum(writeCounter) = i; + rayNum(writeCounter) = j; + bixelNum(writeCounter) = j; + + % set ray specific vmc++ parameters + switch pln.propDoseCalc.vmcOptions.source + case 'beamlet' + % a) change coordinate system (Isocenter cs-> physical cs) and units mm -> cm + rayCorner1 = (stf(i).ray(j).rayCorners_SCD(1,:) + stf(i).isoCenter)/10; + rayCorner2 = (stf(i).ray(j).rayCorners_SCD(2,:) + stf(i).isoCenter)/10; + rayCorner3 = (stf(i).ray(j).rayCorners_SCD(3,:) + stf(i).isoCenter)/10; %vmc needs only three corners (counter-clockwise) + beamSource = (stf(i).sourcePoint + stf(i).isoCenter)/10; + + % b) swap x and y (CT-standard = [y,x,z]) + rayCorner1 = rayCorner1([2,1,3]); + rayCorner2 = rayCorner2([2,1,3]); + rayCorner3 = rayCorner3([2,1,3]); + beamSource = beamSource([2,1,3]); + + % c) set vmc++ parameters + VmcOptions.source.monoEnergy = stf(i).ray(j).energy; % photon energy + %VmcOptions.source.monoEnergy = [] ; % use photon spectrum + VmcOptions.source.beamletEdges = [rayCorner1,rayCorner2,rayCorner3]; % counter-clockwise beamlet edges + VmcOptions.source.virtualPointSourcePosition = beamSource; % virtual beam source position + + case 'phsp' + % use ray-specific file name for the phsp source (bixelized + % phsp) + VmcOptions.source.file_name = strrep(stf(i).ray(j).phspFileName,'\','/'); + end + + + %% create input file with vmc++ parameters + outfile = ['MCpencilbeam_temp_',num2str(mod(writeCounter-1,VmcOptions.run.numOfParMCSim)+1)]; + matRad_createVmcInput(VmcOptions,fullfile(runsPath, [outfile,'.vmc'])); + + % parallelization: only run this block for every numOfParallelMCSimulations!!! + if mod(writeCounter,VmcOptions.run.numOfParMCSim) == 0 || writeCounter == dij.totalNumOfBixels + + % create batch file (enables parallel processes) + if writeCounter == dij.totalNumOfBixels && mod(writeCounter,VmcOptions.run.numOfParMCSim) ~= 0 + currNumOfParMCSim = mod(writeCounter,VmcOptions.run.numOfParMCSim); + else + currNumOfParMCSim = VmcOptions.run.numOfParMCSim; + end + matRad_createVmcBatchFile(currNumOfParMCSim,fullfile(VMCPath,'run_parallel_simulations.bat'),verbose); + + % save max number of executed parallel simulations + if currNumOfParMCSim > maxNumOfParMCSim + maxNumOfParMCSim = currNumOfParMCSim; + end + + %% perform vmc++ simulation + current = pwd; + cd(VMCPath); + if verbose > 0 % only show output if verbose level > 0 + dos('run_parallel_simulations.bat'); + fprintf(['Completed ' num2str(writeCounter) ' of ' num2str(dij.totalNumOfBixels) ' beamlets...\n']); + else + [dummyOut1,dummyOut2] = dos('run_parallel_simulations.bat'); % supress output by assigning dummy output arguments + end + cd(current); + + for k = 1:currNumOfParMCSim + readCounter = readCounter+1; + + % update waitbar + waitbar(writeCounter/dij.totalNumOfBixels); + + %% import calculated dose + idx = regexp(outfile,'_'); + switch pln.propDoseCalc.vmcOptions.version + case 'Carleton' + filename = sprintf('%s%d.dos',outfile(1:idx(2)),k); + case 'dkfz' + filename = sprintf('%s%d_%s.dos',outfile(1:idx(2)),k,VmcOptions.scoringOptions.outputOptions.name); + end + [bixelDose,bixelDoseError] = matRad_readDoseVmc(fullfile(runsPath,filename),VmcOptions); + + %{ + %%% Don't do any sampling, since the correct error is + difficult to figure out. We also don't really need it on + the Graham cluster. + + if ~calcDoseDirect + % if not calculating dose directly, sample dose + + % determine cutoff + doseCutoff = VmcOptions.run.relDoseCutoff*max(bixelDose); + + % determine which voxels to sample + indSample = bixelDose < doseCutoff & bixelDose ~= 0; + r = rand(nnz(indSample),1); + + % sample them + thresRand = bixelDose(indSample)./doseCutoff; + indKeepSampled = r < thresRand; + indKeep = indSample; + indKeep(indKeep) = indKeepSampled; + + bixelDose(indKeep) = doseCutoff; + bixelDose(indSample & ~indKeep) = 0; + + end + %} + + % apply absolute calibration factor + bixelDoseError = sqrt((VmcOptions.run.absCalibrationFactorVmc.*bixelDoseError).^2+(bixelDose.*VmcOptions.run.absCalibrationFactorVmc_err).^2); + bixelDose = bixelDose*VmcOptions.run.absCalibrationFactorVmc; + + % Save dose for every bixel in cell array + doseTmpContainer{mod(readCounter-1,numOfBixelsContainer)+1,1} = sparse(V,1,bixelDose(V),dij.numOfVoxels,1); + doseTmpContainerError{mod(readCounter-1,numOfBixelsContainer)+1,1} = sparse(V,1,bixelDoseError(V),dij.numOfVoxels,1); + + % save computation time and memory by sequentially filling the + % sparse matrix dose.dij from the cell array + if mod(readCounter,numOfBixelsContainer) == 0 || readCounter == dij.totalNumOfBixels + if calcDoseDirect + if isfield(stf(beamNum(readCounter)).ray(rayNum(readCounter)),'weight') + % score physical dose + dij.physicalDose{1}(:,i) = dij.physicalDose{1}(:,i) + stf(beamNum(readCounter)).ray(rayNum(readCounter)).weight{1} * doseTmpContainer{1,1}; + dij.physicalDoseError{1}(:,i) = sqrt(dij.physicalDoseError{1}(:,i).^2 + (stf(beamNum(readCounter)).ray(rayNum(readCounter)).weight{1} * doseTmpContainerError{1,1}).^2); + else + error(['No weight available for beam ' num2str(beamNum(readCounter)) ', ray ' num2str(rayNum(readCounter))]); + end + else + % fill entire dose influence matrix + dij.physicalDose{1}(:,(ceil(readCounter/numOfBixelsContainer)-1)*numOfBixelsContainer+1:readCounter) = ... + [doseTmpContainer{1:mod(readCounter-1,numOfBixelsContainer)+1,1}]; + + dij.physicalDoseError{1}(:,(ceil(readCounter/numOfBixelsContainer)-1)*numOfBixelsContainer+1:readCounter) = ... + [doseTmpContainerError{1:mod(readCounter-1,numOfBixelsContainer)+1,1}]; + end + end + end + + end + + end + + fprintf('Done!\n'); +end + +%% delete temporary files +delete(fullfile(VMCPath, 'run_parallel_simulations.bat')); % batch file +delete(fullfile(phantomPath, 'matRad_CT.ct')); % phantom file +for j = 1:maxNumOfParMCSim + delete(fullfile(runsPath, ['MCpencilbeam_temp_',num2str(mod(j-1,VmcOptions.run.numOfParMCSim)+1),'.vmc'])); % vmc inputfile + switch pln.propDoseCalc.vmcOptions.version + case 'Carleton' + filename = sprintf('%s%d.dos','MCpencilbeam_temp_',j); + case 'dkfz' + filename = sprintf('%s%d_%s.dos','MCpencilbeam_temp_',j,VmcOptions.scoringOptions.outputOptions.name); + end + delete(fullfile(runsPath,filename)); % vmc outputfile +end + +try + % wait 0.1s for closing all waitbars + allWaitBarFigures = findall(0,'type','figure','tag','TMWWaitbar'); + delete(allWaitBarFigures); + pause(0.1); +catch +end diff --git a/matRad/doseCalc/vmc++/matRad_bixelPhspVmc.m b/matRad/doseCalc/vmc++/matRad_bixelPhspVmc.m new file mode 100644 index 000000000..251b1a853 --- /dev/null +++ b/matRad/doseCalc/vmc++/matRad_bixelPhspVmc.m @@ -0,0 +1,240 @@ +function stf = matRad_bixelPhspVmc(stf,masterRayPosBEV,vmcOptions) + +% after everything is done and working, add in code to verify if files +% already exist + + + +switch vmcOptions.version + case 'Carleton' + phspPath = fullfile(fileparts(mfilename('fullpath')), 'run', 'phsp'); + case 'dkfz' + phspPath = fullfile(fileparts(mfilename('fullpath')), 'runs', 'phsp'); +end + +fname_full = fullfile(phspPath,sprintf('%s.egsphsp1',vmcOptions.phspBaseName)); +fid_full = fopen(fname_full,'r'); + +SAD2SCD = vmcOptions.SCD./stf(1).SAD; +% in cm +bixelWidth = stf(1).bixelWidth.*SAD2SCD/10; +X = masterRayPosBEV(:,1).*SAD2SCD/10; +Y = -masterRayPosBEV(:,3).*SAD2SCD/10; % minus sign necessary since to get from BEAM coord. to DICOM, we do a rotation, NOT reflection + +% determine file names, check for existence +numBixels = size(masterRayPosBEV,1); +fname_bixels = cell(numBixels,1); +writeFiles = false; +for i = 1:numBixels + + % file name + fname_bixels{i} = fullfile(phspPath,sprintf('%s_bixelWidth%f_X%f_Y%f.egsphsp1',vmcOptions.phspBaseName,bixelWidth,X(i),Y(i))); + + if ~exist(fname_bixels{i},'file') + % if any file doesn't exist, then we want to write new phsp file + writeFiles = true; + end +end + +% give file name to ray +for i = 1:numel(stf) + + for j = 1:stf(i).numOfRays + + % find correct bixel + bixelInd = all(repelem(stf(i).ray(j).rayPos_bev,numBixels,1) == masterRayPosBEV,2); + % write filename + stf(i).ray(j).phspFileName = fname_bixels{bixelInd}; + end +end + +% FIX THIS TO GENERATE PHSP FILES FOR ALL BIXELS IN FIELD + +if writeFiles + % only do read/write files if they don't already exist + + %% extract information from full phsp file, write to bixel files + + % set up arrays for the bixel phsp files + fid_bixels = cell(numBixels,1); + header_bixels = cell(numBixels,1); + firstParticle_bixels = false(numBixels,1); + + % open header of full phsp + [fid_full, header_full] = getHeader(fid_full); + mode = char(header_full.MODE_RW(5)); + + % loop through each record in full phsp + fprintf('matRad: creating bixel phsp files... '); + for i = 1:header_full.NPPHSP + + % extract record + [fid_full, record] = getRecord(fid_full,mode); + + % sort into correct bixel + bixelInd = find(sum(abs([X Y]-repelem([record.X record.Y],numBixels,1)) < repelem(bixelWidth/2,numBixels,2),2) == 2,1,'first'); + + if ~isempty(bixelInd) + + if isempty(fid_bixels{bixelInd}) + % if not previously opened, open the file, write the header + % the header is just a dummy for now + header_bixels{bixelInd} = header_full; + % these variables will change throughout the read/write process + header_bixels{bixelInd}.NPPHSP = 0; + header_bixels{bixelInd}.NPHOTPHSP = 0; + header_bixels{bixelInd}.EKMAXPHSP = 0; + header_bixels{bixelInd}.EKMINPHSPE = 1000; + + % open file, write header + fid_bixels{bixelInd} = fopen(fname_bixels{bixelInd},'W'); % turn this to 'W'? + writeHeader(fid_bixels{bixelInd},header_bixels{bixelInd}); + end + + %% bixel header + + % increment number of particles + header_bixels{bixelInd}.NPPHSP = header_bixels{bixelInd}.NPPHSP+1; + + % modify max/min energies, increment number of photons + % must determine particle type using LATCH + LATCH = de2bi(record.LATCH,32); + if LATCH(30:31) == [0 0] + % photon + header_bixels{bixelInd}.EKMAXPHSP = max(header_bixels{bixelInd}.EKMAXPHSP,abs(record.E)); + header_bixels{bixelInd}.NPHOTPHSP = header_bixels{bixelInd}.NPHOTPHSP+1; + + elseif LATCH(30:31) == [0 1] + % electron + header_bixels{bixelInd}.EKMINPHSPE = min(header_bixels{bixelInd}.EKMINPHSPE,abs(record.E)-0.511); + header_bixels{bixelInd}.EKMAXPHSP = max(header_bixels{bixelInd}.EKMAXPHSP,abs(record.E)-0.511); + + elseif LATCH(30:31) == [1 0] + % positron + header_bixels{bixelInd}.EKMINPHSPE = min(header_bixels{bixelInd}.EKMINPHSPE,abs(record.E)-0.511); + header_bixels{bixelInd}.EKMAXPHSP = max(header_bixels{bixelInd}.EKMAXPHSP,abs(record.E)-0.511); + + elseif LATCH(30:31) == [1 1] + error('Electron and positron???') + + end + + %% bixel record + + % is this the first particle scored from a new primary history? + if record.E < 0 + % if it is, then we want ALL bixel phsp files to reflect this + % i.e., the next particle in all bixel phsp files should have + % a negative energy (first particle scored from a new primary + % history) + + firstParticle_bixels(:) = true; + end + + if firstParticle_bixels(bixelInd) + % this is the first from a new primary history + % make the energy negative + record.E = -abs(record.E); + % make sure next particle is not negative + firstParticle_bixels(bixelInd) = false; + else + % these particles should already have positive energy + if record.E < 0 + % SHOULDN'T HAPPEN + warning('NEGATIVE ENERGY') + end + end + + % write the record + writeRecord(fid_bixels{bixelInd},record,mode); + end + + % display progress + if mod(i,max(1,round(header_full.NPPHSP/200))) == 0 + matRad_progress(i/max(1,round(header_full.NPPHSP/200)),... + floor(header_full.NPPHSP/max(1,round(header_full.NPPHSP/200)))); + end + end + + + %% clean up bixel phsp headers + fprintf('matRad: updating bixel phsp file headers... '); + for i = 1:numBixels + + if header_bixels{i}.EKMINPHSPE == 1000 + header_bixels{i}.EKMINPHSPE = 0; + end + + % seek to beginning of file + fseek(fid_bixels{i},0,'bof'); + + % write updated header + writeHeader(fid_bixels{i},header_bixels{i}); + + % close file + fclose(fid_bixels{i}); + end + fprintf('Done!\n') + +end + +end + + +% read/write functions + +function [fid, header] = getHeader(fid) + +header.MODE_RW = fread(fid,5,'uint8'); +header.NPPHSP = fread(fid,1,'int32'); +header.NPHOTPHSP = fread(fid,1,'int32'); +header.EKMAXPHSP = fread(fid,1,'float32'); +header.EKMINPHSPE = fread(fid,1,'float32'); +header.NINCPHSP = fread(fid,1,'float32'); +header.garbage = fread(fid,3,'int8'); + +end + +function [fid, record] = getRecord(fid,mode) + +record.LATCH = fread(fid,1,'uint32'); +record.E = fread(fid,1,'float32'); +record.X = fread(fid,1,'float32'); +record.Y = fread(fid,1,'float32'); +record.U = fread(fid,1,'float32'); +record.V = fread(fid,1,'float32'); +record.WT = fread(fid,1,'float32'); + +if mode == 2 + record.ZLAST = fread(fid,1,'float32'); +end + +end + +function writeHeader(fid,header) + +fwrite(fid,header.MODE_RW,'uint8'); +fwrite(fid,header.NPPHSP,'int32'); +fwrite(fid,header.NPHOTPHSP,'int32'); +fwrite(fid,header.EKMAXPHSP,'float32'); +fwrite(fid,header.EKMINPHSPE,'float32'); +fwrite(fid,header.NINCPHSP,'float32'); +fwrite(fid,header.garbage,'int8'); + +end + +function writeRecord(fid,record,mode) + +fwrite(fid,record.LATCH,'uint32'); +fwrite(fid,record.E,'float32'); +fwrite(fid,record.X,'float32'); +fwrite(fid,record.Y,'float32'); +fwrite(fid,record.U,'float32'); +fwrite(fid,record.V,'float32'); +fwrite(fid,record.WT,'float32'); + +if mode == 2 + fwrite(fid,record.ZLAST,'float32'); +end + +end \ No newline at end of file diff --git a/matRad/doseCalc/vmc++/matRad_createVmcBatchFile.m b/matRad/doseCalc/vmc++/matRad_createVmcBatchFile.m new file mode 100644 index 000000000..4ead8a258 --- /dev/null +++ b/matRad/doseCalc/vmc++/matRad_createVmcBatchFile.m @@ -0,0 +1,74 @@ +function matRad_createVmcBatchFile(parallelSimulations,filepath,verboseLevel) +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% matRad batchfile creation +% +% call +% matRad_createVmcBatchFile(parallelSimulations,filepath,verboseLevel) +% +% input +% parallelSimulations: no of parallel simulations +% filepath: path where batchfile is created (this has to be the +% path of the vmc++ folder) +% verboseLevel: optional. number specifying the amount of output +% printed to the command prompt +% +% +% References +% +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% set verbose level +if nargin < 3 + verboseString = '/B'; % to not open terminals by default +else + if verboseLevel > 1 % open terminals is verboseLevel > 1 + verboseString = ''; + else + verboseString = '/B'; + end +end + +if ispc % parallelization only possible on windows systems + + parallelProcesses = cell(1,parallelSimulations); + for i = 1:parallelSimulations + parallelProcesses{1,i} = ['start "" 9>"%lock%',num2str(i),'" ' verboseString ' .\bin\vmc_Windows.exe -i MCpencilbeam_temp_',num2str(i)]; + end + + batchFile = {... + ['@echo off'],... + ['setlocal'],... + ['set "lock=%temp%\wait%random%.lock"'],... + [''],... + parallelProcesses{:},... + [''],... + [':Wait for all processes to finish (wait until lock files are no longer locked)'],... + ['1>nul 2>nul ping /n 2 ::1'],... + ['for %%N in (',strjoin(arrayfun(@(x) num2str(x),(1:parallelSimulations),'UniformOutput',false),' '),') do ('],... + [' (call ) 9>"%lock%%%N" || goto :Wait'],... + [') 2>nul'],... + [''],... + ['del "%lock%*"'],... + ['']... + %,['echo Done - ready to continue processing'] + }; + +elseif isunix + + batchFile = {'./bin/vmc_Linux.exe MCpencilbeam_temp_1'}; + +end + +% write batch file +fid = fopen(filepath,'wt'); +for i = 1 : length(batchFile) + fprintf(fid,'%s\n',batchFile{i}); +end +fclose(fid); + +if isunix + system(['chmod a+x ' filepath]); +end + +end diff --git a/matRad/doseCalc/vmc++/matRad_createVmcInput.m b/matRad/doseCalc/vmc++/matRad_createVmcInput.m new file mode 100644 index 000000000..99b03058e --- /dev/null +++ b/matRad/doseCalc/vmc++/matRad_createVmcInput.m @@ -0,0 +1,153 @@ +function matRad_createVmcInput(VmcOptions,filename) +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% matRad vmc++ inputfile creation +% +% call +% matRad_createVmcInput(VmcOptions,filename) +% +% input +% VmcOptions: structure set with VMC options +% filename: full file name of generated vmc input file (has to be +% located in the runs path in the vmc++ folder) +% +% +% References +% +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + +% define a cell array which is bigger than necessary +% some parts of the input (e.g., the source) are variable +% then delete the empty elements +VmcInput = cell(100,1); +offset = 0; + +% define the scoring options +VmcInput(offset+(1:11)) = {... + [' :start scoring options: ' ] ,... + [' start in geometry: ' VmcOptions.scoringOptions.startInGeometry ] ,... + [' :start dose options: ' ] ,... + [' score in geometries: ' VmcOptions.scoringOptions.doseOptions.scoreInGeometries ] ,... + [' score dose to water: ' VmcOptions.scoringOptions.doseOptions.scoreDoseToWater ] ,... + [' :stop dose options: ' ] ,... + [' :start output options ' VmcOptions.scoringOptions.outputOptions.name ':' ] ,... + [' dump dose: ' num2str(VmcOptions.scoringOptions.outputOptions.dumpDose) ] ,... + [' :stop output options ' VmcOptions.scoringOptions.outputOptions.name ':' ] ,... + [' :stop scoring options: ' ] ,... + [' ' ] ... +}; +offset = offset+11; + +% define the geometry +VmcInput(offset+(1:8)) = {... + [' :start geometry: ' ] ,... + [' :start XYZ geometry: ' ] ,... + [' my name = ' VmcOptions.geometry.XyzGeometry.Ct ] ,... + [' method of input = ' VmcOptions.geometry.XyzGeometry.methodOfInput ] ,... + [' phantom file = ' VmcOptions.geometry.XyzGeometry.CtFile ] ,... + [' :stop XYZ geometry: ' ] ,... + [' :stop geometry: ' ] ,... + [' ' ] ... +}; +offset = offset+8; + +% define the source +if strcmp(VmcOptions.source.type,'beamlet') + + VmcInput(offset+(1:3)) = {... + [' :start beamlet source: ' ] ,... + [' my name = ' VmcOptions.source.myName ] ,... + [' monitor units ' VmcOptions.source.myName ' = ' num2str(VmcOptions.source.monitorUnits) ] ... + }; + offset = offset+3; + + if ~isempty(VmcOptions.source.monoEnergy) && VmcOptions.source.monoEnergy>0 + VmcInput(offset+1) = {... + [' mono energy = ' num2str(VmcOptions.source.monoEnergy) ] ... + }; + offset = offset+1; + end + + VmcInput(offset+(1:6)) = {... + [' spectrum = ' VmcOptions.source.spectrum ] ,... + [' charge = ' num2str(VmcOptions.source.charge) ] ,... + [' beamlet edges = ' num2str(VmcOptions.source.beamletEdges, '%8.5f ') ] ,... + [' virtual point source position = ' num2str(VmcOptions.source.virtualPointSourcePosition, '%8.5f ') ] ,... + [' :stop beamlet source: ' ] ,... + [' ' ] ... + }; + offset = offset+6; + +elseif strcmp(VmcOptions.source.type,'phsp') + + VmcInput(offset+(1:12)) = {... + [' :start general source: ' ] ,... + [' monitor units ' VmcOptions.source.myName ' = ' num2str(VmcOptions.source.monitorUnits) ] ,... + [' translation ' VmcOptions.source.myName ' = ' num2str(VmcOptions.source.translation) ] ,... + [' isocenter ' VmcOptions.source.myName ' = ' num2str(VmcOptions.source.isocenter) ] ,... + [' angles ' VmcOptions.source.myName ' = ' num2str(VmcOptions.source.angles) ] ,... + [' :start phsp source: ' ] ,... + [' my name = ' VmcOptions.source.myName ] ,... + [' file name = ' VmcOptions.source.file_name ] ,... + [' particle type = ' num2str(VmcOptions.source.particleType) ] ,... + [' :stop phsp source: ' ] ,... + [' :stop general source: ' ] ,... + [' ' ] ... + }; +offset = offset+12; +end + +% define the MC parameters +VmcInput(offset+(1:5)) = {... + [' :start MC Parameter: ' ] ,... + [' automatic parameter = ' VmcOptions.McParameter.automatic_parameter ] ,... + [' spin = ' num2str(VmcOptions.McParameter.spin) ] ,... + [' :stop MC Parameter: ' ] ,... + [' ' ] ... + }; +offset = offset+5; + +% define the MC control +VmcInput(offset+(1:6)) = {... + [' :start MC Control: ' ] ,... + [' ncase = ' num2str(VmcOptions.McControl.ncase) ] ,... + [' nbatch = ' num2str(VmcOptions.McControl.nbatch) ] ,... + [' rng seeds = ' num2str(VmcOptions.McControl.rngSeeds) ] ,... + [' :stop MC Control: ' ] ,... + [' ' ] ... +}; +offset = offset+6; + +% define the variance reduction +VmcInput(offset+(1:6)) = {... + [' :start variance reduction: ' ] ,... + [' repeat history = ' num2str(VmcOptions.varianceReduction.repeatHistory) ] ,... + [' split photons = ' num2str(VmcOptions.varianceReduction.splitPhotons) ] ,... + [' photon split factor = ' num2str(VmcOptions.varianceReduction.photonSplitFactor) ] ,... + [' :stop variance reduction: ' ] ,... + [' ' ] ... +}; +offset = offset+6; + +% define the quasi +VmcInput(offset+(1:5)) = {... + [' :start quasi: ' ] ,... + [' base = ' num2str(VmcOptions.quasi.base) ] ,... + [' dimension = ' num2str(VmcOptions.quasi.dimension) ] ,... + [' skip = ' num2str(VmcOptions.quasi.skip) ] ,... + [' :stop quasi: ' ] ... +}; +offset = offset+5; + +% delete empty elements +VmcInput((offset+1):end) = []; + +% write input file +fid = fopen(filename,'wt'); +for i = 1 : length(VmcInput) + fprintf(fid,'%s\n',VmcInput{i}); +end +fclose(fid); + +end \ No newline at end of file diff --git a/matRad/doseCalc/vmc++/matRad_exportCtVmc.m b/matRad/doseCalc/vmc++/matRad_exportCtVmc.m new file mode 100644 index 000000000..01b4e3830 --- /dev/null +++ b/matRad/doseCalc/vmc++/matRad_exportCtVmc.m @@ -0,0 +1,39 @@ +function matRad_exportCtVmc(ct,filename) +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% matRad binary CT export for vmc++ +% +% call +% matRad_exportCtVmc(ct,filename) +% +% input +% ct: matRad ct struct +% filename: path where CTfile is created +% +% +% References +% +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +fid = fopen(filename,'w'); + +% write ct dimensions +fwrite(fid,ct.cubeDim([2 1 3]),'int32'); + +% write voxel corner location in cm in physical cs with ct cube corner at [.5 .5 .5] +X = (0.5:(ct.cubeDim(2)+0.5))*ct.resolution.x/10; +Y = (0.5:(ct.cubeDim(1)+0.5))*ct.resolution.y/10; +Z = (0.5:(ct.cubeDim(3)+0.5))*ct.resolution.z/10; + +fwrite(fid,X,'float32'); +fwrite(fid,Y,'float32'); +fwrite(fid,Z,'float32'); + +% write voxel densities +% first permute indices y <-> x +ctVMC = permute(ct.cube{1},[2 1 3]); +% then reshape into single column +ctVMC = reshape(ctVMC,[],1); +fwrite(fid,ctVMC,'float32'); + +fclose(fid); diff --git a/matRad/doseCalc/vmc++/matRad_matRad2vmcSourceAngles.m b/matRad/doseCalc/vmc++/matRad_matRad2vmcSourceAngles.m new file mode 100644 index 000000000..3c1614ab1 --- /dev/null +++ b/matRad/doseCalc/vmc++/matRad_matRad2vmcSourceAngles.m @@ -0,0 +1,100 @@ +function angles = matRad_matRad2vmcSourceAngles(gantryAngle,couchAngle) +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% matRad convert gantry and couch angles to angles used by vmc++ +% +% call +% matRad_matRad2mvcSourceAngles(gantryAngle,couchAngle) +% +% input +% gantryAngle: gantry angle +% couchAngle: couch angle +% +% +% References +% Notes 25 July 2018 +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + +switch -cosd(couchAngle).*sind(gantryAngle) + % do special cases first, when cosd(thetaY) == 0 + % -cosd(couchAngle)*sind(gantryAngle) = sind(thetaY) + + case 1 + thetaY = 90; + thetaZ = 0; + + if couchAngle == 0 + % then gantryAngle == 270 + thetaX = 90; + else + % then couchAngle == 180, gantryAngle == 90 + thetaX = 270; + end + + case -1 + thetaY = 270; + thetaZ = 0; + if couchAngle == 0 + % then gantryAngle == 90 + thetaX = 90; + else + % then couchAngle == 180, gantryAngle == 270 + thetaX = 270; + end + + otherwise + % general case, cosd(thetaY) ~= 0 + + % first determine thetaY; note that we may have to take + % supplementary angle later + thetaY = asind(-cosd(couchAngle).*sind(gantryAngle)); + + % now determine thetaX and thetaZ from the x and y components + thetaX = atan2d(cosd(gantryAngle)./cosd(thetaY),sind(couchAngle).*sind(gantryAngle)./cosd(thetaY)); + thetaZ = atan2d(-sind(couchAngle)./cosd(thetaY),cosd(couchAngle).*cosd(gantryAngle)./cosd(thetaY)); + + % verify that the remaining angular relations are satisfied + % if not, take thetaY = 180-thetaY and recalculate thetaX and + % thetaZ + if ~verifiedRelations(gantryAngle,couchAngle,thetaX,thetaY,thetaZ) + thetaY = 180-thetaY; + thetaX = atan2d(cosd(gantryAngle)./cosd(thetaY),sind(couchAngle).*sind(gantryAngle)./cosd(thetaY)); + thetaZ = atan2d(-sind(couchAngle)./cosd(thetaY),cosd(couchAngle).*cosd(gantryAngle)./cosd(thetaY)); + end + +end + +% now verify for a final time that the remaining angular relations are satisfied +if ~verifiedRelations(gantryAngle,couchAngle,thetaX,thetaY,thetaZ) + error('Angular relations are not satisfied for some reason'); +end + +angles = [thetaX thetaY thetaZ]; + +end + + +function ver = verifiedRelations(gantryAngle,couchAngle,thetaX,thetaY,thetaZ) + +ver = true; + +% need to verify four relations +% if any of them fail, then fail the test +if abs(sind(gantryAngle) + sind(thetaX).*sind(thetaY).*cosd(thetaZ) + cosd(thetaX).*sind(thetaZ)) > eps*10e3 + ver = false; +end + +if abs(-sind(couchAngle).*cosd(gantryAngle) + cosd(thetaX).*sind(thetaY).*cosd(thetaZ) - sind(thetaX).*sind(thetaZ)) > eps*10e3 + ver = false; +end + +if abs(sind(thetaX).*sind(thetaY).*sind(thetaZ)-cosd(thetaX).*cosd(thetaZ)) > eps*10e3 + ver = false; +end + +if abs(-cosd(couchAngle)+cosd(thetaX).*sind(thetaY).*sind(thetaZ)+sind(thetaX).*cosd(thetaZ)) > eps*10e3 + ver = false; +end + +end \ No newline at end of file diff --git a/matRad/doseCalc/vmc++/matRad_readDoseVmc.m b/matRad/doseCalc/vmc++/matRad_readDoseVmc.m new file mode 100644 index 000000000..e47ce5364 --- /dev/null +++ b/matRad/doseCalc/vmc++/matRad_readDoseVmc.m @@ -0,0 +1,60 @@ +function [bixelDose,bixelDoseError] = matRad_readDoseVmc(filename,VmcOptions) +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% matRad binary dose import from vmc++ +% +% call +% [bixelDose,bixelDoseError] = matRad_readDoseVmc(filename) +% +% input +% filename: path of input file +% +% output +% bixelDose = vector of imported dose values, [D] = 10^-(10) Gy cm^2 +% bixelDoseError = vector of imported dose errors, [deltaD] = 10^-(10) Gy cm^2 +% +% +% References +% +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +fid = fopen(filename,'r'); + +% read header (no regions, no histories, no batches, no beamlets, format specifier (dump_dose)) +switch VmcOptions.run.version + case 'Carleton' + Header = fread(fid,1,'int32'); + no_regions = Header(1); + dump_dose = VmcOptions.scoringOptions.outputOptions.dumpDose; + case 'dkfz' + Header = fread(fid,5,'int32'); + no_regions = Header(1); + dump_dose = Header(5); +end + +% read dose array +if dump_dose == 2 + dmax = fread(fid, 1, 'double'); + bixelDose = fread(fid, no_regions, 'uint16'); + bixelDose = bixelDose/65534*dmax; % conversion short integers to floating numbers + bixelDoseError = zeros(size(bixelDose)); +elseif dump_dose == 1 + bixelDose = fread(fid, no_regions, 'float32'); + bixelDoseError = fread(fid, no_regions, 'float32'); +end +fclose(fid); + +% reshape into array, permute y <-> x, reshape back into column +bixelDose = reshape(bixelDose,VmcOptions.geometry.dimensions([2 1 3])); +bixelDose = permute(bixelDose,[2 1 3]); +bixelDose = reshape(bixelDose,[],1); + +bixelDoseError = reshape(bixelDoseError,VmcOptions.geometry.dimensions([2 1 3])); +bixelDoseError = permute(bixelDoseError,[2 1 3]); +bixelDoseError = reshape(bixelDoseError,[],1); + +end + + + + diff --git a/matRad/doseCalc/vmc++/matRad_vmcOptions.m b/matRad/doseCalc/vmc++/matRad_vmcOptions.m new file mode 100644 index 000000000..7e2c730c6 --- /dev/null +++ b/matRad/doseCalc/vmc++/matRad_vmcOptions.m @@ -0,0 +1,106 @@ +function VmcOptions = matRad_vmcOptions(pln,ct) + +%% run options + +% number of paralle MC simulations +if isfield(pln.propDoseCalc.vmcOptions,'numOfParMCSim') + VmcOptions.run.numOfParMCSim = pln.propDoseCalc.vmcOptions.numOfParMCSim; +else + VmcOptions.run.numOfParMCSim = 4; +end +if isunix && VmcOptions.run.numOfParMCSim > 1 + VmcOptions.run.numOfParMCSim = 1; +end + +% number of histories per bixel +if isfield(pln.propDoseCalc.vmcOptions,'nCasePerBixel') + VmcOptions.run.nCasePerBixel = pln.propDoseCalc.vmcOptions.nCasePerBixel; +else + VmcOptions.run.nCasePerBixel = 5000; +end + +% relative dose cutoff +VmcOptions.run.relDoseCutoff = 10^(-3); + +% version (Carleton, dkfz, etc.) +VmcOptions.run.version = pln.propDoseCalc.vmcOptions.version; + +% set absolute calibration factor +% CALCULATION +% absolute_calibration_factor = 1/D(depth = 100,5mm) -> D(depth = 100,5mm) = 1Gy +% SETUP +% SAD = 1000mm, SCD = 500mm, bixelWidth = 5mm, IC = [240mm,240mm,240mm] +% fieldsize@IC = 105mm x 105mm, phantomsize = 81 x 81 x 81 = 243mm x 243mm x 243mm +% rel_Dose_cutoff = 10^(-3), ncase = 500000/bixel +switch pln.propDoseCalc.vmcOptions.version + case 'Carleton' + switch pln.propDoseCalc.vmcOptions.source + case 'phsp' + + d_50mm = 9.351001892810018e-07; + d_50mm_error = 7.668474434354598e-09; + + VmcOptions.run.absCalibrationFactorVmc = 1./d_50mm; + VmcOptions.run.absCalibrationFactorVmc_err = d_50mm_error./(d_50mm.^2); + end + case 'dkfz' + VmcOptions.run.absCalibrationFactorVmc = 99.818252282632300; +end + +%% source + +VmcOptions.source.myName = 'some_source'; % name of source +VmcOptions.source.monitorUnits = 1; +switch pln.propDoseCalc.vmcOptions.source + case 'beamlet' + VmcOptions.source.spectrum = fullfile(runsPath,'spectra','var_6MV.spectrum'); % energy spectrum source (only used if no mono-Energy given) + VmcOptions.source.charge = 0; % charge (-1,0,1) + VmcOptions.source.type = 'beamlet'; + + case 'phsp' + VmcOptions.source.particleType = 2; + VmcOptions.source.type = 'phsp'; +end + +%% transport parameters + +VmcOptions.McParameter.automatic_parameter = 'yes'; % if yes, automatic transport parameters are used +VmcOptions.McParameter.spin = 0; % 0: spin effects ignored; 1: simplistic; 2: full treatment + +%% MC control + +VmcOptions.McControl.ncase = VmcOptions.run.nCasePerBixel; % number of histories +VmcOptions.McControl.nbatch = 10; % number of batches + +%% variance reduction + +VmcOptions.varianceReduction.repeatHistory = 0.041; +VmcOptions.varianceReduction.splitPhotons = 1; +VmcOptions.varianceReduction.photonSplitFactor = -80; + +%% quasi random numbers + +VmcOptions.quasi.base = 2; +VmcOptions.quasi.dimension = 60; +VmcOptions.quasi.skip = 1; + +%% geometry +switch pln.propDoseCalc.vmcOptions.version + case 'Carleton' + VmcOptions.geometry.XyzGeometry.methodOfInput = 'MMC-PHANTOM'; % input method ('CT-PHANTOM', 'individual', 'groups') + case 'dkfz' + VmcOptions.geometry.XyzGeometry.methodOfInput = 'CT-PHANTOM'; % input method ('CT-PHANTOM', 'individual', 'groups') +end +VmcOptions.geometry.dimensions = ct.cubeDim; +VmcOptions.geometry.XyzGeometry.Ct = 'CT'; % name of geometry + +%% scoring manager +VmcOptions.scoringOptions.startInGeometry = 'CT'; % geometry in which partciles start their transport +VmcOptions.scoringOptions.doseOptions.scoreInGeometries = 'CT'; % geometry in which dose is recorded +VmcOptions.scoringOptions.doseOptions.scoreDoseToWater = 'yes'; % if yes output is dose to water +VmcOptions.scoringOptions.outputOptions.name = 'CT'; % geometry for which dose output is created (geometry has to be scored) +VmcOptions.scoringOptions.outputOptions.dumpDose = pln.propDoseCalc.vmcOptions.dumpDose; % output format (1: format=float, Dose + deltaDose; 2: format=short int, Dose) + + + +end \ No newline at end of file diff --git a/matRad/gui/widgets/matRad_PlanWidget.m b/matRad/gui/widgets/matRad_PlanWidget.m index e0b3a2a20..1c71f12f8 100644 --- a/matRad/gui/widgets/matRad_PlanWidget.m +++ b/matRad/gui/widgets/matRad_PlanWidget.m @@ -821,6 +821,10 @@ set(handles.popUpMenuDoseEngine,'String',{availableEngines(:).shortName}); selectedEngineIx = get(handles.popUpMenuDoseEngine,'Value'); selectedEngine = availableEngines(selectedEngineIx); + + if ~isfield(pln,'propStf') || ~isfield(pln.propStf,'numOfBeams') + pln.propStf.numOfBeams = numel(stfGen.gantryAngles); + end if isfield(pln.propStf,'isoCenter') % sanity check of isoCenter diff --git a/matRad/gui/widgets/matRad_ViewingWidget.m b/matRad/gui/widgets/matRad_ViewingWidget.m index 2f8920e94..9e37be7a5 100644 --- a/matRad/gui/widgets/matRad_ViewingWidget.m +++ b/matRad/gui/widgets/matRad_ViewingWidget.m @@ -1150,7 +1150,18 @@ function initValues(this) if isfield(pln,'propStf') && isfield(pln.propStf,'isoCenter') isoCoordinates = matRad_world2cubeIndex(pln.propStf.isoCenter(1,:), ct); planeCenters = ceil(isoCoordinates); - this.numOfBeams=pln.propStf.numOfBeams; + if ~isfield(pln.propStf,'numOfBeams') + if evalin('base','exist(''stf'')') + stf = evalin('base','stf'); + this.numOfBeams = numel(stf); + elseif isfield(pln.propStf,'gantryAngles') + this.numOfBeams = numel(pln.propStf.gantryAngles); + else + this.numOfBeams = 1; + end + else + this.numOfBeams = 1; + end end end diff --git a/matRad/matRad_calcDeliveryMetrics.m b/matRad/matRad_calcDeliveryMetrics.m new file mode 100644 index 000000000..851a0ef6e --- /dev/null +++ b/matRad/matRad_calcDeliveryMetrics.m @@ -0,0 +1,186 @@ +function result = matRad_calcDeliveryMetrics(result,pln,stf) +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% matRad delivery metric calculation +% +% call +% matRad_calcDeliveryMetrics(result,pln) +% +% input +% result: result struct from fluence optimization/sequencing +% pln: matRad plan meta information struct +% +% output +% All plans: total MU +% VMAT plans: total time, leaf speed, MU rate, and gantry rotation speed +% distributions +% +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2016 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSES.txt. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +apertureInfo = result.apertureInfo; + +l = 0; +if pln.propOpt.runVMAT + + machine = matRad_loadMachine(pln); + + apertureInfo.planMU = 0; + apertureInfo.planTime = 0; + + %All of these are vectors + %Each entry corresponds to a beam angle + %Later, we will convert these to histograms, find max, mean, min, etc. + gantryRot = zeros(1,result.apertureInfo.totalNumOfShapes); + MURate = gantryRot; + times = gantryRot; + angles = gantryRot; + maxLeafSpeed = gantryRot; + + for i = 1:size(apertureInfo.beam,2) + + apertureInfo.planMU = apertureInfo.planMU+apertureInfo.beam(i).shape(1).MU; + apertureInfo.planTime = apertureInfo.planTime+apertureInfo.beam(i).time; %time until next optimized beam + + if apertureInfo.beam(i).numOfShapes %only optimized beams have their time in the data struct + l = l+1; + gantryRot(l) = apertureInfo.beam(i).gantryRot; + MURate(l) = apertureInfo.beam(i).shape(1).MURate*60; + times(l) = apertureInfo.beam(i).time; + maxLeafSpeed(l) = apertureInfo.beam(i).maxLeafSpeed/10; + angles(l) = apertureInfo.beam(i).gantryAngle; + end + end + + + apertureInfoVec = apertureInfo.apertureVector; + if pln.propStf.continuousAperture + leftLeafPos = apertureInfoVec([1:apertureInfo.totalNumOfLeafPairs]+apertureInfo.totalNumOfShapes); + rightLeafPos = apertureInfoVec(1+apertureInfo.totalNumOfLeafPairs+apertureInfo.totalNumOfShapes:apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2); + + timeOptBorderAngles = apertureInfoVec((1+apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2):end); + timeDoseBorderAngles = timeOptBorderAngles.*[apertureInfo.propVMAT.beam([apertureInfo.propVMAT.beam.DAOBeam]).timeFacCurr]'; + + leftLeafDiff = diff(reshape(leftLeafPos,apertureInfo.beam(1).numOfActiveLeafPairs,[]),1,2); + rightLeafDiff = diff(reshape(rightLeafPos,apertureInfo.beam(1).numOfActiveLeafPairs,[]),1,2); + + leftLeafDiff = reshape(leftLeafDiff(repmat([apertureInfo.propVMAT.beam.DAOBeam],apertureInfo.beam(1).numOfActiveLeafPairs,1)),apertureInfo.beam(1).numOfActiveLeafPairs,apertureInfo.totalNumOfShapes); + rightLeafDiff = reshape(rightLeafDiff(repmat([apertureInfo.propVMAT.beam.DAOBeam],apertureInfo.beam(1).numOfActiveLeafPairs,1)),apertureInfo.beam(1).numOfActiveLeafPairs,apertureInfo.totalNumOfShapes); + + lfspd = reshape([leftLeafDiff rightLeafDiff]./ ... + repmat(timeDoseBorderAngles',apertureInfo.beam(1).numOfActiveLeafPairs,2),2*apertureInfo.beam(1).numOfActiveLeafPairs*numel(timeDoseBorderAngles),1); + + optAngles = [apertureInfo.beam([apertureInfo.propVMAT.beam.DAOBeam]).gantryAngle]; + optAnglesMat = reshape(repmat(optAngles,apertureInfo.beam(1).numOfActiveLeafPairs,2),2*apertureInfo.beam(1).numOfActiveLeafPairs*numel(timeDoseBorderAngles),1); + else + leftLeafPos = apertureInfoVec([1:apertureInfo.totalNumOfLeafPairs]+apertureInfo.totalNumOfShapes); + rightLeafPos = apertureInfoVec(1+apertureInfo.totalNumOfLeafPairs+apertureInfo.totalNumOfShapes:apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2); + + optInd = [apertureInfo.propVMAT.beam.DAOBeam]; + timeOptBorderAngles = apertureInfoVec((1+apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2):end); + + i = repelem(1:(apertureInfo.totalNumOfShapes-1),2); + j = repelem(1:(apertureInfo.totalNumOfShapes),2); + j(1) = []; + j(end) = []; + + timeFac = [apertureInfo.propVMAT.beam(optInd).timeFac]'; + timeFac(1) = []; + timeFac(end) = []; + %timeFac(timeFac == 0) = []; + + timeFacMatrix = sparse(i,j,timeFac,(apertureInfo.totalNumOfShapes-1),apertureInfo.totalNumOfShapes); + timeBNOptAngles = timeFacMatrix*timeOptBorderAngles; + + lfspd = reshape([abs(diff(reshape(leftLeafPos,apertureInfo.beam(1).numOfActiveLeafPairs,apertureInfo.totalNumOfShapes),1,2)) ... + abs(diff(reshape(rightLeafPos,apertureInfo.beam(1).numOfActiveLeafPairs,apertureInfo.totalNumOfShapes),1,2))]./ ... + repmat(timeBNOptAngles',apertureInfo.beam(1).numOfActiveLeafPairs,2),2*apertureInfo.beam(1).numOfActiveLeafPairs*numel(timeBNOptAngles),1); + end + + if pln.propStf.continuousAperture + + %FMOBorders = zeros(1,2*numel(pln.propStf.FMOGantryAngles)); + counter = 1; + for i = 1:numel(stf) + if stf(i).propVMAT.FMOBeam + FMOBorders(counter) = stf(i).propVMAT.FMOAngleBorders(1); + FMOBorders(counter+1) = stf(i).propVMAT.FMOAngleBorders(2); + counter = counter+2; + else + continue + end + end + FMOBorders = unique(FMOBorders); + forwardDir = 1-2*mod(1:(numel(FMOBorders)-1),2); + numForward = zeros(numel(forwardDir),1); + numBackward = zeros(numel(forwardDir),1); + timeInInit = zeros(numel(forwardDir),1); + + plot(optAnglesMat,lfspd,'.') + hold on + counter = 1; + for border = FMOBorders + plot([border border],[-machine.constraints.leafSpeed(2) machine.constraints.leafSpeed(2)],'r-') + + if border < FMOBorders(end) + curr_lfspd = lfspd(FMOBorders(counter) <= optAnglesMat & optAnglesMat <= FMOBorders(counter+1)); + + numForward(counter) = nnz(curr_lfspd*forwardDir(counter) >= 0); + numBackward(counter) = nnz(curr_lfspd*forwardDir(counter) < 0); + timeInInit(counter) = sum(times(FMOBorders(counter) <= angles & angles <= FMOBorders(counter+1))); + + counter = counter+1; + end + end + + figure + plot([min(FMOBorders)-5 max(FMOBorders)+5],[0 0],'k--') + xlim([min(FMOBorders)-5 max(FMOBorders)+5]) + ylim([-machine.constraints.leafSpeed(2)-5 machine.constraints.leafSpeed(2)+5]) + xlabel('gantry angle (^\circ)') + ylabel('leaf speed (cm/s)') + + figure + plot(optAngles,gantryRot,'.') + xlim([min(FMOBorders)-5 max(FMOBorders)+5]) + ylim([0 machine.constraints.gantryRotationSpeed(2)+1]) + xlabel('gantry angle (^\circ)') + ylabel('gantry rotation speed (^\circ/s)') + + figure + plot(optAngles,MURate,'.') + xlim([min(FMOBorders)-5 max(FMOBorders)+5]) + ylim([0 60*machine.constraints.monitorUnitRate(2)+5]) + xlabel('gantry angle (^\circ)') + ylabel('MU rate (MU/min)') + + apertureInfo.fracMaxMURate = sum(times(MURate > 60*machine.constraints.monitorUnitRate(2)*(1-1e-5)))./sum(times); + apertureInfo.fracMinMURate = sum(times(MURate < 60*machine.constraints.monitorUnitRate(1)*(1+1e-5)))./sum(times); + apertureInfo.fracMaxGantryRot = sum(times(gantryRot > machine.constraints.gantryRotationSpeed(2)*(1-1e-5)))./sum(times); + apertureInfo.fracMaxLeafSpeed = sum(times(maxLeafSpeed > machine.constraints.leafSpeed(2)/10*(1-1e-5)))./sum(times); + apertureInfo.fracHalfMaxLeafSpeed = sum(times(maxLeafSpeed > machine.constraints.leafSpeed(2)/10*(1-1e-5)/2))./sum(times); + + apertureInfo.fracForward = numForward./(numForward+numBackward); + apertureInfo.fracBackward = 1-apertureInfo.fracForward; + apertureInfo.totalFracForward = mean(apertureInfo.fracForward); + %apertureInfo.totalFracForward = sum(apertureInfo.fracForward.*timeInInit)./sum(timeInInit); + apertureInfo.totalFracBackward = 1-apertureInfo.totalFracForward; + end + %} + +end + +result.apertureInfo = apertureInfo; + diff --git a/matRad/matRad_directApertureOptimization.m b/matRad/matRad_directApertureOptimization.m index 1e909924f..aa4616eb5 100644 --- a/matRad/matRad_directApertureOptimization.m +++ b/matRad/matRad_directApertureOptimization.m @@ -1,4 +1,4 @@ -function [optResult,optimizer] = matRad_directApertureOptimization(dij,cst,apertureInfo,optResult,pln) +function [resultGUI,optimizer] = matRad_directApertureOptimization(dij,cst,apertureInfo,resultGUI,pln) % matRad function to run direct aperture optimization % % call @@ -83,12 +83,46 @@ options.model = pln.bioModel.model; % update aperture info vector -apertureInfo = matRad_OptimizationProblemDAO.matRad_daoVec2ApertureInfo(apertureInfo,apertureInfo.apertureVector); +if isfield(apertureInfo,'scaleFacRx') + %weights were scaled to acheive 95% PTV coverage + %scale back to "optimal" weights + apertureInfo.apertureVector(1:apertureInfo.totalNumOfShapes) = apertureInfo.apertureVector(1:apertureInfo.totalNumOfShapes)/apertureInfo.scaleFacRx; +end + +if ~isfield(pln.propOpt,'preconditioner') + pln.propOpt.preconditioner = false; +end + +if ~isfield(pln.propOpt,'runVMAT') + pln.propOpt.runVMAT = false; +end + +if pln.propOpt.preconditioner + %rescale dij matrix, so that apertureWeight/bixelWidth ~= 1 + % gradient wrt weights ~ 1, gradient wrt leaf pos + % ~ apertureWeight/(bixelWidth) ~1 + + % need to get the actual weights, so use the jacobiScale vector to + % convert from the variables + dij.scaleFactor = mean(apertureInfo.apertureVector(1:apertureInfo.totalNumOfShapes)./apertureInfo.jacobiScale)/(apertureInfo.bixelWidth); + + dij.weightToMU = dij.weightToMU*dij.scaleFactor; + apertureInfo.weightToMU = apertureInfo.weightToMU*dij.scaleFactor; + apertureInfo.apertureVector(1:apertureInfo.totalNumOfShapes) = apertureInfo.apertureVector(1:apertureInfo.totalNumOfShapes)/dij.scaleFactor; +end %Use Dose Projection only backProjection = matRad_DoseProjection(); -optiProb = matRad_OptimizationProblemDAO(backProjection,apertureInfo); +if pln.propOpt.runVMAT + apertureInfo = matRad_OptimizationProblemVMAT.matRad_daoVec2ApertureInfo(apertureInfo,apertureInfo.apertureVector); + apertureInfo.newIteration = true; %do we need this? + optiProb = matRad_OptimizationProblemVMAT(backProjection,apertureInfo); +else + apertureInfo = matRad_OptimizationProblemDAO.matRad_daoVec2ApertureInfo(apertureInfo,apertureInfo.apertureVector); + apertureInfo.newIteration = true; %do we need this? + optiProb = matRad_OptimizationProblemDAO(backProjection,apertureInfo); +end if ~isfield(pln.propOpt,'optimizer') pln.propOpt.optimizer = 'IPOPT'; @@ -106,15 +140,59 @@ % Run IPOPT. optimizer = optimizer.optimize(apertureInfo.apertureVector,optiProb,dij,cst); -wOpt = optimizer.wResult; +optApertureInfoVec = optimizer.wResult; + + +%Additional VMAT stuff +if pln.propOpt.preconditioner + % revert scaling + + dij.weightToMU = dij.weightToMU./dij.scaleFactor; + resultGUI.apertureInfo.weightToMU = resultGUI.apertureInfo.weightToMU./dij.scaleFactor; + optApertureInfoVec(1:apertureInfo.totalNumOfShapes) = optApertureInfoVec(1:apertureInfo.totalNumOfShapes).*dij.scaleFactor; +end % update the apertureInfoStruct and calculate bixel weights -apertureInfo = matRad_OptimizationProblemDAO.matRad_daoVec2ApertureInfo(apertureInfo,wOpt); +newApertureInfo = optiProb.matRad_daoVec2ApertureInfo(resultGUI.apertureInfo,optApertureInfoVec); %Use optiprob here to automatically choose VMAT / DAO code + +% override also bixel weight vector in optResult struct +w = newApertureInfo.bixelWeights; +wDao = newApertureInfo.bixelWeights; + +dij.scaleFactor = 1; + +newApertureInfo = matRad_preconditionFactors(newApertureInfo); % logging final results matRad_cfg.dispInfo('Calculating final cubes...\n'); -resultGUI = matRad_calcCubes(apertureInfo.bixelWeights,dij); -resultGUI.w = apertureInfo.bixelWeights; -resultGUI.wDAO = apertureInfo.bixelWeights; -resultGUI.apertureInfo = apertureInfo; +resultGUI = matRad_calcCubes(w,dij); +resultGUI.w = w; +resultGUI.wDAO = wDao; +resultGUI.apertureInfo = newApertureInfo; + +if isfield(pln,'scaleDRx') && pln.scaleDRx + %Scale D95 in target to RXDose + resultGUI.QI = matRad_calcQualityIndicators(cst,pln,resultGUI.physicalDose); + + resultGUI.apertureInfo.scaleFacRx = max((pln.DRx/pln.numOfFractions)./[resultGUI.QI(pln.RxStruct).D_95]'); + resultGUI.apertureInfo.apertureVector(1:resultGUI.apertureInfo.totalNumOfShapes) = resultGUI.apertureInfo.apertureVector(1:resultGUI.apertureInfo.totalNumOfShapes)*resultGUI.apertureInfo.scaleFacRx; + + % update the apertureInfoStruct and calculate bixel weights + resultGUI.apertureInfo = matRad_daoVec2ApertureInfo(resultGUI.apertureInfo,resultGUI.apertureInfo.apertureVector); + + % override also bixel weight vector in optResult struct + resultGUI.w = resultGUI.apertureInfo.bixelWeights; + resultGUI.wDao = resultGUI.apertureInfo.bixelWeights; + + resultGUI.physicalDose = resultGUI.physicalDose.*resultGUI.apertureInfo.scaleFacRx; +end + +% update apertureInfoStruct with the maximum leaf speeds per segment +if pln.propOpt.runVMAT + resultGUI.apertureInfo = matRad_maxLeafSpeed(resultGUI.apertureInfo); + + %optimize delivery + resultGUI = matRad_optDelivery(resultGUI,1); + %resultGUI = matRad_calcDeliveryMetrics(resultGUI,pln,stf); +end \ No newline at end of file diff --git a/matRad/matRad_doseRecalc.m b/matRad/matRad_doseRecalc.m new file mode 100644 index 000000000..d800db74f --- /dev/null +++ b/matRad/matRad_doseRecalc.m @@ -0,0 +1,161 @@ +function recalc = matRad_doseRecalc(cst,pln,recalc,ct,apertureInfo,calcDoseDirect,dij) +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% matRad function to recalculate dose on a Dij angular resolution, or using +% the dynamic method, whichever the user wants% +% +% call +% resultGUI = matRad_doseRecalc(dij,apertureInfo,resultGUI,pln) +% +% input +% dij: matRad dij struct +% apertureInfo: aperture shape info struct +% resultGUI: resultGUI struct to which the output data will be added, if +% this field is empty optResult struct will be created +% (optional) +% +% output +% resultGUI: struct containing optimized fluence vector, dose, and +% shape info +% +% References +% +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2015 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSES.txt. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +if nargin < 6 + calcDoseDirect = true; +end + +recalc.apertureInfo = apertureInfo; + +%recalculate dose with finer gantry angles +%to do this, we need Dij matrices at these new angles +%Calculate dose directly + +%first, we need to update/generate new apertures for these angles +recalc.pln = matRad_VMATGantryAngles(recalc.pln,cst,ct); +if ~recalc.interpNew + %we will not interpolate new apertures + %easiest way to to this is to make ALL gantryAngles optGantryAngles + recalc.pln.propStf.DAOGantryAngles = recalc.pln.propStf.gantryAngles; +end + +cd stf +fname = sprintf('%.1f deg.mat',recalc.pln.propOpt.VMAToptions.maxGantryAngleSpacing); +if exist(fname,'file') + load(fname); +else + stf = matRad_generateStf(ct,cst,recalc.pln); + save(fname,'stf') +end +recalc.stf = stf; +clear stf +cd .. + +if ~recalc.interpNew || ~recalc.dijNew + %duplicate any beam angles that are directly between two old + %ones + duplicate = false(size(recalc.pln.propStf.gantryAngles)); + for i = 1:numel(recalc.pln.propStf.gantryAngles) + if numel(find(abs(recalc.pln.propStf.gantryAngles(i)-pln.propStf.gantryAngles) == min(abs(recalc.pln.propStf.gantryAngles(i)-pln.propStf.gantryAngles)))) > 1 + duplicate(i) = true; + end + end + newGantryAngles = zeros(1,numel(recalc.pln.propStf.gantryAngles)+nnz(duplicate)); + newCouchAngles = zeros(1,numel(recalc.pln.propStf.gantryAngles)+nnz(duplicate)); + tempStf = recalc.stf; + recalc.stf(1).copyInd = []; + tempStf(1).copyInd = []; + recalc.stf(1).stfCorr = []; + tempStf(1).stfCorr = []; + j = 1; + for i = 1:numel(recalc.pln.propStf.gantryAngles) + if duplicate(i) + tempStf(j).stfCorr = false; + newGantryAngles(j) = recalc.pln.propStf.gantryAngles(i); + newCouchAngles(j) = recalc.pln.propStf.couchAngles(i); + tempStf(j) = recalc.stf(i); + tempStf(j).gantryAngle = recalc.stf(i-1).gantryAngle; + tempStf(j).copyInd = 1; + + j = j+1; + + newGantryAngles(j) = recalc.pln.propStf.gantryAngles(i); + newCouchAngles(j) = recalc.pln.propStf.couchAngles(i); + tempStf(j) = recalc.stf(i); + tempStf(j).gantryAngle = recalc.stf(i+1).gantryAngle; + tempStf(j).copyInd = 2; + else + tempStf(j).stfCorr = true; + newGantryAngles(j) = recalc.pln.propStf.gantryAngles(i); + newCouchAngles(j) = recalc.pln.propStf.couchAngles(i); + tempStf(j) = recalc.stf(i); + end + j = j+1; + end + recalc.pln.propStf.gantryAngles = newGantryAngles; + recalc.pln.propStf.couchAngles = newCouchAngles; + recalc.pln.propStf.numOfBeams = numel(recalc.pln.propStf.gantryAngles); + %recalc.pln.optGantryAngles = recalc.pln.gantryAngles; + recalc.stf = tempStf; +end + +recalc = matRad_recalcApertureInfo(recalc,recalc.apertureInfo); + +if ~recalc.interpNew || ~recalc.dijNew + tempPln = recalc.pln; + tempStf = recalc.stf; + for i = 1:numel(tempPln.propStf.gantryAngles) + diff = abs(tempPln.propStf.gantryAngles(i)-pln.propStf.gantryAngles); + minDiffInd = diff == min(diff); + minDiffInd1 = find(tempPln.propStf.gantryAngles == pln.propStf.gantryAngles(find(minDiffInd,1,'first'))); + minDiffInd2 = find(tempPln.propStf.gantryAngles == pln.propStf.gantryAngles(find(minDiffInd,1,'last'))); + + if ~recalc.dijNew + if isempty(recalc.stf(i).copyInd) + recalc.stf(i) = tempStf(minDiffInd1); + recalc.pln.propStf.gantryAngles(i) = tempPln.propStf.gantryAngles(minDiffInd1); + elseif recalc.stf(i).copyInd == 1 + recalc.stf(i) = tempStf(minDiffInd1); + recalc.pln.propStf.gantryAngles(i) = tempPln.propStf.gantryAngles(minDiffInd1); + elseif recalc.stf(i).copyInd == 2 + recalc.stf(i) = tempStf(minDiffInd2); + recalc.pln.propStf.gantryAngles(i) = tempPln.propStf.gantryAngles(minDiffInd2); + end + elseif ~recalc.interpNew + if numel(minDiffInd) > 1 + recalc.stf(i).gantryAngle = tempPln.propStf.gantryAngles(i); + end + end + end +end + +% rename to something else? matRad_daoVec2ApertureInfo_recalc? +recalc.apertureInfo.propVMAT.continuousAperture = recalc.continuousAperture; +recalc.apertureInfo = matRad_daoVec2ApertureInfo_VMATrecalcDynamic(recalc.apertureInfo,recalc.apertureInfo.apertureVector); + +if calcDoseDirect + clear global + recalc.resultGUI = matRad_calcDoseDirect(ct,recalc.stf,recalc.pln,cst,recalc.apertureInfo.bixelWeights); +else + recalc.resultGUI.w = recalc.apertureInfo.bixelWeights; + + options.numOfScenarios = 1; + options.bioOpt = 'none'; + dij.scaleFactor = apertureInfo.weightToMU./dij.weightToMU; + d = matRad_backProjection(recalc.resultGUI.w,dij,options); + recalc.resultGUI.physicalDose = reshape(d{1},dij.dimensions); +end diff --git a/matRad/matRad_fluenceOptimization.m b/matRad/matRad_fluenceOptimization.m index 640b43e7c..84db4e82e 100644 --- a/matRad/matRad_fluenceOptimization.m +++ b/matRad/matRad_fluenceOptimization.m @@ -1,4 +1,4 @@ -function [resultGUI,optimizer] = matRad_fluenceOptimization(dij,cst,pln,wInit) +function [resultGUI,optimizer] = matRad_fluenceOptimization(dij,cst,pln,stf,wInit) % matRad inverse planning wrapper function % % call @@ -32,6 +32,22 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% input argument handling +if nargin >= 4 + % Check whether the 4th argument is a structure. + if ~isstruct(stf) + % If it is not, then assign it to wInit. + tmp = stf; + if nargin >= 5 + % If there are 5 arguments, swap stf and wInit. + stf = wInit; + end + wInit = tmp; + clear tmp + end +end + + matRad_cfg = MatRad_Config.instance(); % consider VOI priorities @@ -375,6 +391,44 @@ matRad_cfg.dispInfo('Using standard MU bounds of [0,Inf]!\n') end +if isfield(pln.propOpt,'runVMAT') && pln.propOpt.runVMAT + % Only the bixels belonging to FMO gantry angles should have their + % weights optimized. The rest should be initialized and bounded to + % zero. + + % wInit is already defined, but some of the beam weights will be set to + % 0. The remainder should be scaled such that the total weight is + % preserved. In doing this, do not assume that all of the weights in wInit + % are equal. + totalWeightInit = sum(wInit); + + % Loop through angles to find non-FMO beams. + offset = 0; + for i = 1:dij.numOfBeams + + if ~stf(i).propVMAT.FMOBeam + % This is not an FMO beam. Set wOnes for the bixels belonging + % to this beam to 0. + rayIndices = offset + (1:dij.numOfRaysPerBeam(i)); + wOnes(rayIndices) = 0; + end + + offset = offset + dij.numOfRaysPerBeam(i); + end + + % Zero out bixels in wInit. + wInit = wInit .* wOnes; + + % Rescale wInit to preserve sum. + wInit = wInit .* totalWeightInit ./ sum(wInit); + + % Set upper bound on bixels belonging to FMO angles to Inf; the rest, + % to 0. + optiProb.maximumW = wOnes; + optiProb.maximumW(optiProb.maximumW == 1) = Inf; + +end + if ~isfield(pln.propOpt,'optimizer') %While the default optimizer is IPOPT, we can try to fallback to %fmincon in case it does not work for some reason diff --git a/matRad/matRad_preconditionFactors.m b/matRad/matRad_preconditionFactors.m new file mode 100644 index 000000000..1a167ffe8 --- /dev/null +++ b/matRad/matRad_preconditionFactors.m @@ -0,0 +1,75 @@ +function apertureInfo = matRad_preconditionFactors(apertureInfo) +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Calculate preconditioning factors for DAO (only what Esther Wild called +% the Jacobi preconditioner, not the dij scaled). Scale weights in vector +% accordingly. +% +% call +% apertureInfo = +% matRad_preconditionFactors(apertureInfo) +% +% input +% apertureInfo: aperture shape info struct +% +% output +% apertureInfo: aperture shape info struct with new factors +% +% References +% [1] http://onlinelibrary.wiley.com/doi/10.1118/1.4914863/full +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2015 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSES.txt. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% This is the dij scaling factor which will be applied during DAO. It is +% given by the dividing the mean of the actual aperture weights by the +% bixel width. This factor will divide all of the aperture weights. + +% TODO: could probably integrate dijScaleFactor into jacobiScale. These +% were originally separated for research purposes. It would greatly +% simplify the code to have them all together. +dijScaleFactor = mean(apertureInfo.apertureVector(1:apertureInfo.totalNumOfShapes)./apertureInfo.jacobiScale)/(apertureInfo.bixelWidth); + +for i = 1:numel(apertureInfo.beam) + + if ~apertureInfo.runVMAT || (apertureInfo.runVMAT && apertureInfo.propVMAT.beam(i).DAOBeam) + % in other words, do this for every beam if it's not VMAT, and for + % optimized beams only if it is + + for j = 1:apertureInfo.beam(i).numOfShapes + + % To get the jacobi scaling factor, first factor the + % current aperture's weight out of the dijScaling factor. Also + % remove the bixel width. Now we have the mean weight relative + % to the current weight. + % Next, multiply by the sqrt of ~approximately the number of + % open bixels (slight modification to Esther Wild's formula). + % The variables corresponding to the aperture weights will be + % multiplied by this number, which will decrease the gradients. + + if apertureInfo.runVMAT + apertureInfo.beam(i).shape(j).jacobiScale = (dijScaleFactor./apertureInfo.beam(i).shape(j).weight).*sqrt(sum(apertureInfo.beam(i).shape(j).shapeMap(:).^2)./apertureInfo.beam(i).shape(j).sumGradSq); + else + apertureInfo.beam(i).shape(j).jacobiScale = (dijScaleFactor.*apertureInfo.bixelWidth./apertureInfo.beam(i).shape(j).weight).*sqrt(sum(apertureInfo.beam(i).shape(j).shapeMap(:).^2)); + end + apertureInfo.jacobiScale(apertureInfo.beam(i).shape(j).weightOffset) = apertureInfo.beam(i).shape(j).jacobiScale; + + apertureInfo.apertureVector(apertureInfo.beam(i).shape(j).weightOffset) = apertureInfo.beam(i).shape(j).jacobiScale*apertureInfo.beam(i).shape(j).weight; + end + end +end + +end + + diff --git a/matRad/matRad_recalcApertureInfo.m b/matRad/matRad_recalcApertureInfo.m new file mode 100644 index 000000000..3a2b69f97 --- /dev/null +++ b/matRad/matRad_recalcApertureInfo.m @@ -0,0 +1,230 @@ +function recalc = matRad_recalcApertureInfo(recalc,apertureInfoOld) +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% matRad function to apertures for a different dose resolution +% +% call +% recalc = matRad_recalcApertureInfo(recalc,apertureInfo) +% +% input +% recalc: +% apertureInfo: +% +% output +% recalc: +% +% References +% +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2015 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSES.txt. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +pln = recalc.pln; +stf = recalc.stf; + +apertureInfoNew = apertureInfoOld; +apertureInfoNew = rmfield(apertureInfoNew,'beam'); + +apertureInfoNew.totalNumOfBixels = sum([stf(:).totalNumOfBixels]); + +shapeInd = 1; + +if recalc.interpNew + oldGantryAngles = zeros(1,numel(apertureInfoOld.beam)); + oldLeftLeafPoss = zeros(apertureInfoOld.beam(1).numOfActiveLeafPairs,numel(apertureInfoOld.beam)); + oldRightLeafPoss = zeros(apertureInfoOld.beam(1).numOfActiveLeafPairs,numel(apertureInfoOld.beam)); + for phase = 1:apertureInfoOld.numPhases + for i = 1:numel(apertureInfoOld.beam) + oldGantryAngles(i) = apertureInfoOld.beam(i).gantryAngle; + oldLeftLeafPoss(:,i) = apertureInfoOld.beam(i).shape(1).leftLeafPos; + oldRightLeafPoss(:,i) = apertureInfoOld.beam(i).shape(1).rightLeafPos; + end + end +end + +% MLC parameters: +bixelWidth = stf(1).bixelWidth; % [mm] +numOfMLCLeafPairs = 80; +% define central leaf pair (here we want the 0mm position to be in the +% center of a leaf pair (e.g. leaf 41 stretches from -2.5mm to 2.5mm +% for a bixel/leafWidth of 5mm and 81 leaf pairs) +centralLeafPair = ceil(numOfMLCLeafPairs/2); + +% initializing variables +totalNumOfShapes = numel(stf); +% loop over all phases +for phase = 1:apertureInfoOld.numPhases + for i = 1:numel(apertureInfoOld.beam) + newInd = (apertureInfoOld.propVMAT.beam(i).doseAngleBorders(1) <= [stf.gantryAngle] & [stf.gantryAngle] <= apertureInfoOld.propVMAT.beam(i).doseAngleBorders(2)).*(1:numel([stf.gantryAngle])); + newInd(newInd == 0) = []; + + totalAmountOfOldWeight = 0; + + for j = newInd + % get x- and z-coordinates of bixels + rayPos_bev = reshape([stf(j).ray.rayPos_bev],3,[]); + X = rayPos_bev(1,:)'; + Z = rayPos_bev(3,:)'; + + % create ray-map + maxX = max(X); minX = min(X); + maxZ = max(Z); minZ = min(Z); + + dimX = (maxX-minX)/stf(j).bixelWidth + 1; + dimZ = (maxZ-minZ)/stf(j).bixelWidth + 1; + + rayMap = zeros(dimZ,dimX); + + % get indices for x and z positions + xPos = (X-minX)/stf(j).bixelWidth + 1; + zPos = (Z-minZ)/stf(j).bixelWidth + 1; + + % get indices in the ray-map + indInRay = zPos + (xPos-1)*dimZ; + + % fill ray-map + rayMap(indInRay) = 1; + + % create map of bixel indices + bixelIndMap = NaN * ones(dimZ,dimX); + bixelIndMap(indInRay) = [1:stf(j).numOfRays] + (j-1)*stf(1).numOfRays; + + % store physical position of first entry in bixelIndMap + posOfCornerBixel = [minX 0 minZ]; + + % get leaf limits from the leaf map + lim_l = NaN * ones(dimZ,1); + lim_r = NaN * ones(dimZ,1); + % looping oder leaf pairs + for l = 1:dimZ + lim_lInd = find(rayMap(l,:),1,'first'); + lim_rInd = find(rayMap(l,:),1,'last'); + % the physical position [mm] can be calculated from the indices + lim_l(l) = (lim_lInd-1)*bixelWidth + minX - 1/2*bixelWidth; + lim_r(l) = (lim_rInd-1)*bixelWidth + minX + 1/2*bixelWidth; + end + + leafPairPos = unique(Z); + + % find upmost and downmost leaf pair + topLeafPairPos = maxZ; + bottomLeafPairPos = minZ; + + topLeafPair = centralLeafPair - topLeafPairPos/bixelWidth; + bottomLeafPair = centralLeafPair - bottomLeafPairPos/bixelWidth; + + % create bool map of active leaf pairs + isActiveLeafPair = zeros(numOfMLCLeafPairs,1); + isActiveLeafPair(topLeafPair:bottomLeafPair) = 1; + + MLCWindow = [minX-bixelWidth/2 maxX+bixelWidth/2 ... + minZ-bixelWidth/2 maxZ+bixelWidth/2]; + + + % save data for each beam + apertureInfoNew.beam(j).numOfActiveLeafPairs = dimZ; + apertureInfoNew.beam(j).leafPairPos = leafPairPos; + apertureInfoNew.beam(j).isActiveLeafPair = isActiveLeafPair; + apertureInfoNew.beam(j).centralLeafPair = centralLeafPair; + apertureInfoNew.beam(j).lim_l = lim_l; + apertureInfoNew.beam(j).lim_r = lim_r; + apertureInfoNew.beam(j).bixelIndMap = bixelIndMap; + apertureInfoNew.beam(j).posOfCornerBixel = posOfCornerBixel; + apertureInfoNew.beam(j).MLCWindow = MLCWindow; + apertureInfoNew.beam(j).bixOffset = 1+(j-1)*dimZ; + apertureInfoNew.beam(j).shape(1).vectorOffset = totalNumOfShapes+1+(j-1)*dimZ; + + %inherit from old beam + apertureInfoNew.propVMAT.beam(j).leafDir = apertureInfoOld.propVMAT.beam(i).leafDir; + + %specific to new beam + apertureInfoNew.beam(j).gantryAngle = pln.propStf.gantryAngles(j); + apertureInfoNew.propVMAT.beam(j).doseAngleBorders = stf(j).propVMAT.doseAngleBorders; + apertureInfoNew.propVMAT.beam(j).doseAngleBorderCentreDiff = stf(j).propVMAT.doseAngleBorderCentreDiff; + apertureInfoNew.propVMAT.beam(j).doseAngleBordersDiff = stf(j).propVMAT.doseAngleBordersDiff; + apertureInfoNew.propVMAT.beam(j).lastDAOIndex = stf(j).propVMAT.lastDAOIndex; + apertureInfoNew.propVMAT.beam(j).nextDAOIndex = stf(j).propVMAT.lastDAOIndex; + + + amountOfOldSpeed = (min(apertureInfoNew.propVMAT.beam(j).doseAngleBorders(2),apertureInfoOld.propVMAT.beam(i).doseAngleBorders(2))-max(apertureInfoNew.propVMAT.beam(j).doseAngleBorders(1),apertureInfoOld.propVMAT.beam(i).doseAngleBorders(1)))./apertureInfoNew.propVMAT.beam(j).doseAngleBordersDiff; + amountOfOldWeight = (min(apertureInfoNew.propVMAT.beam(j).doseAngleBorders(2),apertureInfoOld.propVMAT.beam(i).doseAngleBorders(2))-max(apertureInfoNew.propVMAT.beam(j).doseAngleBorders(1),apertureInfoOld.propVMAT.beam(i).doseAngleBorders(1)))./apertureInfoOld.propVMAT.beam(i).doseAngleBordersDiff; + + totalAmountOfOldWeight = totalAmountOfOldWeight+amountOfOldWeight; + + amountOfOldWeight_I = (min(apertureInfoNew.beam(j).gantryAngle,apertureInfoOld.propVMAT.beam(i).doseAngleBorders(2))-max(apertureInfoNew.propVMAT.beam(j).doseAngleBorders(1),apertureInfoOld.propVMAT.beam(i).doseAngleBorders(1)))./apertureInfoOld.propVMAT.beam(i).doseAngleBordersDiff; + amountOfOldWeight_F = (min(apertureInfoNew.propVMAT.beam(j).doseAngleBorders(2),apertureInfoOld.propVMAT.beam(i).doseAngleBorders(2))-max(apertureInfoNew.beam(j).gantryAngle,apertureInfoOld.propVMAT.beam(i).doseAngleBorders(1)))./apertureInfoOld.propVMAT.beam(i).doseAngleBordersDiff; + + if ~isfield(apertureInfoNew.beam(j),'gantryRot') || isempty(apertureInfoNew.beam(j).gantryRot) + apertureInfoNew.beam(j).gantryRot = 0; + apertureInfoNew.beam(j).shape(1).weight = 0; + apertureInfoNew.beam(j).shape(1).weight_I = 0; + apertureInfoNew.beam(j).shape(1).weight_F = 0; + end + apertureInfoNew.beam(j).gantryRot = amountOfOldSpeed*apertureInfoOld.beam(i).gantryRot+apertureInfoNew.beam(j).gantryRot; + + %recalculate weight, MU + apertureInfoNew.beam(j).shape(1).weight = apertureInfoNew.beam(j).shape(1).weight+amountOfOldWeight*apertureInfoOld.beam(i).shape(1).weight; + apertureInfoNew.beam(j).shape(1).weight_I = apertureInfoNew.beam(j).shape(1).weight_I+amountOfOldWeight_I*apertureInfoOld.beam(i).shape(1).weight; + apertureInfoNew.beam(j).shape(1).weight_F = apertureInfoNew.beam(j).shape(1).weight_F+amountOfOldWeight_F*apertureInfoOld.beam(i).shape(1).weight; + apertureInfoNew.beam(j).MU = apertureInfoNew.beam(j).shape(1).weight.*apertureInfoNew.weightToMU; + + apertureInfoNew.beam(j).MURate = apertureInfoNew.beam(j).MU.*apertureInfoNew.beam(j).gantryRot./apertureInfoNew.propVMAT.beam(j).doseAngleBordersDiff; + + %apertureInfoNew.beam(j).shape(1).jacobiScale = apertureInfoOld.beam(i).shape(1).jacobiScale; + apertureInfoNew.jacobiScale(j) = 1; + apertureInfoNew.beam(j).shape(1).jacobiScale = 1; + + if recalc.interpNew + %interpolate new apertures now so that weights are not + %overwritten + apertureInfoNew.beam(j).shape(1).leftLeafPos = (interp1(oldGantryAngles',oldLeftLeafPoss',apertureInfoNew.beam(j).gantryAngle))'; + apertureInfoNew.beam(j).shape(1).rightLeafPos = (interp1(oldGantryAngles',oldRightLeafPoss',apertureInfoNew.beam(j).gantryAngle))'; + + apertureInfoNew.beam(j).shape(1).leftLeafPos_I = (interp1(oldGantryAngles',oldLeftLeafPoss',apertureInfoNew.propVMAT.beam(j).doseAngleBorders(1)))'; + apertureInfoNew.beam(j).shape(1).rightLeafPos_I = (interp1(oldGantryAngles',oldRightLeafPoss',apertureInfoNew.propVMAT.beam(j).doseAngleBorders(1)))'; + + apertureInfoNew.beam(j).shape(1).leftLeafPos_F = (interp1(oldGantryAngles',oldLeftLeafPoss',apertureInfoNew.propVMAT.beam(j).doseAngleBorders(2)))'; + apertureInfoNew.beam(j).shape(1).rightLeafPos_F = (interp1(oldGantryAngles',oldRightLeafPoss',apertureInfoNew.propVMAT.beam(j).doseAngleBorders(2)))'; + else + apertureInfoNew.beam(j).shape(1).leftLeafPos = apertureInfoOld.beam(i).shape(1).leftLeafPos; + apertureInfoNew.beam(j).shape(1).rightLeafPos = apertureInfoOld.beam(i).shape(1).rightLeafPos; + end + + %all beams are now "optimized" to prevent their weights from being + %overwritten + %optAngleBorders becomes doseAngleBorders + apertureInfoNew.beam(j).numOfShapes = 1; + apertureInfoNew.propVMAT.beam(j).DAOBeam = true; + apertureInfoNew.propVMAT.beam(j).DAOAngleBorders = stf(j).propVMAT.doseAngleBorders; + apertureInfoNew.propVMAT.beam(j).DAOAngleBorderCentreDiff = stf(j).propVMAT.doseAngleBorderCentreDiff; + apertureInfoNew.propVMAT.beam(j).DAOAngleBordersDiff = stf(j).propVMAT.doseAngleBordersDiff; + apertureInfoNew.propVMAT.beam(j).timeFacCurr = apertureInfoNew.propVMAT.beam(j).doseAngleBordersDiff./apertureInfoNew.propVMAT.beam(j).DAOAngleBordersDiff; % = 1 + + apertureInfoNew.apertureVector(shapeInd) = apertureInfoNew.beam(j).shape(1).weight; + shapeInd = shapeInd+1; + end + + end +end + +apertureInfoNew.totalNumOfShapes = sum([apertureInfoNew.beam.numOfShapes]); +apertureInfoNew.totalNumOfLeafPairs = sum([apertureInfoNew.beam.numOfShapes]*[apertureInfoNew.beam.numOfActiveLeafPairs]'); +apertureInfoNew.doseTotalNumOfLeafPairs = sum([apertureInfoNew.beam(:).numOfActiveLeafPairs]); +apertureInfoNew.totalNumOfOptBixels = apertureInfoNew.totalNumOfBixels; + +%recalc apertureVector +[apertureInfoNew.apertureVector, apertureInfoNew.mappingMx, apertureInfoNew.limMx] = matRad_daoApertureInfo2Vec(apertureInfoNew); + +recalc.apertureInfo = apertureInfoNew; +recalc.stf = stf; \ No newline at end of file diff --git a/matRad/matRad_sequencing.m b/matRad/matRad_sequencing.m index 51207ada2..85fc7df75 100644 --- a/matRad/matRad_sequencing.m +++ b/matRad/matRad_sequencing.m @@ -48,18 +48,24 @@ matRad_cfg.dispWarning ('pln.propSeq.sequencer not specified. Using siochi leaf sequencing (default).') end - if ~isfield(pln.propSeq, 'sequencingLevel') - pln.propSeq.sequencingLevel = 5; - matRad_cfg.dispWarning ('pln.propSeq.sequencingLevel not specified. Using 5 sequencing levels (default).') + + if ~any(isfield(pln.propSeq, {'numLevels','sequencingLevel'})) + pln.propSeq.numLevels = 5; + matRad_cfg.dispWarning ('pln.propSeq.sequencingLevel not specified. Using 5 sequencing levels (default).') + elseif isfield(pln.propSeq,'sequencingLevel') + matRad_cfg.dispDeprecationWarning('The pln.propSeq.sequencingLevel property is deprecated. Use pln.propSeq.numLevels instead!'); + pln.propSeq.numLevels = pln.propSeq.sequencingLevel; end + % Could probably consolidate a lot of the code in the following + % functions. switch pln.propSeq.sequencer case 'xia' - resultGUI = matRad_xiaLeafSequencing(resultGUI,stf,dij,pln.propSeq.sequencingLevel,visBool); + resultGUI = matRad_xiaLeafSequencing(resultGUI,stf,dij,pln,visBool); case 'engel' - resultGUI = matRad_engelLeafSequencing(resultGUI,stf,dij,pln.propSeq.sequencingLevel,visBool); + resultGUI = matRad_engelLeafSequencing(resultGUI,stf,dij,pln,visBool); case 'siochi' - resultGUI = matRad_siochiLeafSequencing(resultGUI,stf,dij,pln.propSeq.sequencingLevel,visBool); + resultGUI = matRad_siochiLeafSequencing(resultGUI,stf,dij,pln,visBool); otherwise matRad_cfg.dispError('Could not find specified sequencing algorithm ''%s''',pln.propSeq.sequencer); end diff --git a/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_constraintFunctions.m b/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_constraintFunctions.m index 66a5bec0f..5dea7ae99 100644 --- a/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_constraintFunctions.m +++ b/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_constraintFunctions.m @@ -39,12 +39,14 @@ apertureInfo = optiProb.apertureInfo; % value of constraints for leaves -leftLeafPos = apertureInfoVec([1:apertureInfo.totalNumOfLeafPairs]+apertureInfo.totalNumOfShapes); -rightLeafPos = apertureInfoVec(1+apertureInfo.totalNumOfLeafPairs+apertureInfo.totalNumOfShapes:end); +leftLeafPos = apertureInfoVec((1:apertureInfo.totalNumOfLeafPairs)+apertureInfo.totalNumOfShapes); +rightLeafPos = apertureInfoVec((1+apertureInfo.totalNumOfLeafPairs+apertureInfo.totalNumOfShapes):(apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2)); c_dao = rightLeafPos - leftLeafPos; + % bixel based objective function calculation c_dos = matRad_constraintFunctions@matRad_OptimizationProblem(optiProb,apertureInfo.bixelWeights,dij,cst); + % concatenate -c = [c_dao; c_dos]; +c = [c_dos; c_dao]; diff --git a/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_constraintJacobian.m b/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_constraintJacobian.m index 30bed9248..5798da5e6 100644 --- a/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_constraintJacobian.m +++ b/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_constraintJacobian.m @@ -41,15 +41,15 @@ % row indices i = repmat(1:apertureInfo.totalNumOfLeafPairs,1,2); % column indices -j = [apertureInfo.totalNumOfShapes+1:apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs ... - apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs+1:apertureInfo.totalNumOfShapes+2*apertureInfo.totalNumOfLeafPairs]; - +j = [(apertureInfo.totalNumOfShapes+1):(apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs) ... + ((apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs)+1):(apertureInfo.totalNumOfShapes+2*apertureInfo.totalNumOfLeafPairs)]; + % -1 for left leaves, 1 for right leaves s = [-1*ones(1,apertureInfo.totalNumOfLeafPairs) ones(1,apertureInfo.totalNumOfLeafPairs)]; jacob_dao = sparse(i,j,s, ... apertureInfo.totalNumOfLeafPairs, ... - apertureInfo.totalNumOfShapes+2*apertureInfo.totalNumOfLeafPairs, ... + numel(apertureInfoVec), ... 2*apertureInfo.totalNumOfLeafPairs); % compute jacobian of dosimetric constrainst @@ -57,41 +57,69 @@ % dosimetric jacobian in bixel space jacob_dos_bixel = matRad_constraintJacobian@matRad_OptimizationProblem(optiProb,apertureInfo.bixelWeights,dij,cst); -% allocate sparse matrix for dosimetric jacobian -jacob_dos = sparse(size(jacob_dos_bixel,1),numel(apertureInfoVec)); -if ~isempty(jacob_dos) +if ~isempty(jacob_dos_bixel) + %If we would have the apertureInfo.bixelJApVec in DAO, we could use + %this instead of the full if branch + %jacob_dos = jacob_dos_bixel*apertureInfo.bixelJApVec'; + + numOfConstraints = size(jacob_dos_bixel,1); + + i_sparse = 1:numOfConstraints; + i_sparse = kron(i_sparse,ones(1,numel(apertureInfoVec))); + + j_sparse = 1:numel(apertureInfoVec); + j_sparse = repmat(j_sparse,1,numOfConstraints); + + jacobSparseVec = zeros(numOfConstraints*size(apertureInfoVec,1),1); + % 1. calculate jacobian for aperture weights % loop over all beams - offset = 0; + conOffset = 0; for i = 1:numel(apertureInfo.beam) - + % get used bixels in beam ix = ~isnan(apertureInfo.beam(i).bixelIndMap); - + % loop over all shapes and add up the gradients x openingFrac for this shape - for j = 1:apertureInfo.beam(i).numOfShapes - jacob_dos(:,offset+j) = jacob_dos_bixel(:,apertureInfo.beam(i).bixelIndMap(ix)) ... - * apertureInfo.beam(i).shape(j).shapeMap(ix); + for j = 1:apertureInfo.beam(i).numOfShapes + + jacobSparseVec(conOffset+j == j_sparse) = jacob_dos_bixel(:,apertureInfo.beam(i).bixelIndMap(ix)) ... + * apertureInfo.beam(i).shape(j).shapeMap(ix)./apertureInfo.beam(i).shape(j).jacobiScale; end - + % increment offset - offset = offset + apertureInfo.beam(i).numOfShapes; - + conOffset = conOffset + apertureInfo.beam(i).numOfShapes; + end - - % 2. find corresponding bixel to the leaf Positions and aperture + + % 2. find corresponding bixel to the leaf Positions and aperture % weights to calculate the jacobian - jacob_dos(:,apertureInfo.totalNumOfShapes+1:end) = ... - ( ones(size(jacob_dos,1),1) * apertureInfoVec(apertureInfo.mappingMx(apertureInfo.totalNumOfShapes+1:end,2))' ) ... - .* jacob_dos_bixel(:,apertureInfo.bixelIndices(apertureInfo.totalNumOfShapes+1:end)) / apertureInfo.bixelWidth; - + + ixAperturesOnly = apertureInfo.totalNumOfShapes+1:apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2; %The first entries in most of the vectors denote shape weights + + indInSparseVec = repmat(ixAperturesOnly,1,numOfConstraints) ... + +kron((0:numOfConstraints-1)*numel(apertureInfoVec),ones(1,apertureInfo.totalNumOfLeafPairs*2)); + + jacobSparseVec(indInSparseVec) = ... + reshape(transpose(( ones(numOfConstraints,1) * apertureInfoVec(apertureInfo.mappingMx(ixAperturesOnly,2))' ) ... + .* jacob_dos_bixel(:,apertureInfo.bixelIndices) ./ ... + (ones(numOfConstraints,1) * (apertureInfo.bixelWidth.*apertureInfo.jacobiScale(apertureInfo.mappingMx(ixAperturesOnly,2)))')),[],1); + + % correct the sign for the left leaf positions - jacob_dos(:,apertureInfo.totalNumOfShapes+1:apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs) = ... - -jacob_dos(:,apertureInfo.totalNumOfShapes+1:apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs); - + %indInSparseVec = repmat(apertureInfo.totalNumOfShapes+1:apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs,1,numOfConstraints) ... + indInSparseVec = repmat(ixAperturesOnly(1:apertureInfo.totalNumOfLeafPairs),1,numOfConstraints) ... + +kron((0:numOfConstraints-1)*numel(apertureInfoVec),ones(1,apertureInfo.totalNumOfLeafPairs)); + + jacobSparseVec(indInSparseVec) = -jacobSparseVec(indInSparseVec); + + jacob_dos = sparse(i_sparse,j_sparse,jacobSparseVec,numOfConstraints,numel(apertureInfoVec)); +else + jacob_dos = sparse(0,0); end + % concatenate -jacob = [jacob_dao;jacob_dos]; +jacob = [jacob_dos; jacob_dao]; diff --git a/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_daoApertureInfo2Vec.m b/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_daoApertureInfo2Vec.m index ee8fca784..fd7a3ee70 100644 --- a/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_daoApertureInfo2Vec.m +++ b/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_daoApertureInfo2Vec.m @@ -16,7 +16,7 @@ % aperture weights (0/inf) and leav positions (custom) % % References -% +% % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -37,17 +37,26 @@ % first: aperature weights % second: left leaf positions % third: right leaf positions +% fourth (VMAT only): times between successive DAO gantry angles % initializing variables -apertureInfoVec = NaN * ones(apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2,1); +vecLength = (apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2); + +if apertureInfo.runVMAT + vecLength = vecLength+apertureInfo.totalNumOfShapes; %Extra set of (apertureInfo.totalNumOfShapes) number of elements, allowing arc sector times to be optimized +end + +apertureInfoVec = NaN * ones(vecLength,1); offset = 0; %% 1. aperture weights for i = 1:size(apertureInfo.beam,2) for j = 1:apertureInfo.beam(i).numOfShapes - apertureInfoVec(offset+j) = apertureInfo.beam(i).shape(j).weight; + + apertureInfoVec(offset+j) = apertureInfo.beam(i).shape(j).jacobiScale*apertureInfo.beam(i).shape(j).weight; %In VMAT, this weight is "spread" over unoptimized beams (assume constant dose rate over sector) + end offset = offset + apertureInfo.beam(i).numOfShapes; end @@ -56,24 +65,74 @@ %% fill the vector for all shapes of all beams for i = 1:size(apertureInfo.beam,2) for j = 1:apertureInfo.beam(i).numOfShapes - apertureInfoVec(offset+[1:apertureInfo.beam(i).numOfActiveLeafPairs]) = apertureInfo.beam(i).shape(j).leftLeafPos; - apertureInfoVec(offset+[1:apertureInfo.beam(i).numOfActiveLeafPairs]+apertureInfo.totalNumOfLeafPairs) = apertureInfo.beam(i).shape(j).rightLeafPos; - offset = offset + apertureInfo.beam(i).numOfActiveLeafPairs; + + if ~apertureInfo.runVMAT || ~apertureInfo.propVMAT.continuousAperture + + apertureInfoVec(offset+[1:apertureInfo.beam(i).numOfActiveLeafPairs]) = apertureInfo.beam(i).shape(j).leftLeafPos; + apertureInfoVec(offset+[1:apertureInfo.beam(i).numOfActiveLeafPairs]+apertureInfo.totalNumOfLeafPairs) = apertureInfo.beam(i).shape(j).rightLeafPos; + + offset = offset + apertureInfo.beam(i).numOfActiveLeafPairs; + else + + if apertureInfo.propVMAT.beam(i).doseAngleDAO(1) + apertureInfoVec(offset+[1:apertureInfo.beam(i).numOfActiveLeafPairs]) = apertureInfo.beam(i).shape(j).leftLeafPos_I; + apertureInfoVec(offset+[1:apertureInfo.beam(i).numOfActiveLeafPairs]+apertureInfo.totalNumOfLeafPairs) = apertureInfo.beam(i).shape(j).rightLeafPos_I; + + offset = offset + apertureInfo.beam(i).numOfActiveLeafPairs; + end + + if apertureInfo.propVMAT.beam(i).doseAngleDAO(2) + apertureInfoVec(offset+[1:apertureInfo.beam(i).numOfActiveLeafPairs]) = apertureInfo.beam(i).shape(j).leftLeafPos_F; + apertureInfoVec(offset+[1:apertureInfo.beam(i).numOfActiveLeafPairs]+apertureInfo.totalNumOfLeafPairs) = apertureInfo.beam(i).shape(j).rightLeafPos_F; + + offset = offset + apertureInfo.beam(i).numOfActiveLeafPairs; + end + end + end end +%% 3. time of arc sector/beam +if apertureInfo.runVMAT + offset = offset + apertureInfo.totalNumOfLeafPairs; + + %this gives a vector of the arc lengths belonging to each optimized CP + %unique gets rid of double-counted angles (which is every interior + %angle) + + optInd = [apertureInfo.propVMAT.beam.DAOBeam]; + optAngleLengths = [apertureInfo.propVMAT.beam(optInd).DAOAngleBordersDiff]; + optGantryRot = [apertureInfo.beam(optInd).gantryRot]; + apertureInfoVec((offset+1):end) = optAngleLengths./optGantryRot; %entries are the times until the next opt gantry angle is reached +end -%% 3. create additional information for later use +%% 4. create additional information for later use if nargout > 1 - mappingMx = NaN * ones(apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2,4); - limMx = NaN * ones(apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2,2); - limMx(1:apertureInfo.totalNumOfShapes,:) = ones(apertureInfo.totalNumOfShapes,1)*[0 inf]; + mappingMx = NaN * ones(vecLength,4); + limMx = NaN * ones(vecLength,2); + + limMx(1:(apertureInfo.totalNumOfShapes),:) = ones((apertureInfo.totalNumOfShapes),1)*[0 inf]; counter = 1; + for i = 1:numel(apertureInfo.beam) for j = 1:apertureInfo.beam(i).numOfShapes mappingMx(counter,1) = i; + if apertureInfo.runVMAT + fileName = apertureInfo.propVMAT.machineConstraintFile; + try + load(fileName,'machine'); + catch + error(['Could not find the following machine file: ' fileName ]); + end + + timeLimL = diff(apertureInfo.propVMAT.beam(i).DAOAngleBorders)/machine.constraints.gantryRotationSpeed(2); %Minimum time interval between two optimized beams/gantry angles + timeLimU = diff(apertureInfo.propVMAT.beam(i).DAOAngleBorders)/machine.constraints.gantryRotationSpeed(1); %Maximum time interval between two optimized beams/gantry angles + + mappingMx(counter+(apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2),1) = i; + limMx(counter+(apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2),:) = [timeLimL timeLimU]; + end counter = counter + 1; end end @@ -90,12 +149,30 @@ limMx(counter,1) = apertureInfo.beam(i).lim_l(k); limMx(counter,2) = apertureInfo.beam(i).lim_r(k); counter = counter + 1; + + if apertureInfo.runVMAT && apertureInfo.propVMAT.continuousAperture && nnz(apertureInfo.propVMAT.beam(i).doseAngleDAO) == 2 + %redo for initial and final leaf positions + %might have to revisit this after looking at gradient, + %esp. mappingMx(counter,2) + %only an issue for non-interpolated deliveries + mappingMx(counter,1) = i; + mappingMx(counter,2) = j + shapeOffset; % store global shape number for grad calc + mappingMx(counter,3) = j; % store local shape number + mappingMx(counter,4) = k; % store local leaf number + + limMx(counter,1) = apertureInfo.beam(i).lim_l(k); + limMx(counter,2) = apertureInfo.beam(i).lim_r(k); + counter = counter + 1; + end end end shapeOffset = shapeOffset + apertureInfo.beam(i).numOfShapes; end - mappingMx(counter:end,:) = mappingMx(apertureInfo.totalNumOfShapes+1:counter-1,:); - limMx(counter:end,:) = limMx(apertureInfo.totalNumOfShapes+1:counter-1,:); + mappingMx(counter:(apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2),:) = mappingMx(apertureInfo.totalNumOfShapes+1:counter-1,:); + limMx(counter:(apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2),:) = limMx(apertureInfo.totalNumOfShapes+1:counter-1,:); + +end end + diff --git a/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_daoVec2ApertureInfo.m b/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_daoVec2ApertureInfo.m index e7741fa73..2625e8bd3 100644 --- a/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_daoVec2ApertureInfo.m +++ b/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_daoVec2ApertureInfo.m @@ -6,7 +6,7 @@ % tips and bixel indices for gradient calculation % % call -% updatedInfo = matRad_daoVec2ApertureInfo(apertureInfo,apertureInfoVect) +% [updatedInfo,w,indVect] = matRad_daoVec2ApertureInfo(apertureInfo,apertureInfoVect) % % input % apertureInfo: aperture shape info struct @@ -16,7 +16,7 @@ % updatedInfo: updated aperture shape info struct according to apertureInfoVect % % References -% +% % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -36,109 +36,435 @@ % function to update the apertureInfo struct after the each iteraton of the % optimization -w = zeros(apertureInfo.totalNumOfBixels,1); - % initializing variables updatedInfo = apertureInfo; updatedInfo.apertureVector = apertureInfoVect; -shapeInd = 1; +if ~updatedInfo.runVMAT + shapeInd = 1; + + indVect = NaN*ones(2*apertureInfo.doseTotalNumOfLeafPairs,1); + offset = 0; + + % helper function to cope with numerical instabilities through rounding + round2 = @(a,b) round(a*10^b)/10^b; +else + + % options for bixel and Jacobian calculation + mlcOptions.bixelWidth = apertureInfo.bixelWidth; + calcOptions.continuousAperture = updatedInfo.propVMAT.continuousAperture; + vectorIndices.totalNumOfShapes = apertureInfo.totalNumOfShapes; +end -indVect = NaN*ones(apertureInfo.totalNumOfShapes + apertureInfo.totalNumOfLeafPairs,1); +w = zeros(apertureInfo.totalNumOfBixels,1); -% helper function to cope with numerical instabilities through rounding -round2 = @(a,b) round(a*10^b)/10^b; +if updatedInfo.runVMAT && ~all([updatedInfo.propVMAT.beam.DAOBeam]) + j = 1; + for i = 1:numel(updatedInfo.beam) + if updatedInfo.propVMAT.beam(i).DAOBeam + % update the shape weight + % rescale the weight from the vector using the previous + % iteration scaling factor + updatedInfo.beam(i).shape(j).weight = apertureInfoVect(updatedInfo.beam(i).shape(j).weightOffset)./updatedInfo.beam(i).shape(j).jacobiScale; + + updatedInfo.beam(i).shape(j).MU = updatedInfo.beam(i).shape(j).weight*updatedInfo.weightToMU; + updatedInfo.beam(i).time = apertureInfoVect((updatedInfo.totalNumOfShapes+updatedInfo.totalNumOfLeafPairs*2)+updatedInfo.propVMAT.beam(i).DAOIndex)*updatedInfo.propVMAT.beam(i).timeFacCurr; + updatedInfo.beam(i).gantryRot = updatedInfo.propVMAT.beam(i).doseAngleBordersDiff/updatedInfo.beam(i).time; + updatedInfo.beam(i).shape(j).MURate = updatedInfo.beam(i).shape(j).MU./updatedInfo.beam(i).time; + end + end +end +if updatedInfo.runVMAT + %% ONLY SUPPORTED IN VMAT FOR NOW + + % Jacobian matrix to be used in the DAO gradient function + % this tells us the gradient of a particular bixel with respect to an + % element in the apertureVector (aperture weight or leaf position) + % store as a vector for now, convert to sparse matrix later + + optBixelFactor = 7; + % For optimized beams: 5 = (1 from weights) + (3 from left leaf positions (I, M, and F)) + (3 from + % right leaf positions (I, M, and F)) + + if updatedInfo.runVMAT + intBixelFactor = 2*optBixelFactor+2; + % For interpolated beams: multiply this number times 2 (influenced by the + % one before and the one after), then add 2 (influenced by the time of the + % times before and after) + else + intBixelFactor = 2*optBixelFactor; + % For interpolated beams: multiply this number times 2 (influenced by the + % one before and the one after) + end + + % for the time (probability) gradients + optBixelFactor = optBixelFactor+apertureInfo.totalNumOfShapes; + intBixelFactor = intBixelFactor+apertureInfo.totalNumOfShapes; + + bixelJApVec_sz = (updatedInfo.totalNumOfOptBixels*optBixelFactor+(updatedInfo.totalNumOfBixels-updatedInfo.totalNumOfOptBixels)*intBixelFactor)*2; + + bixelJApVec_vec = zeros(1,bixelJApVec_sz); + + % vector indices + bixelJApVec_i = nan(1,bixelJApVec_sz); + % bixel indices + bixelJApVec_j = zeros(1,bixelJApVec_sz); + % offset + bixelJApVec_offset = 0; +end %% update the shapeMaps % here the new colimator positions are used to create new shapeMaps that % now include decimal values instead of binary +calcOptions.saveJacobian = true; + % loop over all beams for i = 1:numel(updatedInfo.beam) %posOfRightCornerPixel = apertureInfo.beam(i).posOfCornerBixel(1) + (size(apertureInfo.beam(i).bixelIndMap,2)-1)*apertureInfo.bixelWidth; - + % pre compute left and right bixel edges edges_l = updatedInfo.beam(i).posOfCornerBixel(1)... - + ([1:size(apertureInfo.beam(i).bixelIndMap,2)]-1-1/2)*updatedInfo.bixelWidth; + + ((1:size(apertureInfo.beam(i).bixelIndMap,2))-1-1/2)*updatedInfo.bixelWidth; edges_r = updatedInfo.beam(i).posOfCornerBixel(1)... - + ([1:size(apertureInfo.beam(i).bixelIndMap,2)]-1+1/2)*updatedInfo.bixelWidth; + + ((1:size(apertureInfo.beam(i).bixelIndMap,2))-1+1/2)*updatedInfo.bixelWidth; + + % get dimensions of 2d matrices that store shape/bixel information + n = apertureInfo.beam(i).numOfActiveLeafPairs; % loop over all shapes - for j = 1:updatedInfo.beam(i).numOfShapes - - % update the shape weight - updatedInfo.beam(i).shape(j).weight = apertureInfoVect(shapeInd); - - % get dimensions of 2d matrices that store shape/bixel information - n = apertureInfo.beam(i).numOfActiveLeafPairs; - - % extract left and right leaf positions from shape vector - vectorIx = updatedInfo.beam(i).shape(j).vectorOffset + ([1:n]-1); - leftLeafPos = apertureInfoVect(vectorIx); - rightLeafPos = apertureInfoVect(vectorIx+apertureInfo.totalNumOfLeafPairs); - - % update information in shape structure - updatedInfo.beam(i).shape(j).leftLeafPos = leftLeafPos; - updatedInfo.beam(i).shape(j).rightLeafPos = rightLeafPos; - - % rounding for numerical stability - leftLeafPos = round2(leftLeafPos,6); - rightLeafPos = round2(rightLeafPos,6); - - % - xPosIndLeftLeaf = round((leftLeafPos - apertureInfo.beam(i).posOfCornerBixel(1))/apertureInfo.bixelWidth + 1); - xPosIndRightLeaf = round((rightLeafPos - apertureInfo.beam(i).posOfCornerBixel(1))/apertureInfo.bixelWidth + 1); - - % check limits because of rounding off issues at maximum, i.e., - % enforce round(X.5) -> X - xPosIndLeftLeaf(leftLeafPos == apertureInfo.beam(i).lim_r) = ... - .5 + (leftLeafPos(leftLeafPos == apertureInfo.beam(i).lim_r) ... - - apertureInfo.beam(i).posOfCornerBixel(1))/apertureInfo.bixelWidth; - xPosIndRightLeaf(rightLeafPos == apertureInfo.beam(i).lim_r) = ... - .5 + (rightLeafPos(rightLeafPos == apertureInfo.beam(i).lim_r) ... - - apertureInfo.beam(i).posOfCornerBixel(1))/apertureInfo.bixelWidth; - - % find the bixel index that the leaves currently touch - bixelIndLeftLeaf = apertureInfo.beam(i).bixelIndMap((xPosIndLeftLeaf-1)*n+[1:n]'); - bixelIndRightLeaf = apertureInfo.beam(i).bixelIndMap((xPosIndRightLeaf-1)*n+[1:n]'); + if updatedInfo.runVMAT + numOfShapes = 1; + calcOptions.DAOBeam = updatedInfo.propVMAT.beam(i).DAOBeam; + else + numOfShapes = updatedInfo.beam(i).numOfShapes; + end + + mlcOptions.lim_l = apertureInfo.beam(i).lim_l; + mlcOptions.lim_r = apertureInfo.beam(i).lim_r; + mlcOptions.edges_l = edges_l; + mlcOptions.edges_r = edges_r; + mlcOptions.centres = (edges_l+edges_r)/2; + mlcOptions.widths = edges_r-edges_l; + mlcOptions.n = n; + mlcOptions.numBix = size(apertureInfo.beam(i).bixelIndMap,2); + mlcOptions.bixelIndMap = apertureInfo.beam(i).bixelIndMap; + + for j = 1:numOfShapes - if any(isnan(bixelIndLeftLeaf)) || any(isnan(bixelIndRightLeaf)) - error('cannot map leaf position to bixel index'); + if ~updatedInfo.runVMAT || updatedInfo.propVMAT.beam(i).DAOBeam + % either this is not VMAT, or if it is VMAT, this is a DAO beam + + % update the shape weight + updatedInfo.beam(i).shape(j).weight = apertureInfoVect(updatedInfo.beam(i).shape(j).weightOffset)./updatedInfo.beam(i).shape(j).jacobiScale; + + if updatedInfo.runVMAT + updatedInfo.beam(i).shape(j).MU = updatedInfo.beam(i).shape(j).weight*updatedInfo.weightToMU; + updatedInfo.beam(i).time = apertureInfoVect((updatedInfo.totalNumOfShapes+updatedInfo.totalNumOfLeafPairs*2)+updatedInfo.propVMAT.beam(i).DAOIndex)*updatedInfo.propVMAT.beam(i).timeFacCurr; + updatedInfo.beam(i).gantryRot = updatedInfo.propVMAT.beam(i).doseAngleBordersDiff/updatedInfo.beam(i).time; + updatedInfo.beam(i).shape(j).MURate = updatedInfo.beam(i).shape(j).MU./updatedInfo.beam(i).time; + end + + if ~updatedInfo.runVMAT || ~updatedInfo.propVMAT.continuousAperture + % extract left and right leaf positions from shape vector + vectorIx_L = updatedInfo.beam(i).shape(j).vectorOffset + ((1:n)-1); + vectorIx_R = vectorIx_L+apertureInfo.totalNumOfLeafPairs; + leftLeafPos = apertureInfoVect(vectorIx_L); + rightLeafPos = apertureInfoVect(vectorIx_R); + + % update information in shape structure + updatedInfo.beam(i).shape(j).leftLeafPos = leftLeafPos; + updatedInfo.beam(i).shape(j).leftLeafPos_I = leftLeafPos; + updatedInfo.beam(i).shape(j).leftLeafPos_F = leftLeafPos; + updatedInfo.beam(i).shape(j).rightLeafPos = rightLeafPos; + updatedInfo.beam(i).shape(j).rightLeafPos_I = rightLeafPos; + updatedInfo.beam(i).shape(j).rightLeafPos_F = rightLeafPos; + else + % extract left and right leaf positions from shape vector + vectorIx_LI = updatedInfo.beam(i).shape(j).vectorOffset(1) + ((1:n)-1); + vectorIx_RI = vectorIx_LI+apertureInfo.totalNumOfLeafPairs; + leftLeafPos_I = apertureInfoVect(vectorIx_LI); + rightLeafPos_I = apertureInfoVect(vectorIx_RI); + + vectorIx_LF = updatedInfo.beam(i).shape(j).vectorOffset(2) + ((1:n)-1); + vectorIx_RF = vectorIx_LF+apertureInfo.totalNumOfLeafPairs; + leftLeafPos_F = apertureInfoVect(vectorIx_LF); + rightLeafPos_F = apertureInfoVect(vectorIx_RF); + + % update information in shape structure + updatedInfo.beam(i).shape(j).leftLeafPos_I = leftLeafPos_I; + updatedInfo.beam(i).shape(j).rightLeafPos_I = rightLeafPos_I; + + updatedInfo.beam(i).shape(j).leftLeafPos_F = leftLeafPos_F; + updatedInfo.beam(i).shape(j).rightLeafPos_F = rightLeafPos_F; + end + + else + % this is an interpolated beam + + %MURate is interpolated between MURates of optimized apertures + updatedInfo.beam(i).gantryRot = 1./(updatedInfo.propVMAT.beam(i).timeFracFromLastDAO./updatedInfo.beam(updatedInfo.propVMAT.beam(i).lastDAOIndex).gantryRot+updatedInfo.propVMAT.beam(i).timeFracFromNextDAO./updatedInfo.beam(updatedInfo.propVMAT.beam(i).nextDAOIndex).gantryRot); + updatedInfo.beam(i).time = updatedInfo.propVMAT.beam(i).doseAngleBordersDiff./updatedInfo.beam(i).gantryRot; + updatedInfo.beam(i).shape(j).MURate = updatedInfo.propVMAT.beam(i).fracFromLastDAO*updatedInfo.beam(updatedInfo.propVMAT.beam(i).lastDAOIndex).shape(j).MURate+(1-updatedInfo.propVMAT.beam(i).fracFromLastDAO)*updatedInfo.beam(updatedInfo.propVMAT.beam(i).nextDAOIndex).shape(j).MURate; + + % calculate MU, weight + updatedInfo.beam(i).shape(j).MU = updatedInfo.beam(i).shape(j).MURate.*updatedInfo.beam(i).time; + updatedInfo.beam(i).shape(j).weight = updatedInfo.beam(i).shape(j).MU./updatedInfo.weightToMU; + + if ~updatedInfo.propVMAT.continuousAperture + + fracFromLastOpt = updatedInfo.propVMAT.beam(i).fracFromLastDAO; + fracFromLastOptI = updatedInfo.propVMAT.beam(i).fracFromLastDAO*ones(n,1); + fracFromLastOptF = updatedInfo.propVMAT.beam(i).fracFromLastDAO*ones(n,1); + fracFromNextOptI = (1-updatedInfo.propVMAT.beam(i).fracFromLastDAO)*ones(n,1); + fracFromNextOptF = (1-updatedInfo.propVMAT.beam(i).fracFromLastDAO)*ones(n,1); + + % obtain leaf positions at last DAO beam + vectorIx_LF_last = updatedInfo.beam(updatedInfo.propVMAT.beam(i).lastDAOIndex).shape(j).vectorOffset + ((1:n)-1); + vectorIx_RF_last = vectorIx_LF_last+apertureInfo.totalNumOfLeafPairs; + leftLeafPos_last = apertureInfoVect(vectorIx_LF_last); + rightLeafPos_last = apertureInfoVect(vectorIx_RF_last); + + % obtain leaf positions at next DAO beam + vectorIx_LI_next = updatedInfo.beam(updatedInfo.propVMAT.beam(i).nextDAOIndex).shape(j).vectorOffset + ((1:n)-1); + vectorIx_RI_next = vectorIx_LI_next+apertureInfo.totalNumOfLeafPairs; + leftLeafPos_next = apertureInfoVect(vectorIx_LI_next); + rightLeafPos_next = apertureInfoVect(vectorIx_RI_next); + + % interpolate leaf positions + leftLeafPos = updatedInfo.propVMAT.beam(i).fracFromLastDAO*leftLeafPos_last+(1-updatedInfo.propVMAT.beam(i).fracFromLastDAO)*leftLeafPos_next; + rightLeafPos = updatedInfo.propVMAT.beam(i).fracFromLastDAO*rightLeafPos_last+(1-updatedInfo.propVMAT.beam(i).fracFromLastDAO)*rightLeafPos_next; + + % update information in shape structure + updatedInfo.beam(i).shape(j).leftLeafPos = leftLeafPos; + updatedInfo.beam(i).shape(j).leftLeafPos_I = leftLeafPos; + updatedInfo.beam(i).shape(j).leftLeafPos_F = leftLeafPos; + updatedInfo.beam(i).shape(j).rightLeafPos = rightLeafPos; + updatedInfo.beam(i).shape(j).rightLeafPos_I = rightLeafPos; + updatedInfo.beam(i).shape(j).rightLeafPos_F = rightLeafPos; + else + + fracFromLastOpt = updatedInfo.propVMAT.beam(i).fracFromLastDAO; + fracFromLastOptI = updatedInfo.propVMAT.beam(i).fracFromLastDAO_I*ones(n,1); + fracFromLastOptF = updatedInfo.propVMAT.beam(i).fracFromLastDAO_F*ones(n,1); + fracFromNextOptI = updatedInfo.propVMAT.beam(i).fracFromNextDAO_I*ones(n,1); + fracFromNextOptF = updatedInfo.propVMAT.beam(i).fracFromNextDAO_F*ones(n,1); + + % obtain leaf positions at last DAO beam + vectorIx_LF_last = updatedInfo.beam(updatedInfo.propVMAT.beam(i).lastDAOIndex).shape(j).vectorOffset(2) + ((1:n)-1); + vectorIx_RF_last = vectorIx_LF_last+apertureInfo.totalNumOfLeafPairs; + leftLeafPos_F_last = apertureInfoVect(vectorIx_LF_last); + rightLeafPos_F_last = apertureInfoVect(vectorIx_RF_last); + + % obtain leaf positions at next DAO beam + vectorIx_LI_next = updatedInfo.beam(updatedInfo.propVMAT.beam(i).nextDAOIndex).shape(j).vectorOffset(1) + ((1:n)-1); + vectorIx_RI_next = vectorIx_LI_next+apertureInfo.totalNumOfLeafPairs; + leftLeafPos_I_next = apertureInfoVect(vectorIx_LI_next); + rightLeafPos_I_next = apertureInfoVect(vectorIx_RI_next); + + % interpolate leaf positions + updatedInfo.beam(i).shape(j).leftLeafPos_I = fracFromLastOptI.*leftLeafPos_F_last+fracFromNextOptI.*leftLeafPos_I_next; + updatedInfo.beam(i).shape(j).rightLeafPos_I = fracFromLastOptI.*rightLeafPos_F_last+fracFromNextOptI.*rightLeafPos_I_next; + + updatedInfo.beam(i).shape(j).leftLeafPos_F = fracFromLastOptF.*leftLeafPos_F_last+fracFromNextOptF.*leftLeafPos_I_next; + updatedInfo.beam(i).shape(j).rightLeafPos_F = fracFromLastOptF.*rightLeafPos_F_last+fracFromNextOptF.*rightLeafPos_I_next; + end end - % store information in index vector for gradient calculation - indVect(apertureInfo.beam(i).shape(j).vectorOffset+[1:n]-1) = bixelIndLeftLeaf; - indVect(apertureInfo.beam(i).shape(j).vectorOffset+[1:n]-1+apertureInfo.totalNumOfLeafPairs) = bixelIndRightLeaf; - - % calculate opening fraction for every bixel in shape to construct - % bixel weight vector - - coveredByLeftLeaf = bsxfun(@minus,leftLeafPos,edges_l) / updatedInfo.bixelWidth; - coveredByRightLeaf = bsxfun(@minus,edges_r,rightLeafPos) / updatedInfo.bixelWidth; - - tempMap = 1 - (coveredByLeftLeaf + abs(coveredByLeftLeaf)) / 2 ... - - (coveredByRightLeaf + abs(coveredByRightLeaf)) / 2; + if ~updatedInfo.runVMAT + + % rounding for numerical stability + leftLeafPos = round2(leftLeafPos,10); + rightLeafPos = round2(rightLeafPos,10); + + % check overshoot of leaf positions + leftLeafPos(leftLeafPos <= apertureInfo.beam(i).lim_l) = apertureInfo.beam(i).lim_l(leftLeafPos <= apertureInfo.beam(i).lim_l); + rightLeafPos(rightLeafPos <= apertureInfo.beam(i).lim_l) = apertureInfo.beam(i).lim_l(rightLeafPos <= apertureInfo.beam(i).lim_l); + leftLeafPos(leftLeafPos >= apertureInfo.beam(i).lim_r) = apertureInfo.beam(i).lim_r(leftLeafPos >= apertureInfo.beam(i).lim_r); + rightLeafPos(rightLeafPos >= apertureInfo.beam(i).lim_r) = apertureInfo.beam(i).lim_r(rightLeafPos >= apertureInfo.beam(i).lim_r); + + % + xPosIndLeftLeaf = round((leftLeafPos - apertureInfo.beam(i).posOfCornerBixel(1))/apertureInfo.bixelWidth + 1); + xPosIndRightLeaf = round((rightLeafPos - apertureInfo.beam(i).posOfCornerBixel(1))/apertureInfo.bixelWidth + 1); + + % + xPosIndLeftLeaf_lim = floor((apertureInfo.beam(i).lim_l - apertureInfo.beam(i).posOfCornerBixel(1))/apertureInfo.bixelWidth+1); + xPosIndRightLeaf_lim = ceil((apertureInfo.beam(i).lim_r - apertureInfo.beam(i).posOfCornerBixel(1))/apertureInfo.bixelWidth + 1); + + xPosIndLeftLeaf(xPosIndLeftLeaf <= xPosIndLeftLeaf_lim) = xPosIndLeftLeaf_lim(xPosIndLeftLeaf <= xPosIndLeftLeaf_lim)+1; + xPosIndRightLeaf(xPosIndRightLeaf >= xPosIndRightLeaf_lim) = xPosIndRightLeaf_lim(xPosIndRightLeaf >= xPosIndRightLeaf_lim)-1; + + % check limits because of rounding off issues at maximum, i.e., + % enforce round(X.5) -> X + % LeafPos can occasionally go slightly beyond lim_r, so changed + % == check to >= + xPosIndLeftLeaf(leftLeafPos >= apertureInfo.beam(i).lim_r) = round(... + .5 + (leftLeafPos(leftLeafPos >= apertureInfo.beam(i).lim_r) ... + - apertureInfo.beam(i).posOfCornerBixel(1))/apertureInfo.bixelWidth); + + xPosIndRightLeaf(rightLeafPos >= apertureInfo.beam(i).lim_r) = round(... + .5 + (rightLeafPos(rightLeafPos >= apertureInfo.beam(i).lim_r) ... + - apertureInfo.beam(i).posOfCornerBixel(1))/apertureInfo.bixelWidth); + + % find the bixel index that the leaves currently touch + bixelIndLeftLeaf = apertureInfo.beam(i).bixelIndMap((xPosIndLeftLeaf-1)*n+[1:n]'); + bixelIndRightLeaf = apertureInfo.beam(i).bixelIndMap((xPosIndRightLeaf-1)*n+[1:n]'); + + if any(isnan(bixelIndLeftLeaf)) || any(isnan(bixelIndRightLeaf)) + error('cannot map leaf position to bixel index'); + end + + % store information in index vector for gradient calculation + indVect(offset+(1:n)) = bixelIndLeftLeaf; + indVect(offset+(1:n)+apertureInfo.doseTotalNumOfLeafPairs) = bixelIndRightLeaf; + offset = offset+n; + + % calculate opening fraction for every bixel in shape to construct + % bixel weight vector + + coveredByLeftLeaf = bsxfun(@minus,leftLeafPos,edges_l) / updatedInfo.bixelWidth; + coveredByRightLeaf = bsxfun(@minus,edges_r,rightLeafPos) / updatedInfo.bixelWidth; + + tempMap = 1 - (coveredByLeftLeaf + abs(coveredByLeftLeaf)) / 2 ... + - (coveredByRightLeaf + abs(coveredByRightLeaf)) / 2; + + % find open bixels + tempMapIx = tempMap > 0; + + currBixelIx = apertureInfo.beam(i).bixelIndMap(tempMapIx); + w(currBixelIx) = w(currBixelIx) + tempMap(tempMapIx)*updatedInfo.beam(i).shape(j).weight; - % find open bixels - tempMapIx = tempMap > 0; + % save the tempMap (we need to apply a positivity operator !) + updatedInfo.beam(i).shape(j).shapeMap = (tempMap + abs(tempMap)) / 2; + + % increment shape index + shapeInd = shapeInd +1; + end - currBixelIx = apertureInfo.beam(i).bixelIndMap(tempMapIx); - w(currBixelIx) = w(currBixelIx) + tempMap(tempMapIx)*updatedInfo.beam(i).shape(j).weight; + end - % save the tempMap (we need to apply a positivity operator !) - updatedInfo.beam(i).shape(j).shapeMap = (tempMap + abs(tempMap)) / 2; + if updatedInfo.runVMAT - % increment shape index - shapeInd = shapeInd +1; + for j = 1:numOfShapes + + % shapeMap + shapeMap = zeros(size(updatedInfo.beam(i).bixelIndMap)); + % sumGradSq + sumGradSq = 0; + + % insert variables + vectorIndices.tIx_Vec = (apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2)+(1:apertureInfo.totalNumOfShapes); + + variables.weight = updatedInfo.beam(i).shape(j).weight; + variables.leftLeafPos_I = updatedInfo.beam(i).shape(j).leftLeafPos_I; + variables.leftLeafPos_F = updatedInfo.beam(i).shape(j).leftLeafPos_F; + variables.rightLeafPos_I = updatedInfo.beam(i).shape(j).rightLeafPos_I; + variables.rightLeafPos_F = updatedInfo.beam(i).shape(j).rightLeafPos_F; + + if updatedInfo.propVMAT.beam(i).DAOBeam + + variables.jacobiScale = updatedInfo.beam(i).shape(1).jacobiScale; + + vectorIndices.DAOindex = updatedInfo.propVMAT.beam(i).DAOIndex; + if updatedInfo.propVMAT.continuousAperture + vectorIndices.vectorIx_LI = updatedInfo.beam(i).shape(j).vectorOffset(1) + ((1:n)-1); + vectorIndices.vectorIx_LF = updatedInfo.beam(i).shape(j).vectorOffset(2) + ((1:n)-1); + vectorIndices.vectorIx_RI = vectorIndices.vectorIx_LI+apertureInfo.totalNumOfLeafPairs; + vectorIndices.vectorIx_RF = vectorIndices.vectorIx_LF+apertureInfo.totalNumOfLeafPairs; + else + vectorIndices.vectorIx_LI = updatedInfo.beam(i).shape(j).vectorOffset + ((1:n)-1); + vectorIndices.vectorIx_LF = updatedInfo.beam(i).shape(j).vectorOffset + ((1:n)-1); + vectorIndices.vectorIx_RI = vectorIndices.vectorIx_LI+apertureInfo.totalNumOfLeafPairs; + vectorIndices.vectorIx_RF = vectorIndices.vectorIx_LF+apertureInfo.totalNumOfLeafPairs; + end + else + + variables.weight_last = updatedInfo.beam(updatedInfo.propVMAT.beam(i).lastDAOIndex).shape(j).weight; + variables.weight_next = updatedInfo.beam(updatedInfo.propVMAT.beam(i).nextDAOIndex).shape(j).weight; + + variables.jacobiScale_last = updatedInfo.beam(updatedInfo.propVMAT.beam(i).lastDAOIndex).shape(1).jacobiScale; + variables.jacobiScale_next = updatedInfo.beam(updatedInfo.propVMAT.beam(i).nextDAOIndex).shape(1).jacobiScale; + + variables.time_last = updatedInfo.beam(updatedInfo.propVMAT.beam(i).lastDAOIndex).time; + variables.time_next = updatedInfo.beam(updatedInfo.propVMAT.beam(i).nextDAOIndex).time; + variables.time = updatedInfo.beam(i).time; + + variables.fracFromLastOptI = fracFromLastOptI; + variables.fracFromLastOptF = fracFromLastOptF; + variables.fracFromNextOptI = fracFromNextOptI; + variables.fracFromNextOptF = fracFromNextOptF; + variables.fracFromLastOpt = fracFromLastOpt; + + variables.doseAngleBordersDiff = updatedInfo.propVMAT.beam(i).doseAngleBordersDiff; + variables.doseAngleBordersDiff_last = updatedInfo.propVMAT.beam(updatedInfo.propVMAT.beam(i).lastDAOIndex).doseAngleBordersDiff; + variables.doseAngleBordersDiff_next = updatedInfo.propVMAT.beam(updatedInfo.propVMAT.beam(i).nextDAOIndex).doseAngleBordersDiff; + variables.timeFacCurr_last = updatedInfo.propVMAT.beam(updatedInfo.propVMAT.beam(i).lastDAOIndex).timeFacCurr; + variables.timeFacCurr_next = updatedInfo.propVMAT.beam(updatedInfo.propVMAT.beam(i).nextDAOIndex).timeFacCurr; + variables.fracFromLastDAO = updatedInfo.propVMAT.beam(i).fracFromLastDAO; + variables.timeFracFromLastDAO = updatedInfo.propVMAT.beam(i).timeFracFromLastDAO; + variables.timeFracFromNextDAO = updatedInfo.propVMAT.beam(i).timeFracFromNextDAO; + + vectorIndices.DAOindex_last = updatedInfo.propVMAT.beam(updatedInfo.propVMAT.beam(i).lastDAOIndex).DAOIndex; + vectorIndices.DAOindex_next = updatedInfo.propVMAT.beam(updatedInfo.propVMAT.beam(i).nextDAOIndex).DAOIndex; + vectorIndices.tIx_last = (apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2)+updatedInfo.propVMAT.beam(updatedInfo.propVMAT.beam(i).lastDAOIndex).DAOIndex; + vectorIndices.tIx_next = (apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2)+updatedInfo.propVMAT.beam(updatedInfo.propVMAT.beam(i).nextDAOIndex).DAOIndex; + + if updatedInfo.propVMAT.continuousAperture + vectorIndices.vectorIx_LF_last = updatedInfo.beam(updatedInfo.propVMAT.beam(i).lastDAOIndex).shape(j).vectorOffset(2) + ((1:n)-1); + vectorIndices.vectorIx_LI_next = updatedInfo.beam(updatedInfo.propVMAT.beam(i).nextDAOIndex).shape(j).vectorOffset(1) + ((1:n)-1); + vectorIndices.vectorIx_RF_last = vectorIndices.vectorIx_LF_last+apertureInfo.totalNumOfLeafPairs; + vectorIndices.vectorIx_RI_next = vectorIndices.vectorIx_LI_next+apertureInfo.totalNumOfLeafPairs; + else + vectorIndices.vectorIx_LF_last = updatedInfo.beam(updatedInfo.propVMAT.beam(i).lastDAOIndex).shape(j).vectorOffset + ((1:n)-1); + vectorIndices.vectorIx_LI_next = updatedInfo.beam(updatedInfo.propVMAT.beam(i).nextDAOIndex).shape(j).vectorOffset + ((1:n)-1); + vectorIndices.vectorIx_RF_last = vectorIndices.vectorIx_LF_last+apertureInfo.totalNumOfLeafPairs; + vectorIndices.vectorIx_RI_next = vectorIndices.vectorIx_LI_next+apertureInfo.totalNumOfLeafPairs; + end + end + + counters.bixelJApVec_offset = bixelJApVec_offset; + + % calculate bixel weight and derivative in function + [w,bixelJApVec_vec,bixelJApVec_i,bixelJApVec_j,sumGradSq,shapeMap,counters] = ... + matRad_bixWeightAndGrad(calcOptions,mlcOptions,variables,vectorIndices,counters,w,bixelJApVec_vec,bixelJApVec_i,bixelJApVec_j,sumGradSq,shapeMap); + + bixelJApVec_offset = counters.bixelJApVec_offset; + + % update shapeMap + updatedInfo.beam(i).shape(j).shapeMap = shapeMap; + % update sumGradSq + % FIX THIS FOR INTERPOLATED ANGLES??? + updatedInfo.beam(i).shape(j).sumGradSq = sumGradSq; + + end end - end + +% save bixelWeight, apertureVector updatedInfo.bixelWeights = w; -updatedInfo.bixelIndices = indVect; +updatedInfo.apertureVector = apertureInfoVect; + +if updatedInfo.runVMAT + % save Jacobian between bixelWeight, apertureVector + + deleteInd_i = isnan(bixelJApVec_i); + deleteInd_j = bixelJApVec_j == 0; + if ~all(deleteInd_i == deleteInd_j) + error('Jacobian deletion mismatch'); + else + bixelJApVec_i(deleteInd_i) = []; + bixelJApVec_j(deleteInd_i) = []; + bixelJApVec_vec(deleteInd_i) = []; + end + updatedInfo.bixelJApVec = sparse(bixelJApVec_i,bixelJApVec_j,bixelJApVec_vec,numel(apertureInfoVect),updatedInfo.totalNumOfBixels); +else + % save indVect + updatedInfo.bixelIndices = indVect; +end end diff --git a/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_getConstraintBounds.m b/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_getConstraintBounds.m index f1be15e78..e4275d1b2 100644 --- a/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_getConstraintBounds.m +++ b/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_getConstraintBounds.m @@ -30,16 +30,16 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% get dosimetric bounds from cst (just like for conv opt) by call to +% superclass method +[cl_dos,cu_dos] = matRad_getConstraintBounds@matRad_OptimizationProblem(optiProb,cst); + apertureInfo = optiProb.apertureInfo; % Initialize bounds cl_dao = zeros(apertureInfo.totalNumOfLeafPairs,1); cu_dao = inf*ones(apertureInfo.totalNumOfLeafPairs,1); -% get dosimetric bounds from cst (just like for conv opt) by call to -% superclass method -[cl_dos,cu_dos] = matRad_getConstraintBounds@matRad_OptimizationProblem(optiProb,cst); - % concatenate -cl = [cl_dao; cl_dos]; -cu = [cu_dao; cu_dos]; +cl = [cl_dos; cl_dao]; +cu = [cu_dos; cu_dao]; diff --git a/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_getJacobianStructure.m b/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_getJacobianStructure.m index bf8b0309c..47a2ebfe1 100644 --- a/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_getJacobianStructure.m +++ b/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_getJacobianStructure.m @@ -60,32 +60,53 @@ % all stuff can be done per beam direction and then I use repmat to build % up the big matrix -% allocate -jacobStruct_dos = sparse(size(jacobStruct_dos_bixel,1),size(jacobStruct_dao,2)); +offset = 0; -if ~isempty(jacobStruct_dos) +if ~isempty(jacobStruct_dos_bixel) + numOfConstraints = size(jacobStruct_dos_bixel,1); + + i_sparse = 1:numOfConstraints; + i_sparse = kron(i_sparse,ones(1,numel(apertureInfo.apertureVector))); + + j_sparse = 1:numel(apertureInfo.apertureVector); + j_sparse = repmat(j_sparse,1,numOfConstraints); + + jacobStructSparseVec = zeros(numOfConstraints*numel(apertureInfo.apertureVector),1); - % first aperture weights - for i = 1:apertureInfo.totalNumOfShapes - currBeam = apertureInfo.mappingMx(i,1); - currBixelIxInBeam = dij.beamNum == currBeam; - jacobStruct_dos(:,i) = spones(sum(jacobStruct_dos_bixel(:,currBixelIxInBeam),2)); - end + %counter = apertureInfo.totalNumOfShapes; + for i = 1:numel(apertureInfo.beam) + %for i = 1:size(apertureInfo.beam,2) + + % get used bixels in beam + ixWeight = ~isnan(apertureInfo.beam(i).bixelIndMap); - % second leaves - counter = apertureInfo.totalNumOfShapes; - for i = 1:size(apertureInfo.beam,2) for j = 1:apertureInfo.beam(i).numOfShapes + % first weight + jacobStructSparseVec(offset+j == j_sparse) = jacobStructSparseVec(offset+j == j_sparse)+sum(jacobStruct_dos_bixel(:,apertureInfo.beam(i).bixelIndMap(ixWeight)),2); + + % now leaf positions for k = 1:apertureInfo.beam(i).numOfActiveLeafPairs - counter = counter + 1; - bixelIxInCurrRow = ~isnan(apertureInfo.beam(i).bixelIndMap(k,:)); - jacobStruct_dos(:,counter+[0 apertureInfo.totalNumOfLeafPairs]) = ... - repmat(spones(sum(jacobStruct_dos_bixel(:,bixelIxInCurrRow),2)),1,2); + + ixLeaf = ~isnan(apertureInfo.beam(i).bixelIndMap(k,:)); + indInBixVec = apertureInfo.beam(i).bixelIndMap(k,ixLeaf); + + indInOptVec = apertureInfo.beam(i).shape(1).vectorOffset+k-1+[0 apertureInfo.totalNumOfLeafPairs]; + indInSparseVec = repmat(indInOptVec,1,numOfConstraints)... + +kron((0:numOfConstraints-1)*numel(apertureInfo.apertureVector),ones(1,2)); + + jacobStructSparseVec(indInSparseVec) = jacobStructSparseVec(indInSparseVec)+repelem(sum(jacobStruct_dos_bixel(:,indInBixVec),2),2,1); end - end + + offset = offset+1; + end end - + + jacobStructSparseVec(jacobStructSparseVec ~= 0) = 1; + jacobStruct_dos = sparse(i_sparse,j_sparse,jacobStructSparseVec,numOfConstraints,numel(apertureInfo.apertureVector)); + +else + jacobStruct_dos = sparse(0,0); end % concatenate -jacobStruct = [jacobStruct_dao; jacobStruct_dos]; +jacobStruct = [jacobStruct_dos; jacobStruct_dao]; diff --git a/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_objectiveGradient.m b/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_objectiveGradient.m index e7a9da081..c8719c2c9 100644 --- a/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_objectiveGradient.m +++ b/matRad/optimization/@matRad_OptimizationProblemDAO/matRad_objectiveGradient.m @@ -53,7 +53,7 @@ % loop over all shapes and add up the gradients x openingFrac for this shape for j = 1:apertureInfo.beam(i).numOfShapes - g(j+offset) = apertureInfo.beam(i).shape(j).shapeMap(ix)' ... + g(j+offset) = apertureInfo.beam(i).shape(j).shapeMap(ix)' ./apertureInfo.beam(i).shape(j).jacobiScale ... * bixelG(apertureInfo.beam(i).bixelIndMap(ix)); end @@ -62,12 +62,15 @@ end +ixAperturesOnly = apertureInfo.totalNumOfShapes+1:apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2; %The first entries in most of the vectors denote shape weights + % 2. find corresponding bixel to the leaf Positions and aperture % weights to calculate the gradient -g(apertureInfo.totalNumOfShapes+1:end) = ... - apertureInfoVec(apertureInfo.mappingMx(apertureInfo.totalNumOfShapes+1:end,2)) ... - .* bixelG(apertureInfo.bixelIndices(apertureInfo.totalNumOfShapes+1:end)) / apertureInfo.bixelWidth; - -% correct the sign for the left leaf positions -g(apertureInfo.totalNumOfShapes+1:apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs) = ... - -g(apertureInfo.totalNumOfShapes+1:apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs); +g(ixAperturesOnly) = ... + apertureInfoVec(apertureInfo.mappingMx(ixAperturesOnly,2)) ... + .* bixelG(apertureInfo.bixelIndices) ./ ... + (apertureInfo.bixelWidth.*apertureInfo.jacobiScale(apertureInfo.mappingMx(ixAperturesOnly,2))); + + % correct the sign for the left leaf positions + g(apertureInfo.totalNumOfShapes+(1:(apertureInfo.totalNumOfLeafPairs))) = ... + -g(apertureInfo.totalNumOfShapes+(1:(apertureInfo.totalNumOfLeafPairs))); diff --git a/matRad/optimization/@matRad_OptimizationProblemVMAT/matRad_OptimizationProblemVMAT.m b/matRad/optimization/@matRad_OptimizationProblemVMAT/matRad_OptimizationProblemVMAT.m new file mode 100644 index 000000000..b69025f96 --- /dev/null +++ b/matRad/optimization/@matRad_OptimizationProblemVMAT/matRad_OptimizationProblemVMAT.m @@ -0,0 +1,26 @@ +classdef matRad_OptimizationProblemVMAT < matRad_OptimizationProblemDAO + %handle class to keep state easily + + + methods (Static) + %In External Files + updatedInfo = matRad_daoVec2ApertureInfo(apertureInfo,apertureInfoVect); + + [apertureInfoVec, mappingMx, limMx] = matRad_daoApertureInfo2Vec(apertureInfo); + end + + methods + function obj = matRad_OptimizationProblemVMAT(backProjection,apertureInfo) + obj = obj@matRad_OptimizationProblemDAO(backProjection,apertureInfo); + end + + function lb = lowerBounds(obj,w) + lb = obj.apertureInfo.limMx(:,1); % Lower bound on the variables. + end + + function ub = upperBounds(obj,w) + ub = obj.apertureInfo.limMx(:,2); + end + end +end + diff --git a/matRad/optimization/@matRad_OptimizationProblemVMAT/matRad_constraintFunctions.m b/matRad/optimization/@matRad_OptimizationProblemVMAT/matRad_constraintFunctions.m new file mode 100644 index 000000000..9173ac93e --- /dev/null +++ b/matRad/optimization/@matRad_OptimizationProblemVMAT/matRad_constraintFunctions.m @@ -0,0 +1,151 @@ +function c = matRad_constraintFunctions(optiProb,apertureInfoVec,dij,cst) +% matRad IPOPT callback: constraint function for VMAT +% +% call +% c = matRad_daoObjFunc(apertueInfoVec,dij,cst) +% +% input +% apertueInfoVec: aperture info vector +% dij: dose influence matrix +% cst: matRad cst struct +% options: option struct defining the type of optimization +% +% output +% c: value of constraints +% +% Reference +% [1] http://www.sciencedirect.com/science/article/pii/S0958394701000577 +% [2] http://www.sciencedirect.com/science/article/pii/S0360301601025858 +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2015 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSES.txt. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% DAO constraint function calculation +c_dos_dao = matRad_constraintFunctions@matRad_OptimizationProblemDAO(optiProb,apertureInfoVec,dij,cst); + +% update apertureInfo, bixel weight vector an mapping of leafes to bixels +%This update should have taken place if necessary in the call above +%if ~isequal(apertureInfoVec,optiProb.apertureInfo.apertureVector) +% optiProb.apertureInfo = matRad_daoVec2ApertureInfo(optiProb.apertureInfo,apertureInfoVec); +%end +apertureInfo = optiProb.apertureInfo; + +% We need the leaf pos +leftLeafPos = apertureInfoVec((1:apertureInfo.totalNumOfLeafPairs)+apertureInfo.totalNumOfShapes); +rightLeafPos = apertureInfoVec((1+apertureInfo.totalNumOfLeafPairs+apertureInfo.totalNumOfShapes):(apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2)); + +% values of times spent in an arc surrounding the optimized angles (full +% arc/dose influence arc) +timeDAOBorderAngles = apertureInfoVec(((apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2)+1):end); +timeDoseBorderAngles = timeDAOBorderAngles.*[apertureInfo.propVMAT.beam([apertureInfo.propVMAT.beam.DAOBeam]).timeFacCurr]'; + +if apertureInfo.continuousAperture + % Using the dynamic fluence calculation, we have the leaf positions in + % the vector be the leaf positions at the borders of the Dij arcs (for optimized angles only). + % Therefore we must also use the times between the borders of the Dij + % arc (for optimized angles only). + timeFac = [apertureInfo.propVMAT.beam.timeFac]'; + deleteInd = timeFac == 0; + timeFac(deleteInd) = []; + + i = [apertureInfo.propVMAT.beam.timeFacInd]'; + i(deleteInd) = []; + + j = repelem(1:apertureInfo.totalNumOfShapes,1,3); + j(deleteInd) = []; + + timeFacMatrix = sparse(i,j,timeFac,max(i),apertureInfo.totalNumOfShapes); + timeBNOptAngles = timeFacMatrix*timeDAOBorderAngles; + + % prep + leftLeafSpeed = zeros(apertureInfo.propVMAT.numLeafSpeedConstraint*apertureInfo.beam(1).numOfActiveLeafPairs,1); + rightLeafSpeed = zeros(apertureInfo.propVMAT.numLeafSpeedConstraint*apertureInfo.beam(1).numOfActiveLeafPairs,1); + + offset = 0; + shapeInd = 1; + + for i = 1:numel(apertureInfo.beam) + % loop over beams + n = apertureInfo.beam(i).numOfActiveLeafPairs; + + if ~isempty(apertureInfo.propVMAT.beam(i).leafConstMask) + + % get vector indices + if apertureInfo.propVMAT.beam(i).DAOBeam + % if it's a DAO beam, use own vector offset + vectorIx_LI = apertureInfo.beam(i).shape(1).vectorOffset(1) + ((1:n)-1); + vectorIx_LF = apertureInfo.beam(i).shape(1).vectorOffset(2) + ((1:n)-1); + else + % otherwise, use vector offset of previous and next + % beams + vectorIx_LI = apertureInfo.beam(apertureInfo.propVMAT.beam(i).lastDAOIndex).shape(1).vectorOffset(2) + ((1:n)-1); + vectorIx_LF = apertureInfo.beam(apertureInfo.propVMAT.beam(i).nextDAOIndex).shape(1).vectorOffset(1) + ((1:n)-1); + end + vectorIx_RI = vectorIx_LI+apertureInfo.totalNumOfLeafPairs; + vectorIx_RF = vectorIx_LF+apertureInfo.totalNumOfLeafPairs; + + % extract leaf positions, time + leftLeafPos_I = apertureInfoVec(vectorIx_LI); + rightLeafPos_I = apertureInfoVec(vectorIx_RI); + leftLeafPos_F = apertureInfoVec(vectorIx_LF); + rightLeafPos_F = apertureInfoVec(vectorIx_RF); + t = timeBNOptAngles(shapeInd); + + % determine indices + indInConVec = offset+(1:n); + + % calc speeds + leftLeafSpeed(indInConVec) = abs(leftLeafPos_F-leftLeafPos_I)./t; + rightLeafSpeed(indInConVec) = abs(rightLeafPos_F-rightLeafPos_I)./t; + + % update offset + offset = offset+n; + + % increment shapeInd only for beams which have transtion + % defined + shapeInd = shapeInd+1; + end + end + + c_lfspd = [leftLeafSpeed; rightLeafSpeed]; +else + + i = sort(repmat(1:(apertureInfo.totalNumOfShapes-1),1,2)); + j = sort(repmat(1:apertureInfo.totalNumOfShapes,1,2)); + j(1) = []; + j(end) = []; + + timeFac = [apertureInfo.propVMAT.beam([apertureInfo.propVMAT.beam.DAOBeam]).timeFac]'; + timeFac(1) = []; + timeFac(end) = []; + %timeFac(timeFac == 0) = []; + + timeFacMatrix = sparse(i,j,timeFac,(apertureInfo.totalNumOfShapes-1),apertureInfo.totalNumOfShapes); + timeBNOptAngles = timeFacMatrix*timeDAOBorderAngles; + + % values of average leaf speeds of optimized gantry angles + c_lfspd = reshape([abs(diff(reshape(leftLeafPos,apertureInfo.beam(1).numOfActiveLeafPairs,apertureInfo.totalNumOfShapes),1,2)) ... + abs(diff(reshape(rightLeafPos,apertureInfo.beam(1).numOfActiveLeafPairs,apertureInfo.totalNumOfShapes),1,2))]./ ... + repmat(timeBNOptAngles',apertureInfo.beam(1).numOfActiveLeafPairs,2),2*apertureInfo.beam(1).numOfActiveLeafPairs*numel(timeBNOptAngles),1); +end + +% values of doserate (MU/sec) in an arc surrounding the optimized angles +weights = apertureInfoVec(1:(apertureInfo.totalNumOfShapes))./apertureInfo.jacobiScale; +c_dosrt = apertureInfo.weightToMU.*weights./timeDoseBorderAngles; + +% concatenate %Maybe do in a way we can call the superclass above? +c = [c_dos_dao; c_lfspd; c_dosrt]; + + diff --git a/matRad/optimization/@matRad_OptimizationProblemVMAT/matRad_constraintJacobian.m b/matRad/optimization/@matRad_OptimizationProblemVMAT/matRad_constraintJacobian.m new file mode 100644 index 000000000..08b667b1a --- /dev/null +++ b/matRad/optimization/@matRad_OptimizationProblemVMAT/matRad_constraintJacobian.m @@ -0,0 +1,283 @@ +function jacob = matRad_constraintJacobian(optiProb,apertureInfoVec,dij,cst) +% matRad IPOPT callback: jacobian function for VMAT optimization +% +% call +% jacob = matRad_constraintJacobian(optiProb,apertureInfoVec,dij,cst) +% +% input +% apertureInfoVec: aperture info vector +% dij: dose influence matrix +% cst: matRad cst struct +% +% output +% jacob: jacobian of constraint function +% +% References +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2015 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSES.txt. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% update apertureInfo, bixel weight vector an mapping of leafes to bixels +if ~isequal(apertureInfoVec,optiProb.apertureInfo.apertureVector) + optiProb.apertureInfo = optiProb.matRad_daoVec2ApertureInfo(optiProb.apertureInfo,apertureInfoVec); +end +apertureInfo = optiProb.apertureInfo; + +% jacobian of the dao constraints + +% row indices +i = repmat(1:apertureInfo.totalNumOfLeafPairs,1,2); +% column indices +j = [(apertureInfo.totalNumOfShapes+1):(apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs) ... + ((apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs)+1):(apertureInfo.totalNumOfShapes+2*apertureInfo.totalNumOfLeafPairs)]; + +% -1 for left leaves, 1 for right leaves +s = [-1*ones(1,apertureInfo.totalNumOfLeafPairs) ones(1,apertureInfo.totalNumOfLeafPairs)]; + +jacob_dao = sparse(i,j,s, ... + apertureInfo.totalNumOfLeafPairs, ... + numel(apertureInfoVec), ... + 2*apertureInfo.totalNumOfLeafPairs); + +% compute jacobian of dosimetric constrainst + +% dosimetric jacobian in bixel space +jacob_dos_bixel = matRad_constraintJacobian@matRad_OptimizationProblem(optiProb,apertureInfo.bixelWeights,dij,cst); + +if ~isempty(jacob_dos_bixel) + %Use pre-computed bixelAperture-Jacobian + jacob_dos = jacob_dos_bixel*apertureInfo.bixelJApVec'; +else + jacob_dos = sparse(0,0); +end + +%VMAT +% values of times spent in an arc surrounding the optimized angles (full +% arc/dose influence arc) +timeDAOBorderAngles = apertureInfoVec(((apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2)+1):end); +timeFacCurr = [apertureInfo.propVMAT.beam([apertureInfo.propVMAT.beam.DAOBeam]).timeFacCurr]'; +timeDoseBorderAngles = timeDAOBorderAngles.*timeFacCurr; + +if apertureInfo.continuousAperture + timeFac = [apertureInfo.propVMAT.beam.timeFac]'; + deleteInd = timeFac == 0; + timeFac(deleteInd) = []; + + i = [apertureInfo.propVMAT.beam.timeFacInd]'; + i(deleteInd) = []; + + j = repelem(1:apertureInfo.totalNumOfShapes,1,3); + j(deleteInd) = []; + + timeFacMatrix = sparse(i,j,timeFac,max(i),apertureInfo.totalNumOfShapes); + timeBNOptAngles = timeFacMatrix*timeDAOBorderAngles; + + % set up + n = apertureInfo.beam(1).numOfActiveLeafPairs; + indInSparseVec = (1:n); + indInConVec = (1:n); + shapeInd = 1; + + % sparse matrix + numElem = n.*(apertureInfo.propVMAT.numLeafSpeedConstraintDAO*6+(apertureInfo.propVMAT.numLeafSpeedConstraint-apertureInfo.propVMAT.numLeafSpeedConstraintDAO)*8); + i_sparse = zeros(numElem,1); + j_sparse = zeros(numElem,1); + s_sparse = zeros(numElem,1); + + for i = 1:numel(apertureInfo.beam) + % loop over beams + + if ~isempty(apertureInfo.propVMAT.beam(i).leafConstMask) + + % get vector indices + if apertureInfo.propVMAT.beam(i).DAOBeam + % if it's a DAO beam, use own vector offset + vectorIx_LI = apertureInfo.beam(i).shape(1).vectorOffset(1) + ((1:n)-1); + vectorIx_LF = apertureInfo.beam(i).shape(1).vectorOffset(2) + ((1:n)-1); + else + % otherwise, use vector offset of previous and next + % beams + vectorIx_LI = apertureInfo.beam(apertureInfo.propVMAT.beam(i).lastDAOIndex).shape(1).vectorOffset(2) + ((1:n)-1); + vectorIx_LF = apertureInfo.beam(apertureInfo.propVMAT.beam(i).nextDAOIndex).shape(1).vectorOffset(1) + ((1:n)-1); + end + vectorIx_RI = vectorIx_LI+apertureInfo.totalNumOfLeafPairs; + vectorIx_RF = vectorIx_LF+apertureInfo.totalNumOfLeafPairs; + + % extract leaf positions, time + leftLeafPos_I = apertureInfoVec(vectorIx_LI); + rightLeafPos_I = apertureInfoVec(vectorIx_RI); + leftLeafPos_F = apertureInfoVec(vectorIx_LF); + rightLeafPos_F = apertureInfoVec(vectorIx_RF); + t = timeBNOptAngles(shapeInd); + + % calc diffs + leftLeafDiff = leftLeafPos_F-leftLeafPos_I; + rightLeafDiff = rightLeafPos_F-rightLeafPos_I; + + % calc jacobs + + % wrt initial leaf positions (left, then right) + i_sparse(indInSparseVec) = indInConVec; + j_sparse(indInSparseVec) = vectorIx_LI; + s_sparse(indInSparseVec) = -sign(leftLeafDiff)./t; + indInSparseVec = indInSparseVec+n; + + i_sparse(indInSparseVec) = indInConVec+apertureInfo.propVMAT.numLeafSpeedConstraint*apertureInfo.beam(1).numOfActiveLeafPairs; + j_sparse(indInSparseVec) = vectorIx_RI; + s_sparse(indInSparseVec) = -sign(rightLeafDiff)./t; + indInSparseVec = indInSparseVec+n; + + % wrt final leaf positions (left, then right) + i_sparse(indInSparseVec) = indInConVec; + j_sparse(indInSparseVec) = vectorIx_LF; + s_sparse(indInSparseVec) = sign(leftLeafDiff)./t; + indInSparseVec = indInSparseVec+n; + + i_sparse(indInSparseVec) = indInConVec+apertureInfo.propVMAT.numLeafSpeedConstraint*apertureInfo.beam(1).numOfActiveLeafPairs; + j_sparse(indInSparseVec) = vectorIx_RF; + s_sparse(indInSparseVec) = sign(rightLeafDiff)./t; + indInSparseVec = indInSparseVec+n; + + % wrt time (left, then right) + % how we do this depends on if it's a DAO beam or + % not + if apertureInfo.propVMAT.beam(i).DAOBeam + % if it is, then speeds only depend on its own + % time + i_sparse(indInSparseVec) = indInConVec; + j_sparse(indInSparseVec) = apertureInfo.propVMAT.beam(i).timeInd; + s_sparse(indInSparseVec) = -apertureInfo.propVMAT.beam(i).timeFac(2).*abs(leftLeafDiff)./(t.^2); + indInSparseVec = indInSparseVec+n; + + i_sparse(indInSparseVec) = indInConVec+apertureInfo.propVMAT.numLeafSpeedConstraint*apertureInfo.beam(1).numOfActiveLeafPairs; + j_sparse(indInSparseVec) = apertureInfo.propVMAT.beam(i).timeInd; + s_sparse(indInSparseVec) = -apertureInfo.propVMAT.beam(i).timeFac(2).*abs(rightLeafDiff)./(t.^2); + indInSparseVec = indInSparseVec+n; + + else + % otherwise, speed depends on time of DAO + % before and DAO after + + % before + i_sparse(indInSparseVec) = indInConVec; + j_sparse(indInSparseVec) = apertureInfo.propVMAT.beam(apertureInfo.propVMAT.beam(i).lastDAOIndex).timeInd; + s_sparse(indInSparseVec) = -apertureInfo.propVMAT.beam(apertureInfo.propVMAT.beam(i).lastDAOIndex).timeFac(3).*abs(leftLeafDiff)./(t.^2); + indInSparseVec = indInSparseVec+n; + + i_sparse(indInSparseVec) = indInConVec+apertureInfo.propVMAT.numLeafSpeedConstraint*apertureInfo.beam(1).numOfActiveLeafPairs; + j_sparse(indInSparseVec) = apertureInfo.propVMAT.beam(apertureInfo.propVMAT.beam(i).lastDAOIndex).timeInd; + s_sparse(indInSparseVec) = -apertureInfo.propVMAT.beam(apertureInfo.propVMAT.beam(i).lastDAOIndex).timeFac(3).*abs(rightLeafDiff)./(t.^2); + indInSparseVec = indInSparseVec+n; + + % after + i_sparse(indInSparseVec) = indInConVec; + j_sparse(indInSparseVec) = apertureInfo.propVMAT.beam(apertureInfo.propVMAT.beam(i).nextDAOIndex).timeInd; + s_sparse(indInSparseVec) = -apertureInfo.propVMAT.beam(apertureInfo.propVMAT.beam(i).nextDAOIndex).timeFac(1).*abs(leftLeafDiff)./(t.^2); + indInSparseVec = indInSparseVec+n; + + i_sparse(indInSparseVec) = indInConVec+apertureInfo.propVMAT.numLeafSpeedConstraint*apertureInfo.beam(1).numOfActiveLeafPairs; + j_sparse(indInSparseVec) = apertureInfo.propVMAT.beam(apertureInfo.propVMAT.beam(i).nextDAOIndex).timeInd; + s_sparse(indInSparseVec) = -apertureInfo.propVMAT.beam(apertureInfo.propVMAT.beam(i).nextDAOIndex).timeFac(1).*abs(rightLeafDiff)./(t.^2); + indInSparseVec = indInSparseVec+n; + + end + + % update offset + indInConVec = indInConVec+n; + + % increment shapeInd only for beams which have transtion + % defined + shapeInd = shapeInd+1; + end + end + + jacob_lfspd = sparse(i_sparse,j_sparse,s_sparse,2*apertureInfo.beam(1).numOfActiveLeafPairs*apertureInfo.propVMAT.numLeafSpeedConstraint,numel(apertureInfoVec)); + +else + + % get index values for the jacobian + % variable index + % value of constraints for leaves + leftLeafPos = apertureInfoVec((1:apertureInfo.totalNumOfLeafPairs)+apertureInfo.totalNumOfShapes); + rightLeafPos = apertureInfoVec(1+apertureInfo.totalNumOfLeafPairs+apertureInfo.totalNumOfShapes:apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2); + + i = sort(repmat(1:(apertureInfo.totalNumOfShapes-1),1,2)); + j = sort(repmat(1:apertureInfo.totalNumOfShapes,1,2)); + j(1) = []; + j(end) = []; + + timeFac = [apertureInfo.propVMAT.beam([apertureInfo.propVMAT.beam.DAOBeam]).timeFac]'; + timeFac(1) = []; + timeFac(end) = []; + %timeFac(timeFac == 0) = []; + + timeFacMatrix = sparse(i,j,timeFac,(apertureInfo.totalNumOfShapes-1),apertureInfo.totalNumOfShapes); + timeBNOptAngles = timeFacMatrix*timeDAOBorderAngles; + + currentLeftLeafInd = (apertureInfo.totalNumOfShapes+1):(apertureInfo.totalNumOfShapes+apertureInfo.beam(1).numOfActiveLeafPairs*numel(timeBNOptAngles)); + currentRightLeafInd = (apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs+1):(apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs+apertureInfo.beam(1).numOfActiveLeafPairs*numel(timeBNOptAngles)); + nextLeftLeafInd = (apertureInfo.beam(1).numOfActiveLeafPairs+apertureInfo.totalNumOfShapes+1):(apertureInfo.beam(1).numOfActiveLeafPairs+apertureInfo.totalNumOfShapes+apertureInfo.beam(1).numOfActiveLeafPairs*numel(timeBNOptAngles)); + nextRightLeafInd = (apertureInfo.beam(1).numOfActiveLeafPairs+apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs+1):(apertureInfo.beam(1).numOfActiveLeafPairs+apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs+apertureInfo.beam(1).numOfActiveLeafPairs*numel(timeBNOptAngles)); + leftTimeInd = kron(j,ones(1,apertureInfo.beam(1).numOfActiveLeafPairs))+apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2; + rightTimeInd = kron(j,ones(1,apertureInfo.beam(1).numOfActiveLeafPairs))+apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2; + %leftTimeInd = repelem(j,apertureInfo.beam(1).numOfActiveLeafPairs)+apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2; + %rightTimeInd = repelem(j,apertureInfo.beam(1).numOfActiveLeafPairs)+apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2; + % constraint index + constraintInd = 1:2*apertureInfo.beam(1).numOfActiveLeafPairs*numel(timeBNOptAngles); + + + % jacobian of the leafspeed constraint + i = repmat((i'-1)*apertureInfo.beam(1).numOfActiveLeafPairs,1,apertureInfo.beam(1).numOfActiveLeafPairs)+repmat(1:apertureInfo.beam(1).numOfActiveLeafPairs,2*numel(timeBNOptAngles),1); + i = reshape([i' i'+apertureInfo.beam(1).numOfActiveLeafPairs*numel(timeBNOptAngles)],1,[]); + + i = [repmat(constraintInd,1,2) i]; + j = [currentLeftLeafInd currentRightLeafInd nextLeftLeafInd nextRightLeafInd leftTimeInd rightTimeInd]; + % first do jacob wrt current leaf position (left, right), then next leaf + % position (left, right), then time (left, right) + j_lfspd_cur = -reshape([sign(diff(reshape(leftLeafPos,apertureInfo.beam(1).numOfActiveLeafPairs,apertureInfo.totalNumOfShapes),1,2)) ... + sign(diff(reshape(rightLeafPos,apertureInfo.beam(1).numOfActiveLeafPairs,apertureInfo.totalNumOfShapes),1,2))]./ ... + repmat(timeBNOptAngles',apertureInfo.beam(1).numOfActiveLeafPairs,2),2*apertureInfo.beam(1).numOfActiveLeafPairs*numel(timeBNOptAngles),1); + + j_lfspd_nxt = reshape([sign(diff(reshape(leftLeafPos,apertureInfo.beam(1).numOfActiveLeafPairs,apertureInfo.totalNumOfShapes),1,2)) ... + sign(diff(reshape(rightLeafPos,apertureInfo.beam(1).numOfActiveLeafPairs,apertureInfo.totalNumOfShapes),1,2))]./ ... + repmat(timeBNOptAngles',apertureInfo.beam(1).numOfActiveLeafPairs,2),2*apertureInfo.beam(1).numOfActiveLeafPairs*numel(timeBNOptAngles),1); + + j_lfspd_t = -reshape([kron(abs(diff(reshape(leftLeafPos,apertureInfo.beam(1).numOfActiveLeafPairs,apertureInfo.totalNumOfShapes),1,2)),ones(1,2)).*repmat(timeFac',apertureInfo.beam(1).numOfActiveLeafPairs,1) ... + kron(abs(diff(reshape(rightLeafPos,apertureInfo.beam(1).numOfActiveLeafPairs,apertureInfo.totalNumOfShapes),1,2)),ones(1,2)).*repmat(timeFac',apertureInfo.beam(1).numOfActiveLeafPairs,1)]./ ... + repmat(kron((timeBNOptAngles.^2)',ones(1,2)),apertureInfo.beam(1).numOfActiveLeafPairs,2),[],1); + + s = [j_lfspd_cur; j_lfspd_nxt; j_lfspd_t]; + + jacob_lfspd = sparse(i,j,s,2*apertureInfo.beam(1).numOfActiveLeafPairs*(apertureInfo.totalNumOfShapes-1),numel(apertureInfoVec),numel(s)); +end + +% jacobian of the doserate constraint +% values of doserate (MU/sec) between optimized gantry angles +weights = apertureInfoVec(1:(apertureInfo.totalNumOfShapes))./apertureInfo.jacobiScale; + +i = repmat(1:(apertureInfo.totalNumOfShapes),1,2); +j = [1:(apertureInfo.totalNumOfShapes) ... + ((apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2)+1):((apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2)+apertureInfo.totalNumOfShapes)]; +% first do jacob wrt weights, then wrt times + +s = [apertureInfo.weightToMU./(timeDoseBorderAngles.*apertureInfo.jacobiScale); -apertureInfo.weightToMU.*weights.*timeFacCurr./(timeDoseBorderAngles.^2)]; + +jacob_dosrt = sparse(i,j,s,apertureInfo.totalNumOfShapes,numel(apertureInfoVec),2*apertureInfo.totalNumOfShapes); + + +% concatenate +jacob = [jacob_dos; jacob_dao; jacob_lfspd; jacob_dosrt]; + + diff --git a/matRad/optimization/@matRad_OptimizationProblemVMAT/matRad_daoApertureInfo2Vec.m b/matRad/optimization/@matRad_OptimizationProblemVMAT/matRad_daoApertureInfo2Vec.m new file mode 100644 index 000000000..d6bb721db --- /dev/null +++ b/matRad/optimization/@matRad_OptimizationProblemVMAT/matRad_daoApertureInfo2Vec.m @@ -0,0 +1,172 @@ +function [apertureInfoVec, mappingMx, limMx] = matRad_daoApertureInfo2Vec(apertureInfo) +% matRad function to generate a vector respresentation of the aperture +% weights and shapes and (optional) some meta information needed during +% optimization +% +% call +% [apertureInfoVec, mappingMx, limMx] = matRad_daoApertureInfo2Vec(apertureInfo) +% +% input +% apertureInfo: aperture weight and shape info struct +% +% output +% apertureInfoVec: vector respresentation of the apertue weights and shapes +% mappingMx: mapping of vector components to beams, shapes and leaves +% limMx: bounds on vector components, i.e., minimum and maximum +% aperture weights (0/inf) and leav positions (custom) +% +% References +% +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2015 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSES.txt. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% function to create a single vector for the direct aperature optimization +% first: aperature weights +% second: left leaf positions +% third: right leaf positions +% fourth (VMAT only): times between successive DAO gantry angles + +% initializing variables + +vecLength = (apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2); + +if apertureInfo.runVMAT + vecLength = vecLength+apertureInfo.totalNumOfShapes; %Extra set of (apertureInfo.totalNumOfShapes) number of elements, allowing arc sector times to be optimized +end + +apertureInfoVec = NaN * ones(vecLength,1); + +offset = 0; + +%% 1. aperture weights +for i = 1:size(apertureInfo.beam,2) + for j = 1:apertureInfo.beam(i).numOfShapes + + apertureInfoVec(offset+j) = apertureInfo.beam(i).shape(j).jacobiScale*apertureInfo.beam(i).shape(j).weight; %In VMAT, this weight is "spread" over unoptimized beams (assume constant dose rate over sector) + + end + offset = offset + apertureInfo.beam(i).numOfShapes; +end + +% 2. left and right leaf positions +%% fill the vector for all shapes of all beams +for i = 1:size(apertureInfo.beam,2) + for j = 1:apertureInfo.beam(i).numOfShapes + + if ~apertureInfo.runVMAT || ~apertureInfo.continuousAperture + + apertureInfoVec(offset+[1:apertureInfo.beam(i).numOfActiveLeafPairs]) = apertureInfo.beam(i).shape(j).leftLeafPos; + apertureInfoVec(offset+[1:apertureInfo.beam(i).numOfActiveLeafPairs]+apertureInfo.totalNumOfLeafPairs) = apertureInfo.beam(i).shape(j).rightLeafPos; + + offset = offset + apertureInfo.beam(i).numOfActiveLeafPairs; + else + + if apertureInfo.propVMAT.beam(i).doseAngleDAO(1) + apertureInfoVec(offset+[1:apertureInfo.beam(i).numOfActiveLeafPairs]) = apertureInfo.beam(i).shape(j).leftLeafPos_I; + apertureInfoVec(offset+[1:apertureInfo.beam(i).numOfActiveLeafPairs]+apertureInfo.totalNumOfLeafPairs) = apertureInfo.beam(i).shape(j).rightLeafPos_I; + + offset = offset + apertureInfo.beam(i).numOfActiveLeafPairs; + end + + if apertureInfo.propVMAT.beam(i).doseAngleDAO(2) + apertureInfoVec(offset+[1:apertureInfo.beam(i).numOfActiveLeafPairs]) = apertureInfo.beam(i).shape(j).leftLeafPos_F; + apertureInfoVec(offset+[1:apertureInfo.beam(i).numOfActiveLeafPairs]+apertureInfo.totalNumOfLeafPairs) = apertureInfo.beam(i).shape(j).rightLeafPos_F; + + offset = offset + apertureInfo.beam(i).numOfActiveLeafPairs; + end + end + + end +end +%% 3. time of arc sector/beam +if apertureInfo.runVMAT + offset = offset + apertureInfo.totalNumOfLeafPairs; + + %this gives a vector of the arc lengths belonging to each optimized CP + %unique gets rid of double-counted angles (which is every interior + %angle) + + optInd = [apertureInfo.propVMAT.beam.DAOBeam]; + optAngleLengths = [apertureInfo.propVMAT.beam(optInd).DAOAngleBordersDiff]; + optGantryRot = [apertureInfo.beam(optInd).gantryRot]; + apertureInfoVec((offset+1):end) = optAngleLengths./optGantryRot; %entries are the times until the next opt gantry angle is reached + +end + +%% 4. create additional information for later use +if nargout > 1 + + mappingMx = NaN * ones(vecLength,4); + limMx = NaN * ones(vecLength,2); + + limMx(1:(apertureInfo.totalNumOfShapes),:) = ones((apertureInfo.totalNumOfShapes),1)*[0 inf]; + + counter = 1; + + for i = 1:numel(apertureInfo.beam) + for j = 1:apertureInfo.beam(i).numOfShapes + mappingMx(counter,1) = i; + if apertureInfo.runVMAT + + timeLimL = diff(apertureInfo.propVMAT.beam(i).DAOAngleBorders)/apertureInfo.propVMAT.constraints.gantryRotationSpeed(2); %Minimum time interval between two optimized beams/gantry angles + timeLimU = diff(apertureInfo.propVMAT.beam(i).DAOAngleBorders)/apertureInfo.propVMAT.constraints.gantryRotationSpeed(1); %Maximum time interval between two optimized beams/gantry angles + + mappingMx(counter+(apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2),1) = i; + limMx(counter+(apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2),:) = [timeLimL timeLimU]; + end + counter = counter + 1; + end + end + + shapeOffset = 0; + for i = 1:numel(apertureInfo.beam) + for j = 1:apertureInfo.beam(i).numOfShapes + for k = 1:apertureInfo.beam(i).numOfActiveLeafPairs + mappingMx(counter,1) = i; + mappingMx(counter,2) = j + shapeOffset; % store global shape number for grad calc + mappingMx(counter,3) = j; % store local shape number + mappingMx(counter,4) = k; % store local leaf number + + limMx(counter,1) = apertureInfo.beam(i).lim_l(k); + limMx(counter,2) = apertureInfo.beam(i).lim_r(k); + counter = counter + 1; + + if apertureInfo.runVMAT && apertureInfo.continuousAperture && nnz(apertureInfo.propVMAT.beam(i).doseAngleDAO) == 2 + %redo for initial and final leaf positions + %might have to revisit this after looking at gradient, + %esp. mappingMx(counter,2) + %only an issue for non-interpolated deliveries + mappingMx(counter,1) = i; + mappingMx(counter,2) = j + shapeOffset; % store global shape number for grad calc + mappingMx(counter,3) = j; % store local shape number + mappingMx(counter,4) = k; % store local leaf number + + limMx(counter,1) = apertureInfo.beam(i).lim_l(k); + limMx(counter,2) = apertureInfo.beam(i).lim_r(k); + counter = counter + 1; + end + end + end + shapeOffset = shapeOffset + apertureInfo.beam(i).numOfShapes; + end + + mappingMx(counter:(apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2),:) = mappingMx(apertureInfo.totalNumOfShapes+1:counter-1,:); + limMx(counter:(apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2),:) = limMx(apertureInfo.totalNumOfShapes+1:counter-1,:); + +end + +end + diff --git a/matRad/optimization/@matRad_OptimizationProblemVMAT/matRad_daoVec2ApertureInfo.m b/matRad/optimization/@matRad_OptimizationProblemVMAT/matRad_daoVec2ApertureInfo.m new file mode 100644 index 000000000..2b26f6f15 --- /dev/null +++ b/matRad/optimization/@matRad_OptimizationProblemVMAT/matRad_daoVec2ApertureInfo.m @@ -0,0 +1,481 @@ +function updatedInfo = matRad_daoVec2ApertureInfo(apertureInfo,apertureInfoVect) +% matRad function to translate the vector representation of the aperture +% shape and weight into an aperture info struct. At the same time, the +% updated bixel weight vector w is computed and a vector listing the +% correspondence between leaf tips and bixel indices for gradient +% calculation +% +% call +% [updatedInfo,w,indVect] = matRad_daoVec2ApertureInfo(apertureInfo,apertureInfoVect) +% +% input +% apertureInfo: aperture shape info struct +% apertureInfoVect: aperture weights and shapes parameterized as vector +% +% output +% updatedInfo: updated aperture shape info struct according to apertureInfoVect +% +% References +% +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2015, Mark Bangert, on behalf of the matRad development team +% +% m.bangert@dkfz.de +% +% This file is part of matRad. +% +% matrad is free software: you can redistribute it and/or modify it under +% the terms of the GNU General Public License as published by the Free +% Software Foundation, either version 3 of the License, or (at your option) +% any later version. +% +% matRad is distributed in the hope that it will be useful, but WITHOUT ANY +% WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +% FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +% details. +% +% You should have received a copy of the GNU General Public License in the +% file license.txt along with matRad. If not, see +% . +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% function to update the apertureInfo struct after the each iteraton of the +% optimization + +% initializing variables +updatedInfo = apertureInfo; + +updatedInfo.apertureVector = apertureInfoVect; + +if ~updatedInfo.runVMAT + shapeInd = 1; + + indVect = NaN*ones(2*apertureInfo.doseTotalNumOfLeafPairs,1); + offset = 0; + + % helper function to cope with numerical instabilities through rounding + round2 = @(a,b) round(a*10^b)/10^b; +else + + % options for bixel and Jacobian calculation + mlcOptions.bixelWidth = apertureInfo.bixelWidth; + calcOptions.continuousAperture = updatedInfo.continuousAperture; + vectorIndices.totalNumOfShapes = apertureInfo.totalNumOfShapes; +end + +w = zeros(apertureInfo.totalNumOfBixels,1); + +if updatedInfo.runVMAT && ~all([updatedInfo.propVMAT.beam.DAOBeam]) + j = 1; + for i = 1:numel(updatedInfo.beam) + if updatedInfo.propVMAT.beam(i).DAOBeam + % update the shape weight + % rescale the weight from the vector using the previous + % iteration scaling factor + updatedInfo.beam(i).shape(j).weight = apertureInfoVect(updatedInfo.beam(i).shape(j).weightOffset)./updatedInfo.beam(i).shape(j).jacobiScale; + + updatedInfo.beam(i).shape(j).MU = updatedInfo.beam(i).shape(j).weight*updatedInfo.weightToMU; + updatedInfo.beam(i).time = apertureInfoVect((updatedInfo.totalNumOfShapes+updatedInfo.totalNumOfLeafPairs*2)+updatedInfo.propVMAT.beam(i).DAOIndex)*updatedInfo.propVMAT.beam(i).timeFacCurr; + updatedInfo.beam(i).gantryRot = updatedInfo.propVMAT.beam(i).doseAngleBordersDiff/updatedInfo.beam(i).time; + updatedInfo.beam(i).shape(j).MURate = updatedInfo.beam(i).shape(j).MU./updatedInfo.beam(i).time; + end + end +end + +if updatedInfo.runVMAT + %% ONLY SUPPORTED IN VMAT FOR NOW + + % Jacobian matrix to be used in the DAO gradient function + % this tells us the gradient of a particular bixel with respect to an + % element in the apertureVector (aperture weight or leaf position) + % store as a vector for now, convert to sparse matrix later + + optBixelFactor = 7; + % For optimized beams: 5 = (1 from weights) + (3 from left leaf positions (I, M, and F)) + (3 from + % right leaf positions (I, M, and F)) + + if updatedInfo.runVMAT + intBixelFactor = 2*optBixelFactor+2; + % For interpolated beams: multiply this number times 2 (influenced by the + % one before and the one after), then add 2 (influenced by the time of the + % times before and after) + else + intBixelFactor = 2*optBixelFactor; + % For interpolated beams: multiply this number times 2 (influenced by the + % one before and the one after) + end + + % for the time (probability) gradients + optBixelFactor = optBixelFactor+apertureInfo.totalNumOfShapes; + intBixelFactor = intBixelFactor+apertureInfo.totalNumOfShapes; + + bixelJApVec_sz = (updatedInfo.totalNumOfOptBixels*optBixelFactor+(updatedInfo.totalNumOfBixels-updatedInfo.totalNumOfOptBixels)*intBixelFactor)*2; + + bixelJApVec_vec = zeros(1,bixelJApVec_sz); + + % vector indices + bixelJApVec_i = nan(1,bixelJApVec_sz); + % bixel indices + bixelJApVec_j = zeros(1,bixelJApVec_sz); + % offset + bixelJApVec_offset = 0; +end + +%% update the shapeMaps +% here the new colimator positions are used to create new shapeMaps that +% now include decimal values instead of binary + +calcOptions.saveJacobian = true; + +% loop over all beams +for i = 1:numel(updatedInfo.beam) + + %posOfRightCornerPixel = apertureInfo.beam(i).posOfCornerBixel(1) + (size(apertureInfo.beam(i).bixelIndMap,2)-1)*apertureInfo.bixelWidth; + + % pre compute left and right bixel edges + edges_l = updatedInfo.beam(i).posOfCornerBixel(1)... + + ((1:size(apertureInfo.beam(i).bixelIndMap,2))-1-1/2)*updatedInfo.bixelWidth; + edges_r = updatedInfo.beam(i).posOfCornerBixel(1)... + + ((1:size(apertureInfo.beam(i).bixelIndMap,2))-1+1/2)*updatedInfo.bixelWidth; + + % get dimensions of 2d matrices that store shape/bixel information + n = apertureInfo.beam(i).numOfActiveLeafPairs; + + % loop over all shapes + if updatedInfo.runVMAT + numOfShapes = 1; + calcOptions.DAOBeam = updatedInfo.propVMAT.beam(i).DAOBeam; + else + numOfShapes = updatedInfo.beam(i).numOfShapes; + end + + mlcOptions.lim_l = apertureInfo.beam(i).lim_l; + mlcOptions.lim_r = apertureInfo.beam(i).lim_r; + mlcOptions.edges_l = edges_l; + mlcOptions.edges_r = edges_r; + mlcOptions.centres = (edges_l+edges_r)/2; + mlcOptions.widths = edges_r-edges_l; + mlcOptions.n = n; + mlcOptions.numBix = size(apertureInfo.beam(i).bixelIndMap,2); + mlcOptions.bixelIndMap = apertureInfo.beam(i).bixelIndMap; + + for j = 1:numOfShapes + + if ~updatedInfo.runVMAT || updatedInfo.propVMAT.beam(i).DAOBeam + % either this is not VMAT, or if it is VMAT, this is a DAO beam + + % update the shape weight + updatedInfo.beam(i).shape(j).weight = apertureInfoVect(updatedInfo.beam(i).shape(j).weightOffset)./updatedInfo.beam(i).shape(j).jacobiScale; + + if updatedInfo.runVMAT + updatedInfo.beam(i).shape(j).MU = updatedInfo.beam(i).shape(j).weight*updatedInfo.weightToMU; + updatedInfo.beam(i).time = apertureInfoVect((updatedInfo.totalNumOfShapes+updatedInfo.totalNumOfLeafPairs*2)+updatedInfo.propVMAT.beam(i).DAOIndex)*updatedInfo.propVMAT.beam(i).timeFacCurr; + updatedInfo.beam(i).gantryRot = updatedInfo.propVMAT.beam(i).doseAngleBordersDiff/updatedInfo.beam(i).time; + updatedInfo.beam(i).shape(j).MURate = updatedInfo.beam(i).shape(j).MU./updatedInfo.beam(i).time; + end + + if ~updatedInfo.runVMAT || ~updatedInfo.continuousAperture + % extract left and right leaf positions from shape vector + vectorIx_L = updatedInfo.beam(i).shape(j).vectorOffset + ((1:n)-1); + vectorIx_R = vectorIx_L+apertureInfo.totalNumOfLeafPairs; + leftLeafPos = apertureInfoVect(vectorIx_L); + rightLeafPos = apertureInfoVect(vectorIx_R); + + % update information in shape structure + updatedInfo.beam(i).shape(j).leftLeafPos = leftLeafPos; + updatedInfo.beam(i).shape(j).leftLeafPos_I = leftLeafPos; + updatedInfo.beam(i).shape(j).leftLeafPos_F = leftLeafPos; + updatedInfo.beam(i).shape(j).rightLeafPos = rightLeafPos; + updatedInfo.beam(i).shape(j).rightLeafPos_I = rightLeafPos; + updatedInfo.beam(i).shape(j).rightLeafPos_F = rightLeafPos; + else + % extract left and right leaf positions from shape vector + vectorIx_LI = updatedInfo.beam(i).shape(j).vectorOffset(1) + ((1:n)-1); + vectorIx_RI = vectorIx_LI+apertureInfo.totalNumOfLeafPairs; + leftLeafPos_I = apertureInfoVect(vectorIx_LI); + rightLeafPos_I = apertureInfoVect(vectorIx_RI); + + vectorIx_LF = updatedInfo.beam(i).shape(j).vectorOffset(2) + ((1:n)-1); + vectorIx_RF = vectorIx_LF+apertureInfo.totalNumOfLeafPairs; + leftLeafPos_F = apertureInfoVect(vectorIx_LF); + rightLeafPos_F = apertureInfoVect(vectorIx_RF); + + % update information in shape structure + updatedInfo.beam(i).shape(j).leftLeafPos_I = leftLeafPos_I; + updatedInfo.beam(i).shape(j).rightLeafPos_I = rightLeafPos_I; + + updatedInfo.beam(i).shape(j).leftLeafPos_F = leftLeafPos_F; + updatedInfo.beam(i).shape(j).rightLeafPos_F = rightLeafPos_F; + end + + else + % this is an interpolated beam + + %MURate is interpolated between MURates of optimized apertures + updatedInfo.beam(i).gantryRot = 1./(updatedInfo.propVMAT.beam(i).timeFracFromLastDAO./updatedInfo.beam(updatedInfo.propVMAT.beam(i).lastDAOIndex).gantryRot+updatedInfo.propVMAT.beam(i).timeFracFromNextDAO./updatedInfo.beam(updatedInfo.propVMAT.beam(i).nextDAOIndex).gantryRot); + updatedInfo.beam(i).time = updatedInfo.propVMAT.beam(i).doseAngleBordersDiff./updatedInfo.beam(i).gantryRot; + updatedInfo.beam(i).shape(j).MURate = updatedInfo.propVMAT.beam(i).fracFromLastDAO*updatedInfo.beam(updatedInfo.propVMAT.beam(i).lastDAOIndex).shape(j).MURate+(1-updatedInfo.propVMAT.beam(i).fracFromLastDAO)*updatedInfo.beam(updatedInfo.propVMAT.beam(i).nextDAOIndex).shape(j).MURate; + + % calculate MU, weight + updatedInfo.beam(i).shape(j).MU = updatedInfo.beam(i).shape(j).MURate.*updatedInfo.beam(i).time; + updatedInfo.beam(i).shape(j).weight = updatedInfo.beam(i).shape(j).MU./updatedInfo.weightToMU; + + if ~updatedInfo.continuousAperture + + fracFromLastOpt = updatedInfo.propVMAT.beam(i).fracFromLastDAO; + fracFromLastOptI = updatedInfo.propVMAT.beam(i).fracFromLastDAO*ones(n,1); + fracFromLastOptF = updatedInfo.propVMAT.beam(i).fracFromLastDAO*ones(n,1); + fracFromNextOptI = (1-updatedInfo.propVMAT.beam(i).fracFromLastDAO)*ones(n,1); + fracFromNextOptF = (1-updatedInfo.propVMAT.beam(i).fracFromLastDAO)*ones(n,1); + + % obtain leaf positions at last DAO beam + vectorIx_LF_last = updatedInfo.beam(updatedInfo.propVMAT.beam(i).lastDAOIndex).shape(j).vectorOffset + ((1:n)-1); + vectorIx_RF_last = vectorIx_LF_last+apertureInfo.totalNumOfLeafPairs; + leftLeafPos_last = apertureInfoVect(vectorIx_LF_last); + rightLeafPos_last = apertureInfoVect(vectorIx_RF_last); + + % obtain leaf positions at next DAO beam + vectorIx_LI_next = updatedInfo.beam(updatedInfo.propVMAT.beam(i).nextDAOIndex).shape(j).vectorOffset + ((1:n)-1); + vectorIx_RI_next = vectorIx_LI_next+apertureInfo.totalNumOfLeafPairs; + leftLeafPos_next = apertureInfoVect(vectorIx_LI_next); + rightLeafPos_next = apertureInfoVect(vectorIx_RI_next); + + % interpolate leaf positions + leftLeafPos = updatedInfo.propVMAT.beam(i).fracFromLastDAO*leftLeafPos_last+(1-updatedInfo.propVMAT.beam(i).fracFromLastDAO)*leftLeafPos_next; + rightLeafPos = updatedInfo.propVMAT.beam(i).fracFromLastDAO*rightLeafPos_last+(1-updatedInfo.propVMAT.beam(i).fracFromLastDAO)*rightLeafPos_next; + + % update information in shape structure + updatedInfo.beam(i).shape(j).leftLeafPos = leftLeafPos; + updatedInfo.beam(i).shape(j).leftLeafPos_I = leftLeafPos; + updatedInfo.beam(i).shape(j).leftLeafPos_F = leftLeafPos; + updatedInfo.beam(i).shape(j).rightLeafPos = rightLeafPos; + updatedInfo.beam(i).shape(j).rightLeafPos_I = rightLeafPos; + updatedInfo.beam(i).shape(j).rightLeafPos_F = rightLeafPos; + else + + fracFromLastOpt = updatedInfo.propVMAT.beam(i).fracFromLastDAO; + fracFromLastOptI = updatedInfo.propVMAT.beam(i).fracFromLastDAO_I*ones(n,1); + fracFromLastOptF = updatedInfo.propVMAT.beam(i).fracFromLastDAO_F*ones(n,1); + fracFromNextOptI = updatedInfo.propVMAT.beam(i).fracFromNextDAO_I*ones(n,1); + fracFromNextOptF = updatedInfo.propVMAT.beam(i).fracFromNextDAO_F*ones(n,1); + + % obtain leaf positions at last DAO beam + vectorIx_LF_last = updatedInfo.beam(updatedInfo.propVMAT.beam(i).lastDAOIndex).shape(j).vectorOffset(2) + ((1:n)-1); + vectorIx_RF_last = vectorIx_LF_last+apertureInfo.totalNumOfLeafPairs; + leftLeafPos_F_last = apertureInfoVect(vectorIx_LF_last); + rightLeafPos_F_last = apertureInfoVect(vectorIx_RF_last); + + % obtain leaf positions at next DAO beam + vectorIx_LI_next = updatedInfo.beam(updatedInfo.propVMAT.beam(i).nextDAOIndex).shape(j).vectorOffset(1) + ((1:n)-1); + vectorIx_RI_next = vectorIx_LI_next+apertureInfo.totalNumOfLeafPairs; + leftLeafPos_I_next = apertureInfoVect(vectorIx_LI_next); + rightLeafPos_I_next = apertureInfoVect(vectorIx_RI_next); + + % interpolate leaf positions + updatedInfo.beam(i).shape(j).leftLeafPos_I = fracFromLastOptI.*leftLeafPos_F_last+fracFromNextOptI.*leftLeafPos_I_next; + updatedInfo.beam(i).shape(j).rightLeafPos_I = fracFromLastOptI.*rightLeafPos_F_last+fracFromNextOptI.*rightLeafPos_I_next; + + updatedInfo.beam(i).shape(j).leftLeafPos_F = fracFromLastOptF.*leftLeafPos_F_last+fracFromNextOptF.*leftLeafPos_I_next; + updatedInfo.beam(i).shape(j).rightLeafPos_F = fracFromLastOptF.*rightLeafPos_F_last+fracFromNextOptF.*rightLeafPos_I_next; + end + end + + if ~updatedInfo.runVMAT + + % rounding for numerical stability + leftLeafPos = round2(leftLeafPos,10); + rightLeafPos = round2(rightLeafPos,10); + + % check overshoot of leaf positions + leftLeafPos(leftLeafPos <= apertureInfo.beam(i).lim_l) = apertureInfo.beam(i).lim_l(leftLeafPos <= apertureInfo.beam(i).lim_l); + rightLeafPos(rightLeafPos <= apertureInfo.beam(i).lim_l) = apertureInfo.beam(i).lim_l(rightLeafPos <= apertureInfo.beam(i).lim_l); + leftLeafPos(leftLeafPos >= apertureInfo.beam(i).lim_r) = apertureInfo.beam(i).lim_r(leftLeafPos >= apertureInfo.beam(i).lim_r); + rightLeafPos(rightLeafPos >= apertureInfo.beam(i).lim_r) = apertureInfo.beam(i).lim_r(rightLeafPos >= apertureInfo.beam(i).lim_r); + + % + xPosIndLeftLeaf = round((leftLeafPos - apertureInfo.beam(i).posOfCornerBixel(1))/apertureInfo.bixelWidth + 1); + xPosIndRightLeaf = round((rightLeafPos - apertureInfo.beam(i).posOfCornerBixel(1))/apertureInfo.bixelWidth + 1); + + % + xPosIndLeftLeaf_lim = floor((apertureInfo.beam(i).lim_l - apertureInfo.beam(i).posOfCornerBixel(1))/apertureInfo.bixelWidth+1); + xPosIndRightLeaf_lim = ceil((apertureInfo.beam(i).lim_r - apertureInfo.beam(i).posOfCornerBixel(1))/apertureInfo.bixelWidth + 1); + + xPosIndLeftLeaf(xPosIndLeftLeaf <= xPosIndLeftLeaf_lim) = xPosIndLeftLeaf_lim(xPosIndLeftLeaf <= xPosIndLeftLeaf_lim)+1; + xPosIndRightLeaf(xPosIndRightLeaf >= xPosIndRightLeaf_lim) = xPosIndRightLeaf_lim(xPosIndRightLeaf >= xPosIndRightLeaf_lim)-1; + + % check limits because of rounding off issues at maximum, i.e., + % enforce round(X.5) -> X + % LeafPos can occasionally go slightly beyond lim_r, so changed + % == check to >= + xPosIndLeftLeaf(leftLeafPos >= apertureInfo.beam(i).lim_r) = round(... + .5 + (leftLeafPos(leftLeafPos >= apertureInfo.beam(i).lim_r) ... + - apertureInfo.beam(i).posOfCornerBixel(1))/apertureInfo.bixelWidth); + + xPosIndRightLeaf(rightLeafPos >= apertureInfo.beam(i).lim_r) = round(... + .5 + (rightLeafPos(rightLeafPos >= apertureInfo.beam(i).lim_r) ... + - apertureInfo.beam(i).posOfCornerBixel(1))/apertureInfo.bixelWidth); + + % find the bixel index that the leaves currently touch + bixelIndLeftLeaf = apertureInfo.beam(i).bixelIndMap((xPosIndLeftLeaf-1)*n+[1:n]'); + bixelIndRightLeaf = apertureInfo.beam(i).bixelIndMap((xPosIndRightLeaf-1)*n+[1:n]'); + + if any(isnan(bixelIndLeftLeaf)) || any(isnan(bixelIndRightLeaf)) + error('cannot map leaf position to bixel index'); + end + + % store information in index vector for gradient calculation + indVect(offset+(1:n)) = bixelIndLeftLeaf; + indVect(offset+(1:n)+apertureInfo.doseTotalNumOfLeafPairs) = bixelIndRightLeaf; + offset = offset+n; + + % calculate opening fraction for every bixel in shape to construct + % bixel weight vector + + coveredByLeftLeaf = bsxfun(@minus,leftLeafPos,edges_l) / updatedInfo.bixelWidth; + coveredByRightLeaf = bsxfun(@minus,edges_r,rightLeafPos) / updatedInfo.bixelWidth; + + tempMap = 1 - (coveredByLeftLeaf + abs(coveredByLeftLeaf)) / 2 ... + - (coveredByRightLeaf + abs(coveredByRightLeaf)) / 2; + + % find open bixels + tempMapIx = tempMap > 0; + + currBixelIx = apertureInfo.beam(i).bixelIndMap(tempMapIx); + w(currBixelIx) = w(currBixelIx) + tempMap(tempMapIx)*updatedInfo.beam(i).shape(j).weight; + + % save the tempMap (we need to apply a positivity operator !) + updatedInfo.beam(i).shape(j).shapeMap = (tempMap + abs(tempMap)) / 2; + + % increment shape index + shapeInd = shapeInd +1; + end + + end + + if updatedInfo.runVMAT + + for j = 1:numOfShapes + + % shapeMap + shapeMap = zeros(size(updatedInfo.beam(i).bixelIndMap)); + % sumGradSq + sumGradSq = 0; + + % insert variables + vectorIndices.tIx_Vec = (apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2)+(1:apertureInfo.totalNumOfShapes); + + variables.weight = updatedInfo.beam(i).shape(j).weight; + variables.leftLeafPos_I = updatedInfo.beam(i).shape(j).leftLeafPos_I; + variables.leftLeafPos_F = updatedInfo.beam(i).shape(j).leftLeafPos_F; + variables.rightLeafPos_I = updatedInfo.beam(i).shape(j).rightLeafPos_I; + variables.rightLeafPos_F = updatedInfo.beam(i).shape(j).rightLeafPos_F; + + if updatedInfo.propVMAT.beam(i).DAOBeam + + variables.jacobiScale = updatedInfo.beam(i).shape(1).jacobiScale; + + vectorIndices.DAOindex = updatedInfo.propVMAT.beam(i).DAOIndex; + if updatedInfo.continuousAperture + vectorIndices.vectorIx_LI = updatedInfo.beam(i).shape(j).vectorOffset(1) + ((1:n)-1); + vectorIndices.vectorIx_LF = updatedInfo.beam(i).shape(j).vectorOffset(2) + ((1:n)-1); + vectorIndices.vectorIx_RI = vectorIndices.vectorIx_LI+apertureInfo.totalNumOfLeafPairs; + vectorIndices.vectorIx_RF = vectorIndices.vectorIx_LF+apertureInfo.totalNumOfLeafPairs; + else + vectorIndices.vectorIx_LI = updatedInfo.beam(i).shape(j).vectorOffset + ((1:n)-1); + vectorIndices.vectorIx_LF = updatedInfo.beam(i).shape(j).vectorOffset + ((1:n)-1); + vectorIndices.vectorIx_RI = vectorIndices.vectorIx_LI+apertureInfo.totalNumOfLeafPairs; + vectorIndices.vectorIx_RF = vectorIndices.vectorIx_LF+apertureInfo.totalNumOfLeafPairs; + end + else + + variables.weight_last = updatedInfo.beam(updatedInfo.propVMAT.beam(i).lastDAOIndex).shape(j).weight; + variables.weight_next = updatedInfo.beam(updatedInfo.propVMAT.beam(i).nextDAOIndex).shape(j).weight; + + variables.jacobiScale_last = updatedInfo.beam(updatedInfo.propVMAT.beam(i).lastDAOIndex).shape(1).jacobiScale; + variables.jacobiScale_next = updatedInfo.beam(updatedInfo.propVMAT.beam(i).nextDAOIndex).shape(1).jacobiScale; + + variables.time_last = updatedInfo.beam(updatedInfo.propVMAT.beam(i).lastDAOIndex).time; + variables.time_next = updatedInfo.beam(updatedInfo.propVMAT.beam(i).nextDAOIndex).time; + variables.time = updatedInfo.beam(i).time; + + variables.fracFromLastOptI = fracFromLastOptI; + variables.fracFromLastOptF = fracFromLastOptF; + variables.fracFromNextOptI = fracFromNextOptI; + variables.fracFromNextOptF = fracFromNextOptF; + variables.fracFromLastOpt = fracFromLastOpt; + + variables.doseAngleBordersDiff = updatedInfo.propVMAT.beam(i).doseAngleBordersDiff; + variables.doseAngleBordersDiff_last = updatedInfo.propVMAT.beam(updatedInfo.propVMAT.beam(i).lastDAOIndex).doseAngleBordersDiff; + variables.doseAngleBordersDiff_next = updatedInfo.propVMAT.beam(updatedInfo.propVMAT.beam(i).nextDAOIndex).doseAngleBordersDiff; + variables.timeFacCurr_last = updatedInfo.propVMAT.beam(updatedInfo.propVMAT.beam(i).lastDAOIndex).timeFacCurr; + variables.timeFacCurr_next = updatedInfo.propVMAT.beam(updatedInfo.propVMAT.beam(i).nextDAOIndex).timeFacCurr; + variables.fracFromLastDAO = updatedInfo.propVMAT.beam(i).fracFromLastDAO; + variables.timeFracFromLastDAO = updatedInfo.propVMAT.beam(i).timeFracFromLastDAO; + variables.timeFracFromNextDAO = updatedInfo.propVMAT.beam(i).timeFracFromNextDAO; + + vectorIndices.DAOindex_last = updatedInfo.propVMAT.beam(updatedInfo.propVMAT.beam(i).lastDAOIndex).DAOIndex; + vectorIndices.DAOindex_next = updatedInfo.propVMAT.beam(updatedInfo.propVMAT.beam(i).nextDAOIndex).DAOIndex; + vectorIndices.tIx_last = (apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2)+updatedInfo.propVMAT.beam(updatedInfo.propVMAT.beam(i).lastDAOIndex).DAOIndex; + vectorIndices.tIx_next = (apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2)+updatedInfo.propVMAT.beam(updatedInfo.propVMAT.beam(i).nextDAOIndex).DAOIndex; + + if updatedInfo.continuousAperture + vectorIndices.vectorIx_LF_last = updatedInfo.beam(updatedInfo.propVMAT.beam(i).lastDAOIndex).shape(j).vectorOffset(2) + ((1:n)-1); + vectorIndices.vectorIx_LI_next = updatedInfo.beam(updatedInfo.propVMAT.beam(i).nextDAOIndex).shape(j).vectorOffset(1) + ((1:n)-1); + vectorIndices.vectorIx_RF_last = vectorIndices.vectorIx_LF_last+apertureInfo.totalNumOfLeafPairs; + vectorIndices.vectorIx_RI_next = vectorIndices.vectorIx_LI_next+apertureInfo.totalNumOfLeafPairs; + else + vectorIndices.vectorIx_LF_last = updatedInfo.beam(updatedInfo.propVMAT.beam(i).lastDAOIndex).shape(j).vectorOffset + ((1:n)-1); + vectorIndices.vectorIx_LI_next = updatedInfo.beam(updatedInfo.propVMAT.beam(i).nextDAOIndex).shape(j).vectorOffset + ((1:n)-1); + vectorIndices.vectorIx_RF_last = vectorIndices.vectorIx_LF_last+apertureInfo.totalNumOfLeafPairs; + vectorIndices.vectorIx_RI_next = vectorIndices.vectorIx_LI_next+apertureInfo.totalNumOfLeafPairs; + end + end + + counters.bixelJApVec_offset = bixelJApVec_offset; + + % calculate bixel weight and derivative in function + [w,bixelJApVec_vec,bixelJApVec_i,bixelJApVec_j,sumGradSq,shapeMap,counters] = ... + matRad_bixWeightAndGrad(calcOptions,mlcOptions,variables,vectorIndices,counters,w,bixelJApVec_vec,bixelJApVec_i,bixelJApVec_j,sumGradSq,shapeMap); + + bixelJApVec_offset = counters.bixelJApVec_offset; + + % update shapeMap + updatedInfo.beam(i).shape(j).shapeMap = shapeMap; + % update sumGradSq + % FIX THIS FOR INTERPOLATED ANGLES??? + updatedInfo.beam(i).shape(j).sumGradSq = sumGradSq; + + end + end +end + + +% save bixelWeight, apertureVector +updatedInfo.bixelWeights = w; +updatedInfo.apertureVector = apertureInfoVect; + +if updatedInfo.runVMAT + % save Jacobian between bixelWeight, apertureVector + + deleteInd_i = isnan(bixelJApVec_i); + deleteInd_j = bixelJApVec_j == 0; + if ~all(deleteInd_i == deleteInd_j) + error('Jacobian deletion mismatch'); + else + bixelJApVec_i(deleteInd_i) = []; + bixelJApVec_j(deleteInd_i) = []; + bixelJApVec_vec(deleteInd_i) = []; + end + updatedInfo.bixelJApVec = sparse(bixelJApVec_i,bixelJApVec_j,bixelJApVec_vec,numel(apertureInfoVect),updatedInfo.totalNumOfBixels); +else + % save indVect + updatedInfo.bixelIndices = indVect; +end + +end + diff --git a/matRad/optimization/@matRad_OptimizationProblemVMAT/matRad_getConstraintBounds.m b/matRad/optimization/@matRad_OptimizationProblemVMAT/matRad_getConstraintBounds.m new file mode 100644 index 000000000..438dda557 --- /dev/null +++ b/matRad/optimization/@matRad_OptimizationProblemVMAT/matRad_getConstraintBounds.m @@ -0,0 +1,61 @@ +function [cl,cu] = matRad_getConstraintBounds(optiProb,cst) +% matRad IPOPT get constraint bounds function for VMAT +% +% call +% [cl,cu] = matRad_daoGetConstBounds(cst,apertureInfo,type) +% +% input +% cst: matRad cst struct +% apertureInfo: aperture info struct +% options: option struct defining the type of optimization +% +% output +% cl: lower bounds on constraints +% cu: lower bounds on constraints +% +% References +% +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2015 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSES.txt. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +apertureInfo = optiProb.apertureInfo; + +% get dosimetric bounds from cst by call to DAO superclass method +[cl_dos_dao,cu_dos_dao] = matRad_getConstraintBounds@matRad_OptimizationProblemDAO(optiProb,cst); + +optInd = find([apertureInfo.propVMAT.beam.DAOBeam]); + + +if apertureInfo.continuousAperture + cl_lfspd = apertureInfo.propVMAT.constraints.leafSpeed(1)*ones(2*apertureInfo.propVMAT.numLeafSpeedConstraint*apertureInfo.beam(1).numOfActiveLeafPairs,1); %Minimum leaf travel speed (mm/s) + cu_lfspd = apertureInfo.propVMAT.constraints.leafSpeed(2)*ones(2*apertureInfo.propVMAT.numLeafSpeedConstraint*apertureInfo.beam(1).numOfActiveLeafPairs,1); %Maximum leaf travel speed (mm/s) + %apertureInfo.beam(i).numOfActiveLeafPairs should be independent of i, due to using the union of all ray positions in the stf + %Convert from cm/deg when checking constraints; cannot do it at this stage since gantry rotation speed is not hard-coded +else + + cl_lfspd = apertureInfo.propVMAT.constraints.leafSpeed(1)*ones(2*(numel(optInd)-1)*apertureInfo.beam(1).numOfActiveLeafPairs,1); %Minimum leaf travel speed (mm/s) + cu_lfspd = apertureInfo.propVMAT.constraints.leafSpeed(2)*ones(2*(numel(optInd)-1)*apertureInfo.beam(1).numOfActiveLeafPairs,1); %Maximum leaf travel speed (mm/s) + %apertureInfo.beam(i).numOfActiveLeafPairs should be independent of i, due to using the union of all ray positions in the stf + %Convert from cm/deg when checking constraints; cannot do it at this stage since gantry rotation speed is not hard-coded +end +cl_dosrt = apertureInfo.propVMAT.constraints.monitorUnitRate(1)*ones(numel(optInd),1); %Minimum MU/sec +cu_dosrt = apertureInfo.propVMAT.constraints.monitorUnitRate(2)*ones(numel(optInd),1); %Maximum MU/sec + +% concatenate +cl = [cl_dos_dao; cl_lfspd; cl_dosrt]; +cu = [cu_dos_dao; cu_lfspd; cu_dosrt]; + + diff --git a/matRad/optimization/@matRad_OptimizationProblemVMAT/matRad_getJacobianStructure.m b/matRad/optimization/@matRad_OptimizationProblemVMAT/matRad_getJacobianStructure.m new file mode 100644 index 000000000..149c31ce3 --- /dev/null +++ b/matRad/optimization/@matRad_OptimizationProblemVMAT/matRad_getJacobianStructure.m @@ -0,0 +1,326 @@ +function jacobStruct = matRad_getJacobianStructure(optiProb,apertureInfoVec,dij,cst) +% matRad IPOPT callback: get jacobian structure for direct aperture optimization +% +% call +% jacobStruct = matRad_daoGetJacobStruct(apertureInfo,dij,cst) +% +% input +% apertureInfo: aperture info struct +% dij: dose influence matrix +% cst: matRad cst struct +% +% output +% jacobStruct: jacobian of constraint function +% +% References +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2015 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSES.txt. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%Here we can't use the DAO superclass function, since the dosimetric +%jacobian needs to be altered + +apertureInfo = optiProb.apertureInfo; + +% jacobian structure of the dao constraints +% row indices +i = repmat(1:apertureInfo.totalNumOfLeafPairs,1,2); +% column indices +j = [(apertureInfo.totalNumOfShapes+1):(apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs) ... + ((apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs)+1):(apertureInfo.totalNumOfShapes+2*apertureInfo.totalNumOfLeafPairs)]; + +% -1 for left leaves, 1 for right leaves +s = ones(1,2*apertureInfo.totalNumOfLeafPairs); + +jacobStruct_dao = sparse(i,j,s, ... + apertureInfo.totalNumOfLeafPairs, ... + numel(apertureInfo.apertureVector), ... + 2*apertureInfo.totalNumOfLeafPairs); + +jacobStruct_dos_bixel = matRad_getJacobianStructure@matRad_OptimizationProblem(optiProb,apertureInfo.bixelWeights,dij,cst); + +% --> gives me a matrix with number of rows = num of constraints and tells +% me in th columns if a beamlet has an influence on this constraint + +% for apertures I need to check if the very beam orientation of the aperture has a bixel +% that potentially influences the constraint + +% for leaves I need to check if that particular leaf row has bixels that +% potentially influence the objective which works via apertureInfo.beam(i).bixelIndMap + +% all stuff can be done per beam direction and then I use repmat to build +% up the big matrix + +numOfConstraints = size(jacobStruct_dos_bixel,1); + +i_sparse = 1:numOfConstraints; +i_sparse = kron(i_sparse,ones(1,numel(apertureInfo.apertureVector))); + +j_sparse = 1:numel(apertureInfo.apertureVector); +j_sparse = repmat(j_sparse,1,numOfConstraints); + +jacobStructSparseVec = zeros(numOfConstraints*numel(apertureInfo.apertureVector),1); + +offset = 1; +if apertureInfo.runVMAT && apertureInfo.continuousAperture + repFactor = 2; +else + repFactor = 1; +end + +if ~isempty(jacobStruct_dos_bixel) + + DAOBeams = find([apertureInfo.propVMAT.beam.DAOBeam]); + + for i = 1:numel(apertureInfo.beam) + + % get used bixels in beam + ixWeight = ~isnan(apertureInfo.beam(i).bixelIndMap); + + if apertureInfo.propVMAT.beam(i).DAOBeam + % DAO beam, don't worry about adding since this is just + % struct, i.e. we are only interested if the element is + % non-zero + + % first weight + jacobStructSparseVec(offset == j_sparse) = jacobStructSparseVec(offset == j_sparse)+sum(jacobStruct_dos_bixel(:,apertureInfo.beam(i).bixelIndMap(ixWeight)),2); + + % now leaf positions + for k = 1:apertureInfo.beam(i).numOfActiveLeafPairs + + ixLeaf = ~isnan(apertureInfo.beam(i).bixelIndMap(k,:)); + indInBixVec = apertureInfo.beam(i).bixelIndMap(k,ixLeaf); + + indInOptVec = apertureInfo.beam(i).shape(1).vectorOffset+k-1; + indInOptVec = repmat(indInOptVec,1,repFactor)+repelem([0 apertureInfo.totalNumOfLeafPairs],1,repFactor); + + indInSparseVec = repmat(indInOptVec,1,numOfConstraints)... + +kron((0:numOfConstraints-1)*numel(apertureInfo.apertureVector),ones(1,2*repFactor)); + + jacobStructSparseVec(indInSparseVec) = jacobStructSparseVec(indInSparseVec)+repelem(sum(jacobStruct_dos_bixel(:,indInBixVec),2),2*repFactor,1); + end + + offset = offset+1; + else + % not DAO beam, these may contain bixels which affect the + % constraints which are influenced by DAO leaf pairs that + % do not affect the constraints (unlikely to happen, but it + % might) + + %first weight + + %give fraction of gradient to previous optimized beam + lastDAOInd = find(DAOBeams == apertureInfo.propVMAT.beam(i).lastDAOIndex,1); + jacobStructSparseVec(lastDAOInd == j_sparse) = jacobStructSparseVec(lastDAOInd == j_sparse)+sum(jacobStruct_dos_bixel(:,apertureInfo.beam(i).bixelIndMap(ixWeight)),2); + %give the other fraction to next optimized beam + nextDAOInd = find(DAOBeams == apertureInfo.propVMAT.beam(i).nextDAOIndex,1); + jacobStructSparseVec(nextDAOInd == j_sparse) = jacobStructSparseVec(nextDAOInd == j_sparse)+sum(jacobStruct_dos_bixel(:,apertureInfo.beam(i).bixelIndMap(ixWeight)),2); + + %now leaf pos + + for k = 1:apertureInfo.beam(i).numOfActiveLeafPairs + + ixLeaf = ~isnan(apertureInfo.beam(i).bixelIndMap(k,:)); + indInBixVec = apertureInfo.beam(i).bixelIndMap(k,ixLeaf); + + %give fraction of gradient to previous optimized beam + indInOptVec = apertureInfo.beam(apertureInfo.propVMAT.beam(i).lastDAOIndex).shape(1).vectorOffset+k-1; + indInOptVec = repmat(indInOptVec,1,repFactor)+repelem([0 apertureInfo.totalNumOfLeafPairs],1,repFactor); + indInSparseVec = repmat(indInOptVec,1,numOfConstraints)... + +kron((0:numOfConstraints-1)*numel(apertureInfo.apertureVector),ones(1,2*repFactor)); + + jacobStructSparseVec(indInSparseVec) = jacobStructSparseVec(indInSparseVec)+repelem(sum(jacobStruct_dos_bixel(:,indInBixVec),2),2*repFactor,1); + + + %give the other fraction to next optimized beam + indInOptVec = apertureInfo.beam(apertureInfo.propVMAT.beam(i).nextDAOIndex).shape(1).vectorOffset+k-1; + indInOptVec = repmat(indInOptVec,1,repFactor)+repelem([0 apertureInfo.totalNumOfLeafPairs],1,repFactor); + indInSparseVec = repmat(indInOptVec,1,numOfConstraints)... + +kron((0:numOfConstraints-1)*numel(apertureInfo.apertureVector),ones(1,2*repFactor)); + + jacobStructSparseVec(indInSparseVec) = jacobStructSparseVec(indInSparseVec)+repelem(sum(jacobStruct_dos_bixel(:,indInBixVec),2),2*repFactor,1); + end + + %now time + + %give fraction of gradient to previous optimized beam + lastDAOIndTime = find(DAOBeams == apertureInfo.propVMAT.beam(i).lastDAOIndex,1)+(apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2); + jacobStructSparseVec(lastDAOIndTime == j_sparse) = jacobStructSparseVec(lastDAOIndTime == j_sparse)+sum(jacobStruct_dos_bixel(:,apertureInfo.beam(i).bixelIndMap(ixWeight)),2); + + %give the other fraction to next optimized beam + nextDAOIndTime = find(DAOBeams == apertureInfo.propVMAT.beam(i).nextDAOIndex,1)+(apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2); + jacobStructSparseVec(nextDAOIndTime == j_sparse) = jacobStructSparseVec(nextDAOIndTime == j_sparse)+sum(jacobStruct_dos_bixel(:,apertureInfo.beam(i).bixelIndMap(ixWeight)),2); + end + end + + jacobStructSparseVec(jacobStructSparseVec ~= 0) = 1; + jacobStruct_dos = sparse(i_sparse,j_sparse,jacobStructSparseVec,numOfConstraints,numel(apertureInfo.apertureVector)); +else + jacobStruct_dos = sparse(0,0); +end + +jacobStruct_dos_dao = [jacobStruct_dos; jacobStruct_dao]; + +if apertureInfo.continuousAperture + % set up + n = apertureInfo.beam(1).numOfActiveLeafPairs; + indInSparseVec = (1:n); + indInConVec = (1:n); + shapeInd = 1; + + % sparse matrix + numElem = n.*(apertureInfo.propVMAT.numLeafSpeedConstraintDAO*6+(apertureInfo.propVMAT.numLeafSpeedConstraint-apertureInfo.propVMAT.numLeafSpeedConstraintDAO)*8); + i_sparse = zeros(numElem,1); + j_sparse = zeros(numElem,1); + s_sparse = ones(numElem,1); + + for i = 1:numel(apertureInfo.beam) + % loop over beams + + if ~isempty(apertureInfo.propVMAT.beam(i).leafConstMask) + + % get vector indices + if apertureInfo.propVMAT.beam(i).DAOBeam + % if it's a DAO beam, use own vector offset + vectorIx_LI = apertureInfo.beam(i).shape(1).vectorOffset(1) + ((1:n)-1); + vectorIx_LF = apertureInfo.beam(i).shape(1).vectorOffset(2) + ((1:n)-1); + else + % otherwise, use vector offset of previous and next + % beams + vectorIx_LI = apertureInfo.beam(apertureInfo.propVMAT.beam(i).lastDAOIndex).shape(1).vectorOffset(2) + ((1:n)-1); + vectorIx_LF = apertureInfo.beam(apertureInfo.propVMAT.beam(i).nextDAOIndex).shape(1).vectorOffset(1) + ((1:n)-1); + end + vectorIx_RI = vectorIx_LI+apertureInfo.totalNumOfLeafPairs; + vectorIx_RF = vectorIx_LF+apertureInfo.totalNumOfLeafPairs; + + % calc jacobs + + % wrt initial leaf positions (left, then right) + i_sparse(indInSparseVec) = indInConVec; + j_sparse(indInSparseVec) = vectorIx_LI; + indInSparseVec = indInSparseVec+n; + + i_sparse(indInSparseVec) = indInConVec+apertureInfo.propVMAT.numLeafSpeedConstraint*apertureInfo.beam(1).numOfActiveLeafPairs; + j_sparse(indInSparseVec) = vectorIx_RI; + indInSparseVec = indInSparseVec+n; + + % wrt final leaf positions (left, then right) + i_sparse(indInSparseVec) = indInConVec; + j_sparse(indInSparseVec) = vectorIx_LF; + indInSparseVec = indInSparseVec+n; + + i_sparse(indInSparseVec) = indInConVec+apertureInfo.propVMAT.numLeafSpeedConstraint*apertureInfo.beam(1).numOfActiveLeafPairs; + j_sparse(indInSparseVec) = vectorIx_RF; + indInSparseVec = indInSparseVec+n; + + % wrt time (left, then right) + % how we do this depends on if it's a DAO beam or + % not + if apertureInfo.propVMAT.beam(i).DAOBeam + % if it is, then speeds only depend on its own + % time + i_sparse(indInSparseVec) = indInConVec; + j_sparse(indInSparseVec) = apertureInfo.propVMAT.beam(i).timeInd; + indInSparseVec = indInSparseVec+n; + + i_sparse(indInSparseVec) = indInConVec+apertureInfo.propVMAT.numLeafSpeedConstraint*apertureInfo.beam(1).numOfActiveLeafPairs; + j_sparse(indInSparseVec) = apertureInfo.propVMAT.beam(i).timeInd; + indInSparseVec = indInSparseVec+n; + + else + % otherwise, speed depends on time of DAO + % before and DAO after + + % before + i_sparse(indInSparseVec) = indInConVec; + j_sparse(indInSparseVec) = apertureInfo.propVMAT.beam(apertureInfo.propVMAT.beam(i).lastDAOIndex).timeInd; + indInSparseVec = indInSparseVec+n; + + i_sparse(indInSparseVec) = indInConVec+apertureInfo.propVMAT.numLeafSpeedConstraint*apertureInfo.beam(1).numOfActiveLeafPairs; + j_sparse(indInSparseVec) = apertureInfo.propVMAT.beam(apertureInfo.propVMAT.beam(i).lastDAOIndex).timeInd; + indInSparseVec = indInSparseVec+n; + + % after + i_sparse(indInSparseVec) = indInConVec; + j_sparse(indInSparseVec) = apertureInfo.propVMAT.beam(apertureInfo.propVMAT.beam(i).nextDAOIndex).timeInd; + indInSparseVec = indInSparseVec+n; + + i_sparse(indInSparseVec) = indInConVec+apertureInfo.propVMAT.numLeafSpeedConstraint*apertureInfo.beam(1).numOfActiveLeafPairs; + j_sparse(indInSparseVec) = apertureInfo.propVMAT.beam(apertureInfo.propVMAT.beam(i).nextDAOIndex).timeInd; + indInSparseVec = indInSparseVec+n; + + end + + % update offset + indInConVec = indInConVec+n; + + % increment shapeInd only for beams which have transtion + % defined + shapeInd = shapeInd+1; + end + end + + jacobStruct_lfspd = sparse(i_sparse,j_sparse,s_sparse,2*apertureInfo.beam(1).numOfActiveLeafPairs*apertureInfo.propVMAT.numLeafSpeedConstraint,numel(apertureInfo.apertureVector)); + +else + + i = sort(repmat(1:(apertureInfo.totalNumOfShapes-1),1,2)); + j = sort(repmat(1:apertureInfo.totalNumOfShapes,1,2)); + j(1) = []; + j(end) = []; + + % get index values for the jacobian + % variable index + timeInd = (1+apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2):(apertureInfo.totalNumOfShapes*2+apertureInfo.totalNumOfLeafPairs*2-1); + currentLeftLeafInd = (apertureInfo.totalNumOfShapes+1):(apertureInfo.totalNumOfShapes+apertureInfo.beam(1).numOfActiveLeafPairs*numel(timeInd)); + currentRightLeafInd = (apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs+1):(apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs+apertureInfo.beam(1).numOfActiveLeafPairs*numel(timeInd)); + nextLeftLeafInd = (apertureInfo.beam(1).numOfActiveLeafPairs+apertureInfo.totalNumOfShapes+1):(apertureInfo.beam(1).numOfActiveLeafPairs+apertureInfo.totalNumOfShapes+apertureInfo.beam(1).numOfActiveLeafPairs*numel(timeInd)); + nextRightLeafInd = (apertureInfo.beam(1).numOfActiveLeafPairs+apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs+1):(apertureInfo.beam(1).numOfActiveLeafPairs+apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs+apertureInfo.beam(1).numOfActiveLeafPairs*numel(timeInd)); + leftTimeInd = kron(j,ones(1,apertureInfo.beam(1).numOfActiveLeafPairs))+apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2; + rightTimeInd = kron(j,ones(1,apertureInfo.beam(1).numOfActiveLeafPairs))+apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2; + %leftTimeInd = repelem(j,apertureInfo.beam(1).numOfActiveLeafPairs)+apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2; + %rightTimeInd = repelem(j,apertureInfo.beam(1).numOfActiveLeafPairs)+apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2; + % constraint index + leafConstraintInd = 1:2*apertureInfo.beam(1).numOfActiveLeafPairs*numel(timeInd); + + % jacobian of the leafspeed constraint + i = repmat((i'-1)*apertureInfo.beam(1).numOfActiveLeafPairs,1,apertureInfo.beam(1).numOfActiveLeafPairs)+repmat(1:apertureInfo.beam(1).numOfActiveLeafPairs,2*numel(timeInd),1); + i = reshape([i' i'+apertureInfo.beam(1).numOfActiveLeafPairs*numel(timeInd)],1,[]); + + i = [repmat(leafConstraintInd,1,2) i]; + j = [currentLeftLeafInd currentRightLeafInd nextLeftLeafInd nextRightLeafInd leftTimeInd rightTimeInd]; + % first do jacob wrt current leaf position (left, right), then next leaf + % position (left, right), then time (left, right) + + s = ones(1,numel(j)); + + jacobStruct_lfspd = sparse(i,j,s,2*apertureInfo.beam(1).numOfActiveLeafPairs*(apertureInfo.totalNumOfShapes-1),numel(apertureInfo.apertureVector),numel(s)); +end + +% jacobian of the doserate constraint +i = repmat(1:(apertureInfo.totalNumOfShapes),1,2); +j = [1:(apertureInfo.totalNumOfShapes) ... + ((apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2)+1):((apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2)+apertureInfo.totalNumOfShapes)]; +% first do jacob wrt weights, then wrt times + +s = ones(1,2*apertureInfo.totalNumOfShapes); + +jacobStruct_dosrt = sparse(i,j,s,apertureInfo.totalNumOfShapes,numel(apertureInfo.apertureVector),2*apertureInfo.totalNumOfShapes); + +% concatenate +jacobStruct = [jacobStruct_dos_dao; jacobStruct_lfspd; jacobStruct_dosrt]; + + + diff --git a/matRad/optimization/@matRad_OptimizationProblemVMAT/matRad_objectiveGradient.m b/matRad/optimization/@matRad_OptimizationProblemVMAT/matRad_objectiveGradient.m new file mode 100644 index 000000000..e20ab5c63 --- /dev/null +++ b/matRad/optimization/@matRad_OptimizationProblemVMAT/matRad_objectiveGradient.m @@ -0,0 +1,51 @@ +function g = matRad_objectiveGradient(optiProb,apertureInfoVec,dij,cst) +% matRad IPOPT callback: gradient function for direct aperture optimization +% +% call +% g = matRad_daoGradFunc(apertureInfoVec,apertureInfo,dij,cst,type) +% +% input +% apertureInfoVec: aperture info in form of vector +% dij: matRad dij struct as generated by bixel-based dose calculation +% cst: matRad cst struct +% options: option struct defining the type of optimization +% +% output +% g: gradient +% +% References +% [1] http://dx.doi.org/10.1118/1.4914863 +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2015 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSES.txt. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%We don't use the DAO gradient in here, because dosimetric stuff is altered + +% update apertureInfo, bixel weight vector an mapping of leafes to bixels +if ~isequal(apertureInfoVec,optiProb.apertureInfo.apertureVector) + optiProb.apertureInfo = optiProb.matRad_daoVec2ApertureInfo(optiProb.apertureInfo,apertureInfoVec); +end +apertureInfo = optiProb.apertureInfo; + +% bixel based gradient calculation +bixelG = matRad_objectiveGradient@matRad_OptimizationProblem(optiProb,apertureInfo.bixelWeights,dij,cst); + +% allocate gradient vector for aperture weights and leaf positions +%g = NaN * ones(size(apertureInfoVec,1),1); + +% use the Jacobian calculated in daoVec2ApertureInfo. +% should also do this for non-VMAT +g = apertureInfo.bixelJApVec * bixelG; + diff --git a/matRad/optimization/matRad_bixWeightAndGrad.m b/matRad/optimization/matRad_bixWeightAndGrad.m new file mode 100644 index 000000000..63466d5ae --- /dev/null +++ b/matRad/optimization/matRad_bixWeightAndGrad.m @@ -0,0 +1,422 @@ +function [w,bixelJApVec_vec,bixelJApVec_i,bixelJApVec_j,sumGradSq,shapeMapW,counters] = ... + matRad_bixWeightAndGrad(calcOptions,mlcOptions,variables,vectorIndices,counters,w,bixelJApVec_vec,bixelJApVec_i,bixelJApVec_j,sumGradSq,shapeMapW) +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% matRad function to calculate the bixel weights from the aperture vector, +% and also the Jacobian matrix relating these two. +% +% call +% [w,bixelJApVec_vec,bixelJApVec_i,bixelJApVec_j,sqrtSumGradSq,shapeMap,counters] = ... +% matRad_bixWeightAndGrad(calcOptions,mlcOptions,variables,vectorIndices,counters,w,bixelJApVec_vec,bixelJApVec_i,bixelJApVec_j) +% +% input +% +% output +% +% References +% +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2018, Mark Bangert, on behalf of the matRad development team +% +% m.bangert@dkfz.de +% +% This file is part of matRad. +% +% matrad is free software: you can redistribute it and/or modify it under +% the terms of the GNU General Public License as published by the Free +% Software Foundation, either version 3 of the License, or (at your option) +% any later version. +% +% matRad is distributed in the hope that it will be useful, but WITHOUT ANY +% WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +% FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +% details. +% +% You should have received a copy of the GNU General Public License in the +% file license.txt along with matRad. If not, see +% . +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +round2 = @(a,b) round(a*10^b)/10^b; + +%% extract variables from inputs +lim_l = mlcOptions.lim_l; +lim_r = mlcOptions.lim_r; +edges_l = mlcOptions.edges_l; +edges_r = mlcOptions.edges_r; +centres = mlcOptions.centres; +widths = mlcOptions.widths; +n = mlcOptions.n; +numBix = mlcOptions.numBix; +bixelWidth = mlcOptions.bixelWidth; +bixelIndMap = mlcOptions.bixelIndMap; + +weight = variables.weight; +leftLeafPos_I = variables.leftLeafPos_I; +leftLeafPos_F = variables.leftLeafPos_F; +rightLeafPos_I = variables.rightLeafPos_I; +rightLeafPos_F = variables.rightLeafPos_F; + +bixelJApVec_offset = counters.bixelJApVec_offset; + + +%% sort out order, set up calculation of bixel weight and gradients + +% set the initial leaf positions to the minimum leaf positions +% always, instead of the leaf positions at the actual beginning +% of the arc +% this simplifies the calculation +% remember which one is actually I and F in leftMinInd +[leftLeafPosI,leftMinInd] = min([leftLeafPos_I,leftLeafPos_F],[],2); +leftLeafPosF = max([leftLeafPos_I,leftLeafPos_F],[],2); +[rightLeafPosI,rightMinInd] = min([rightLeafPos_I,rightLeafPos_F],[],2); +rightLeafPosF = max([rightLeafPos_I,rightLeafPos_F],[],2); + +if calcOptions.saveJacobian + % only need these variables for the Jacobian + + if calcOptions.DAOBeam + jacobiScale = variables.jacobiScale; + + vectorIx_LI = vectorIndices.vectorIx_LI; + vectorIx_LF = vectorIndices.vectorIx_LF; + vectorIx_RI = vectorIndices.vectorIx_RI; + vectorIx_RF = vectorIndices.vectorIx_RF; + DAOindex = vectorIndices.DAOindex; + + % change the vectorIx_xy elements to remember which + % apertureVector elements the "new" I and F + % if leftMinInd is 2, the I and F are switched + tempL = vectorIx_LI; + tempR = vectorIx_RI; + vectorIx_LI(leftMinInd == 2) = vectorIx_LF(leftMinInd == 2); + vectorIx_LF(leftMinInd == 2) = tempL(leftMinInd == 2); + vectorIx_RI(rightMinInd == 2) = vectorIx_RF(rightMinInd == 2); + vectorIx_RF(rightMinInd == 2) = tempR(rightMinInd == 2); + else + + weight_last = variables.weight_last; + weight_next = variables.weight_next; + jacobiScale_last = variables.jacobiScale_last; + jacobiScale_next = variables.jacobiScale_next; + + time_last = variables.time_last; + time_next = variables.time_next; + time = variables.time; + + fracFromLastOptI = variables.fracFromLastOptI; + fracFromLastOptF = variables.fracFromLastOptF; + fracFromNextOptI = variables.fracFromNextOptI; + fracFromNextOptF = variables.fracFromNextOptF; + fracFromLastOpt = variables.fracFromLastOpt; + + % replicate + fracFromLastOptI = repmat(fracFromLastOptI,1,numBix); + fracFromLastOptF = repmat(fracFromLastOptF,1,numBix); + fracFromNextOptI = repmat(fracFromNextOptI,1,numBix); + fracFromNextOptF = repmat(fracFromNextOptF,1,numBix); + + doseAngleBordersDiff = variables.doseAngleBordersDiff; + doseAngleBordersDiff_last = variables.doseAngleBordersDiff_last; + doseAngleBordersDiff_next = variables.doseAngleBordersDiff_next; + timeFacCurr_last = variables.timeFacCurr_last; + timeFacCurr_next = variables.timeFacCurr_next; + fracFromLastDAO = variables.fracFromLastDAO; + timeFracFromLastDAO = variables.timeFracFromLastDAO; + timeFracFromNextDAO = variables.timeFracFromNextDAO; + + vectorIx_LF_last = vectorIndices.vectorIx_LF_last; + vectorIx_LI_next = vectorIndices.vectorIx_LI_next; + vectorIx_RF_last = vectorIndices.vectorIx_RF_last; + vectorIx_RI_next = vectorIndices.vectorIx_RI_next; + DAOindex_last = vectorIndices.DAOindex_last; + DAOindex_next = vectorIndices.DAOindex_next; + tIx_last = vectorIndices.tIx_last; + tIx_next = vectorIndices.tIx_next; + + tempL = vectorIx_LF_last; + tempR = vectorIx_RF_last; + + vectorIx_LF_last(leftMinInd == 2) = vectorIx_LI_next(leftMinInd == 2); + vectorIx_LI_next(leftMinInd == 2) = tempL(leftMinInd == 2); + + vectorIx_RF_last(rightMinInd == 2) = vectorIx_RI_next(rightMinInd == 2); + vectorIx_RI_next(rightMinInd == 2) = tempR(rightMinInd == 2); + end +end + +leftLeafPosI = round2(leftLeafPosI,10); +leftLeafPosF = round2(leftLeafPosF,10); +rightLeafPosI = round2(rightLeafPosI,10); +rightLeafPosF = round2(rightLeafPosF,10); + +leftLeafPosI(leftLeafPosI <= lim_l) = lim_l(leftLeafPosI <= lim_l); +leftLeafPosF(leftLeafPosF <= lim_l) = lim_l(leftLeafPosF <= lim_l); +rightLeafPosI(rightLeafPosI <= lim_l) = lim_l(rightLeafPosI <= lim_l); +rightLeafPosF(rightLeafPosF <= lim_l) = lim_l(rightLeafPosF <= lim_l); +leftLeafPosI(leftLeafPosI >= lim_r) = lim_r(leftLeafPosI >= lim_r); +leftLeafPosF(leftLeafPosF >= lim_r) = lim_r(leftLeafPosF >= lim_r); +rightLeafPosI(rightLeafPosI >= lim_r) = lim_r(rightLeafPosI >= lim_r); +rightLeafPosF(rightLeafPosF >= lim_r) = lim_r(rightLeafPosF >= lim_r); + +% find bixel indices where leaves are located +xPosIndLeftLeafI = min(floor((leftLeafPosI-edges_l(1))./bixelWidth)+1,numBix); +xPosIndLeftLeafF = min(floor((leftLeafPosF-edges_l(1))./bixelWidth)+1,numBix); +xPosIndRightLeafI = min(floor((rightLeafPosI-edges_l(1))./bixelWidth)+1,numBix); +xPosIndRightLeafF = min(floor((rightLeafPosF-edges_l(1))./bixelWidth)+1,numBix); +% +xPosLinearIndLeftLeafI = sub2ind([n numBix],(1:n)',xPosIndLeftLeafI); +xPosLinearIndLeftLeafF = sub2ind([n numBix],(1:n)',xPosIndLeftLeafF); +xPosLinearIndRightLeafI = sub2ind([n numBix],(1:n)',xPosIndRightLeafI); +xPosLinearIndRightLeafF = sub2ind([n numBix],(1:n)',xPosIndRightLeafF); + + +% +% leaves sweep from _I to _F, with weight +% + + +%% bixel weight calculation + +%calculate fraction of fluence uncovered by left leaf +%initial computation +uncoveredByLeftLeaf = bsxfun(@minus,centres,leftLeafPosI)./repmat(leftLeafPosF-leftLeafPosI,1,numBix); +%correct for overshoot in initial and final leaf positions +uncoveredByLeftLeaf(xPosLinearIndLeftLeafI) = uncoveredByLeftLeaf(xPosLinearIndLeftLeafI) + (leftLeafPosI-edges_l(xPosIndLeftLeafI)').^2./((leftLeafPosF-leftLeafPosI).*(widths(xPosIndLeftLeafI)').*2); +uncoveredByLeftLeaf(xPosLinearIndLeftLeafF) = uncoveredByLeftLeaf(xPosLinearIndLeftLeafF) - (edges_r(xPosIndLeftLeafF)'-leftLeafPosF).^2./((leftLeafPosF-leftLeafPosI).*(widths(xPosIndLeftLeafF)').*2); +%round <0 to 0, >1 to 1 +uncoveredByLeftLeaf(uncoveredByLeftLeaf < 0) = 0; +uncoveredByLeftLeaf(uncoveredByLeftLeaf > 1) = 1; + +%calculate fraction of fluence covered by right leaf +%initial computation +coveredByRightLeaf = bsxfun(@minus,centres,rightLeafPosI)./repmat(rightLeafPosF-rightLeafPosI,1,numBix); +%correct for overshoot in initial and final leaf positions +coveredByRightLeaf(xPosLinearIndRightLeafI) = coveredByRightLeaf(xPosLinearIndRightLeafI) + (rightLeafPosI-edges_l(xPosIndRightLeafI)').^2./((rightLeafPosF-rightLeafPosI).*(widths(xPosIndRightLeafI)').*2); +coveredByRightLeaf(xPosLinearIndRightLeafF) = coveredByRightLeaf(xPosLinearIndRightLeafF) - (edges_r(xPosIndRightLeafF)'-rightLeafPosF).^2./((rightLeafPosF-rightLeafPosI).*(widths(xPosIndRightLeafF)').*2); +%round <0 to 0, >1 to 1 +coveredByRightLeaf(coveredByRightLeaf < 0) = 0; +coveredByRightLeaf(coveredByRightLeaf > 1) = 1; + +%% gradient calculation + +dUl_dLI = bsxfun(@minus,centres,leftLeafPosF)./(repmat(leftLeafPosF-leftLeafPosI,1,numBix)).^2; +dUl_dLF = bsxfun(@minus,leftLeafPosI,centres)./(repmat(leftLeafPosF-leftLeafPosI,1,numBix)).^2; + +dCr_dRI = bsxfun(@minus,centres,rightLeafPosF)./(repmat(rightLeafPosF-rightLeafPosI,1,numBix)).^2; +dCr_dRF = bsxfun(@minus,rightLeafPosI,centres)./(repmat(rightLeafPosF-rightLeafPosI,1,numBix)).^2; + +dUl_dLI(xPosLinearIndLeftLeafI) = dUl_dLI(xPosLinearIndLeftLeafI) + ((leftLeafPosI-edges_l(xPosIndLeftLeafI)').*(2*leftLeafPosF-leftLeafPosI-edges_l(xPosIndLeftLeafI)'))./((leftLeafPosF-leftLeafPosI).^2.*(widths(xPosIndLeftLeafI)').*2); +dUl_dLF(xPosLinearIndLeftLeafI) = dUl_dLF(xPosLinearIndLeftLeafI) - ((leftLeafPosI-edges_l(xPosIndLeftLeafI)').^2)./((leftLeafPosF-leftLeafPosI).^2.*(widths(xPosIndLeftLeafI)').*2); +dUl_dLI(xPosLinearIndLeftLeafF) = dUl_dLI(xPosLinearIndLeftLeafF) - ((edges_r(xPosIndLeftLeafF)'-leftLeafPosF).^2)./((leftLeafPosF-leftLeafPosI).^2.*(widths(xPosIndLeftLeafF)').*2); +dUl_dLF(xPosLinearIndLeftLeafF) = dUl_dLF(xPosLinearIndLeftLeafF) + ((edges_r(xPosIndLeftLeafF)'-leftLeafPosF).*(leftLeafPosF+edges_r(xPosIndLeftLeafF)'-2*leftLeafPosI))./((leftLeafPosF-leftLeafPosI).^2.*(widths(xPosIndLeftLeafF)').*2); + +dCr_dRI(xPosLinearIndRightLeafI) = dCr_dRI(xPosLinearIndRightLeafI) + ((rightLeafPosI-edges_l(xPosIndRightLeafI)').*(2*rightLeafPosF-rightLeafPosI-edges_l(xPosIndRightLeafI)'))./((rightLeafPosF-rightLeafPosI).^2.*(widths(xPosIndRightLeafI)').*2); +dCr_dRF(xPosLinearIndRightLeafI) = dCr_dRF(xPosLinearIndRightLeafI) - ((rightLeafPosI-edges_l(xPosIndRightLeafI)').^2)./((rightLeafPosF-rightLeafPosI).^2.*(widths(xPosIndRightLeafI)').*2); +dCr_dRI(xPosLinearIndRightLeafF) = dCr_dRI(xPosLinearIndRightLeafF) - ((edges_r(xPosIndRightLeafF)'-rightLeafPosF).^2)./((rightLeafPosF-rightLeafPosI).^2.*(widths(xPosIndRightLeafF)').*2); +dCr_dRF(xPosLinearIndRightLeafF) = dCr_dRF(xPosLinearIndRightLeafF) + ((edges_r(xPosIndRightLeafF)'-rightLeafPosF).*(rightLeafPosF+edges_r(xPosIndRightLeafF)'-2*rightLeafPosI))./((rightLeafPosF-rightLeafPosI).^2.*(widths(xPosIndRightLeafF)').*2); + +for k = 1:n + dUl_dLI(k,1:(xPosIndLeftLeafI(k)-1)) = 0; + dUl_dLF(k,1:(xPosIndLeftLeafI(k)-1)) = 0; + dUl_dLI(k,(xPosIndLeftLeafF(k)+1):numBix) = 0; + dUl_dLF(k,(xPosIndLeftLeafF(k)+1):numBix) = 0; + + if xPosIndLeftLeafI(k) >= xPosIndLeftLeafF(k) + % in discrete aperture, the xPosIndLeftLeafI is greater than + % xPosIndLeftLeafM when leaf positions are at a bixel boundary + + %19 July 2017 in journal + dUl_dLI(k,xPosIndLeftLeafI(k)) = -1/(2*widths(xPosIndLeftLeafI(k))'); + dUl_dLF(k,xPosIndLeftLeafF(k)) = -1/(2*widths(xPosIndLeftLeafF(k))'); + if leftLeafPosF(k)-leftLeafPosI(k) <= eps(max(lim_r)) + uncoveredByLeftLeaf(k,xPosIndLeftLeafI(k)) = (edges_r(xPosIndLeftLeafI(k))-leftLeafPosI(k))./widths(xPosIndLeftLeafI(k)); + uncoveredByLeftLeaf(k,xPosIndLeftLeafF(k)) = (edges_r(xPosIndLeftLeafF(k))-leftLeafPosF(k))./widths(xPosIndLeftLeafF(k)); + end + end + + dCr_dRI(k,1:(xPosIndRightLeafI(k)-1)) = 0; + dCr_dRF(k,1:(xPosIndRightLeafI(k)-1)) = 0; + dCr_dRI(k,(xPosIndRightLeafF(k)+1):numBix) = 0; + dCr_dRF(k,(xPosIndRightLeafF(k)+1):numBix) = 0; + + if xPosIndRightLeafI(k) >= xPosIndRightLeafF(k) + dCr_dRI(k,xPosIndRightLeafI(k)) = -1/(2*widths(xPosIndRightLeafI(k))'); + dCr_dRF(k,xPosIndRightLeafF(k)) = -1/(2*widths(xPosIndRightLeafF(k))'); + if rightLeafPosF(k)-rightLeafPosI(k) <= eps(max(lim_r)) + coveredByRightLeaf(k,xPosIndRightLeafI(k)) = (edges_r(xPosIndRightLeafI(k))-rightLeafPosI(k))./widths(xPosIndRightLeafI(k)); + coveredByRightLeaf(k,xPosIndRightLeafF(k)) = (edges_r(xPosIndRightLeafF(k))-rightLeafPosF(k))./widths(xPosIndRightLeafF(k)); + end + end +end + +% store information for Jacobi preconditioning +sumGradSq = sumGradSq+mean([sum((dUl_dLI).^2,2); sum((dUl_dLF).^2,2); sum((dUl_dLF).^2,2); sum((dCr_dRI).^2,2); sum((dCr_dRF).^2,2); sum((dCr_dRF).^2,2)]); + +%% save the bixel weights +%fluence is equal to fluence not covered by left leaf minus +%fluence covered by left leaf +shapeMap = uncoveredByLeftLeaf-coveredByRightLeaf; +shapeMap = round2(shapeMap,15); +shapeMap(isnan(shapeMap)) = 0; + +% find open bixels +%shapeMapIx = shapeMap > 0; +shapeMapIx = ~isnan(bixelIndMap); + +currBixelIx = bixelIndMap(shapeMapIx); +w(currBixelIx) = w(currBixelIx) + shapeMap(shapeMapIx).*weight; +shapeMapW = shapeMapW+shapeMap.*weight; + +%% save the gradients + +if calcOptions.saveJacobian + + numSaveBixel = nnz(shapeMapIx); + + if calcOptions.DAOBeam + % indices + vectorIxMat_LI = repmat(vectorIx_LI',1,numBix); + vectorIxMat_LF = repmat(vectorIx_LF',1,numBix); + vectorIxMat_RI = repmat(vectorIx_RI',1,numBix); + vectorIxMat_RF = repmat(vectorIx_RF',1,numBix); + + % wrt weight + bixelJApVec_vec(bixelJApVec_offset+(1:numSaveBixel)) = shapeMap(shapeMapIx)./jacobiScale; + bixelJApVec_i(bixelJApVec_offset+(1:numSaveBixel)) = DAOindex; + bixelJApVec_j(bixelJApVec_offset+(1:numSaveBixel)) = bixelIndMap(shapeMapIx); + bixelJApVec_offset = bixelJApVec_offset+numSaveBixel; + + % wrt initial left + bixelJApVec_vec(bixelJApVec_offset+(1:numSaveBixel)) = dUl_dLI(shapeMapIx).*weight; + bixelJApVec_i(bixelJApVec_offset+(1:numSaveBixel)) = vectorIxMat_LI(shapeMapIx); + bixelJApVec_j(bixelJApVec_offset+(1:numSaveBixel)) = bixelIndMap(shapeMapIx); + bixelJApVec_offset = bixelJApVec_offset+numSaveBixel; + + % wrt final left + bixelJApVec_vec(bixelJApVec_offset+(1:numSaveBixel)) = dUl_dLF(shapeMapIx).*weight; + bixelJApVec_i(bixelJApVec_offset+(1:numSaveBixel)) = vectorIxMat_LF(shapeMapIx); + bixelJApVec_j(bixelJApVec_offset+(1:numSaveBixel)) = bixelIndMap(shapeMapIx); + bixelJApVec_offset = bixelJApVec_offset+numSaveBixel; + + % wrt initial right + bixelJApVec_vec(bixelJApVec_offset+(1:numSaveBixel)) = -dCr_dRI(shapeMapIx).*weight; + bixelJApVec_i(bixelJApVec_offset+(1:numSaveBixel)) = vectorIxMat_RI(shapeMapIx); + bixelJApVec_j(bixelJApVec_offset+(1:numSaveBixel)) = bixelIndMap(shapeMapIx); + bixelJApVec_offset = bixelJApVec_offset+numSaveBixel; + + % wrt final right + bixelJApVec_vec(bixelJApVec_offset+(1:numSaveBixel)) = -dCr_dRF(shapeMapIx).*weight; + bixelJApVec_i(bixelJApVec_offset+(1:numSaveBixel)) = vectorIxMat_RF(shapeMapIx); + bixelJApVec_j(bixelJApVec_offset+(1:numSaveBixel)) = bixelIndMap(shapeMapIx); + bixelJApVec_offset = bixelJApVec_offset+numSaveBixel; + + else + % indices + vectorIxMat_LF_last = repmat(vectorIx_LF_last',1,numBix); + vectorIxMat_LI_next = repmat(vectorIx_LI_next',1,numBix); + vectorIxMat_RF_last = repmat(vectorIx_RF_last',1,numBix); + vectorIxMat_RI_next = repmat(vectorIx_RI_next',1,numBix); + + % wrt last weight + bixelJApVec_vec(bixelJApVec_offset+(1:numSaveBixel)) = fracFromLastOpt*(time./time_last)*shapeMap(shapeMapIx)./jacobiScale_last; + %bixelJApVec_vec(bixelJApVec_offset+(1:numSaveBixel)) = (updatedInfo.beam(i).doseAngleBordersDiff*fracFromLastOpt*updatedInfo.beam(apertureInfo.beam(i).lastOptIndex).gantryRot ... + %/(updatedInfo.beam(apertureInfo.beam(i).lastOptIndex).doseAngleBordersDiff*updatedInfo.beam(i).gantryRot))*updatedInfo.beam(i).shape(j).shapeMap(shapeMapIx) ... + %./ apertureInfo.beam(apertureInfo.beam(i).lastOptIndex).shape(1).jacobiScale; + bixelJApVec_i(bixelJApVec_offset+(1:numSaveBixel)) = DAOindex_last; + bixelJApVec_j(bixelJApVec_offset+(1:numSaveBixel)) = bixelIndMap(shapeMapIx); + bixelJApVec_offset = bixelJApVec_offset+numSaveBixel; + + % wrt next weight + bixelJApVec_vec(bixelJApVec_offset+(1:numSaveBixel)) = (1-fracFromLastOpt)*(time./time_next)*shapeMap(shapeMapIx)./jacobiScale_next; + %bixelJApVec_vec(bixelJApVec_offset+(1:numSaveBixel)) = (updatedInfo.beam(i).doseAngleBordersDiff*(1-fracFromLastOpt)*updatedInfo.beam(apertureInfo.beam(i).nextOptIndex).gantryRot ... + %/(updatedInfo.beam(apertureInfo.beam(i).nextOptIndex).doseAngleBordersDiff*updatedInfo.beam(i).gantryRot))*updatedInfo.beam(i).shape(j).shapeMap(shapeMapIx) ... + %./ apertureInfo.beam(apertureInfo.beam(i).nextOptIndex).shape(1).jacobiScale; + bixelJApVec_i(bixelJApVec_offset+(1:numSaveBixel)) = DAOindex_next; + bixelJApVec_j(bixelJApVec_offset+(1:numSaveBixel)) = bixelIndMap(shapeMapIx); + bixelJApVec_offset = bixelJApVec_offset+numSaveBixel; + + + % updatedInfo.beam(i).shape(j).leftLeafPos_I = fracFromLastOptI.*leftLeafPos_F_last+fracFromNextOptI.*leftLeafPos_I_next; + %updatedInfo.beam(i).shape(j).rightLeafPos_I = fracFromLastOptI.*rightLeafPos_F_last+fracFromNextOptI.*rightLeafPos_I_next; + + % updatedInfo.beam(i).shape(j).leftLeafPos_F = fracFromLastOptF.*leftLeafPos_F_last+fracFromNextOptF.*leftLeafPos_I_next; + % updatedInfo.beam(i).shape(j).rightLeafPos_F = fracFromLastOptF.*rightLeafPos_F_last+fracFromNextOptF.*rightLeafPos_I_next; + + % wrt initial left (optimization vector) + % initial (interpolated arc) + bixelJApVec_vec(bixelJApVec_offset+(1:numSaveBixel)) = fracFromLastOptI(shapeMapIx).*dUl_dLI(shapeMapIx).*weight; + bixelJApVec_i(bixelJApVec_offset+(1:numSaveBixel)) = vectorIxMat_LF_last(shapeMapIx); + bixelJApVec_j(bixelJApVec_offset+(1:numSaveBixel)) = bixelIndMap(shapeMapIx); + bixelJApVec_offset = bixelJApVec_offset+numSaveBixel; + % final (interpolated arc) + bixelJApVec_vec(bixelJApVec_offset+(1:numSaveBixel)) = fracFromLastOptF(shapeMapIx).*dUl_dLF(shapeMapIx).*weight; + bixelJApVec_i(bixelJApVec_offset+(1:numSaveBixel)) = vectorIxMat_LF_last(shapeMapIx); + bixelJApVec_j(bixelJApVec_offset+(1:numSaveBixel)) = bixelIndMap(shapeMapIx); + bixelJApVec_offset = bixelJApVec_offset+numSaveBixel; + + % wrt final left (optimization vector) + % initial (interpolated arc) + bixelJApVec_vec(bixelJApVec_offset+(1:numSaveBixel)) = fracFromNextOptI(shapeMapIx).*dUl_dLI(shapeMapIx).*weight; + bixelJApVec_i(bixelJApVec_offset+(1:numSaveBixel)) = vectorIxMat_LI_next(shapeMapIx); + bixelJApVec_j(bixelJApVec_offset+(1:numSaveBixel)) = bixelIndMap(shapeMapIx); + bixelJApVec_offset = bixelJApVec_offset+numSaveBixel; + % final (interpolated arc) + bixelJApVec_vec(bixelJApVec_offset+(1:numSaveBixel)) = fracFromNextOptF(shapeMapIx).*dUl_dLF(shapeMapIx).*weight; + bixelJApVec_i(bixelJApVec_offset+(1:numSaveBixel)) = vectorIxMat_LI_next(shapeMapIx); + bixelJApVec_j(bixelJApVec_offset+(1:numSaveBixel)) = bixelIndMap(shapeMapIx); + bixelJApVec_offset = bixelJApVec_offset+numSaveBixel; + + % wrt initial right (optimization vector) + % initial (interpolated arc) + bixelJApVec_vec(bixelJApVec_offset+(1:numSaveBixel)) = -fracFromLastOptI(shapeMapIx).*dCr_dRI(shapeMapIx).*weight; + bixelJApVec_i(bixelJApVec_offset+(1:numSaveBixel)) = vectorIxMat_RF_last(shapeMapIx); + bixelJApVec_j(bixelJApVec_offset+(1:numSaveBixel)) = bixelIndMap(shapeMapIx); + bixelJApVec_offset = bixelJApVec_offset+numSaveBixel; + % final (interpolated arc) + bixelJApVec_vec(bixelJApVec_offset+(1:numSaveBixel)) = -fracFromLastOptF(shapeMapIx).*dCr_dRF(shapeMapIx).*weight; + bixelJApVec_i(bixelJApVec_offset+(1:numSaveBixel)) = vectorIxMat_RF_last(shapeMapIx); + bixelJApVec_j(bixelJApVec_offset+(1:numSaveBixel)) = bixelIndMap(shapeMapIx); + bixelJApVec_offset = bixelJApVec_offset+numSaveBixel; + + % wrt final right (optimization vector) + % initial (interpolated arc) + bixelJApVec_vec(bixelJApVec_offset+(1:numSaveBixel)) = -fracFromNextOptI(shapeMapIx).*dCr_dRI(shapeMapIx).*weight; + bixelJApVec_i(bixelJApVec_offset+(1:numSaveBixel)) = vectorIxMat_RI_next(shapeMapIx); + bixelJApVec_j(bixelJApVec_offset+(1:numSaveBixel)) = bixelIndMap(shapeMapIx); + bixelJApVec_offset = bixelJApVec_offset+numSaveBixel; + % final (interpolated arc) + bixelJApVec_vec(bixelJApVec_offset+(1:numSaveBixel)) = -fracFromNextOptF(shapeMapIx).*dCr_dRF(shapeMapIx).*weight; + bixelJApVec_i(bixelJApVec_offset+(1:numSaveBixel)) = vectorIxMat_RI_next(shapeMapIx); + bixelJApVec_j(bixelJApVec_offset+(1:numSaveBixel)) = bixelIndMap(shapeMapIx); + bixelJApVec_offset = bixelJApVec_offset+numSaveBixel; + + % wrt last time + bixelJApVec_vec(bixelJApVec_offset+(1:numSaveBixel)) = (doseAngleBordersDiff.*timeFacCurr_last) ... + .*(-fracFromLastDAO.*timeFracFromNextDAO.*(weight_last./doseAngleBordersDiff_next).*(time_next./time_last.^2) ... + +(1-fracFromLastDAO).*timeFracFromLastDAO.*(weight_next./doseAngleBordersDiff_last).*(1./time_next)) ... + * shapeMap(shapeMapIx); + bixelJApVec_i(bixelJApVec_offset+(1:numSaveBixel)) = tIx_last; + bixelJApVec_j(bixelJApVec_offset+(1:numSaveBixel)) = bixelIndMap(shapeMapIx); + bixelJApVec_offset = bixelJApVec_offset+numSaveBixel; + + % wrt next time + bixelJApVec_vec(bixelJApVec_offset+(1:numSaveBixel)) = (doseAngleBordersDiff.*timeFacCurr_next) ... + .*(fracFromLastDAO.*timeFracFromNextDAO.*(weight_last./doseAngleBordersDiff_next).*(1./time_last) ... + -(1-fracFromLastDAO).*timeFracFromLastDAO.*(weight_next./doseAngleBordersDiff_last).*(time_last./time_next.^2)) ... + * shapeMap(shapeMapIx); + bixelJApVec_i(bixelJApVec_offset+(1:numSaveBixel)) = tIx_next; + bixelJApVec_j(bixelJApVec_offset+(1:numSaveBixel)) = bixelIndMap(shapeMapIx); + bixelJApVec_offset = bixelJApVec_offset+numSaveBixel; + end + +end + +% update counters +counters.bixelJApVec_offset = bixelJApVec_offset; + +end \ No newline at end of file diff --git a/matRad/optimization/matRad_daoVec2ApertureInfo_VMATrecalcDynamic.m b/matRad/optimization/matRad_daoVec2ApertureInfo_VMATrecalcDynamic.m new file mode 100644 index 000000000..5d5346c08 --- /dev/null +++ b/matRad/optimization/matRad_daoVec2ApertureInfo_VMATrecalcDynamic.m @@ -0,0 +1,211 @@ +function updatedInfo = matRad_daoVec2ApertureInfo_VMATrecalcDynamic(apertureInfo,apertureInfoVect) +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% matRad function to translate the vector representation of the aperture +% shape and weight into an aperture info struct. At the same time, the +% updated bixel weight vector w is computed and a vector listing the +% correspondence between leaf tips and bixel indices for gradient +% calculation +% +% call +% updatedInfo = matRad_daoVec2ApertureInfo(apertureInfo,apertureInfoVect) +% +% input +% apertureInfo: aperture shape info struct +% apertureInfoVect: aperture weights and shapes parameterized as vector +% touchingFlag: if this is one, clean up instances of leaf touching, +% otherwise, do not +% +% output +% updatedInfo: updated aperture shape info struct according to apertureInfoVect +% +% References +% +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2015 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSES.txt. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% function to update the apertureInfo struct after the each iteraton of the +% optimization + +% initializing variables +updatedInfo = apertureInfo; + +updatedInfo.apertureVector = apertureInfoVect; + +% options for bixel and Jacobian calculation +mlcOptions.bixelWidth = apertureInfo.bixelWidth; +calcOptions.continuousAperture = updatedInfo.propVMAT.continuousAperture; +vectorIndices.totalNumOfShapes = apertureInfo.totalNumOfShapes; + +w = cell(apertureInfo.numPhases,1); +w(:) = {zeros(apertureInfo.totalNumOfBixels,1)}; + +if updatedInfo.runVMAT && ~all([updatedInfo.propVMAT.beam.DAOBeam]) + j = 1; + for i = 1:numel(updatedInfo.beam) + if updatedInfo.propVMAT.beam(i).DAOBeam + % update the shape weight + % rescale the weight from the vector using the previous + % iteration scaling factor + updatedInfo.beam(i).shape(j).weight = apertureInfoVect(updatedInfo.beam(i).shape(j).weightOffset)./updatedInfo.beam(i).shape(j).jacobiScale; + + updatedInfo.beam(i).shape(j).MU = updatedInfo.beam(i).shape(j).weight*updatedInfo.weightToMU; + updatedInfo.beam(i).time = apertureInfoVect((updatedInfo.totalNumOfShapes+updatedInfo.totalNumOfLeafPairs*2)+updatedInfo.propVMAT.beam(i).DAOIndex)*updatedInfo.propVMAT.beam(i).timeFacCurr; + updatedInfo.beam(i).gantryRot = updatedInfo.propVMAT.beam(i).doseAngleBordersDiff/updatedInfo.beam(i).time; + updatedInfo.beam(i).shape(j).MURate = updatedInfo.beam(i).shape(j).MU./updatedInfo.beam(i).time; + end + end +end + +bixelJApVec_vec = cell(apertureInfo.numPhases,1); + +% dummy variables +bixelJApVec_i = 0; +bixelJApVec_j = 0; +bixelJApVec_offset = 0; +counters.bixelJApVec_offset = bixelJApVec_offset; + +% Interpolate segment between adjacent optimized gantry angles. +% Include in updatedInfo, but NOT the vector (since these are not +% optimized by DAO). Also update bixel weights to include these. + + +%% update the shapeMaps +% here the new colimator positions are used to create new shapeMaps that +% now include decimal values instead of binary + +calcOptions.saveJacobian = false; + +% loop over all beams +for i = 1:numel(updatedInfo.beam) + + %posOfRightCornerPixel = apertureInfo.beam(i).posOfCornerBixel(1) + (size(apertureInfo.beam(i).bixelIndMap,2)-1)*apertureInfo.bixelWidth; + + % pre compute left and right bixel edges + edges_l = updatedInfo.beam(i).posOfCornerBixel(1)... + + ([1:size(apertureInfo.beam(i).bixelIndMap,2)]-1-1/2)*updatedInfo.bixelWidth; + edges_r = updatedInfo.beam(i).posOfCornerBixel(1)... + + ([1:size(apertureInfo.beam(i).bixelIndMap,2)]-1+1/2)*updatedInfo.bixelWidth; + + % get dimensions of 2d matrices that store shape/bixel information + n = apertureInfo.beam(i).numOfActiveLeafPairs; + + %weightFactor_I = updatedInfo.propVMAT.beam(i).doseAngleBorderCentreDiff(1)./updatedInfo.propVMAT.beam(i).doseAngleBordersDiff; + %weightFactor_F = updatedInfo.propVMAT.beam(i).doseAngleBorderCentreDiff(2)./updatedInfo.propVMAT.beam(i).doseAngleBordersDiff; + + % we are necessarily doing VMAT + numOfShapes = 1; + + mlcOptions.lim_l = apertureInfo.beam(i).lim_l; + mlcOptions.lim_r = apertureInfo.beam(i).lim_r; + mlcOptions.edges_l = edges_l; + mlcOptions.edges_r = edges_r; + mlcOptions.centres = (edges_l+edges_r)/2; + mlcOptions.widths = edges_r-edges_l; + mlcOptions.n = n; + mlcOptions.numBix = size(apertureInfo.beam(i).bixelIndMap,2); + mlcOptions.bixelIndMap = apertureInfo.beam(i).bixelIndMap; + calcOptions.DAOBeam = updatedInfo.propVMAT.beam(i).DAOBeam; + + % loop over all shapes + for j = 1:numOfShapes + + % shapeMap + shapeMap_I = zeros(size(updatedInfo.beam(i).bixelIndMap)); + shapeMap_F = zeros(size(updatedInfo.beam(i).bixelIndMap)); + % sumGradSq + sumGradSq = 0; + + % no need to update weights or anything from the vector, just + % extract the weights and leaf positions from the apertureInfo + + weight = updatedInfo.beam(i).shape(j).weight; + if isfield(updatedInfo.beam(i).shape(j),'weight_I') + weight_I = updatedInfo.beam(i).shape(j).weight_I; + weight_F = updatedInfo.beam(i).shape(j).weight_F; + else + %only happens at original angular resolution + weight_I = weight.*updatedInfo.beam(i).doseAngleBorderCentreDiff(1)./updatedInfo.beam(i).doseAngleBordersDiff; + weight_F = weight.*updatedInfo.beam(i).doseAngleBorderCentreDiff(2)./updatedInfo.beam(i).doseAngleBordersDiff; + end + + if weight_I+weight_F ~= weight + %sometimes the sum is different than one by ~10^-16 + %(rounding error in the division) + weight_F = weight-weight_I; + end + + %% enter in variables and options + + %%%%%%%%%%%%%%%% + %do initial and final arc separately, more accurate + %calculation + + %INITIAL + variables.weight_I = weight_I; + variables.weight_F = weight_I; + variables.weightFactor_I = 1/2; + variables.weightFactor_F = 1/2; + + if updatedInfo.propVMAT.continuousAperture + variables.leftLeafPos_I = updatedInfo.beam(i).shape(j).leftLeafPos_I; + variables.leftLeafPos_F = updatedInfo.beam(i).shape(j).leftLeafPos; + variables.rightLeafPos_I = updatedInfo.beam(i).shape(j).rightLeafPos_I; + variables.rightLeafPos_F = updatedInfo.beam(i).shape(j).rightLeafPos; + else + variables.leftLeafPos_I = updatedInfo.beam(i).shape(j).leftLeafPos; + variables.leftLeafPos_F = updatedInfo.beam(i).shape(j).leftLeafPos; + variables.rightLeafPos_I = updatedInfo.beam(i).shape(j).rightLeafPos; + variables.rightLeafPos_F = updatedInfo.beam(i).shape(j).rightLeafPos; + end + + % calculate bixel weight and derivative in function + [w,~,bixelJApVec_i,bixelJApVec_j,sumGradSq,shapeMap_I,counters] = ... + matRad_bixWeightAndGrad(calcOptions,mlcOptions,variables,vectorIndices,counters,w,bixelJApVec_vec,bixelJApVec_i,bixelJApVec_j,sumGradSq,shapeMap_I); + + %FINAL + variables.weight_I = weight_F; + variables.weight_F = weight_F; + variables.weightFactor_I = 1/2; + variables.weightFactor_F = 1/2; + + if updatedInfo.propVMAT.continuousAperture + variables.leftLeafPos_I = updatedInfo.beam(i).shape(j).leftLeafPos; + variables.leftLeafPos_F = updatedInfo.beam(i).shape(j).leftLeafPos_F; + variables.rightLeafPos_I = updatedInfo.beam(i).shape(j).rightLeafPos; + variables.rightLeafPos_F = updatedInfo.beam(i).shape(j).rightLeafPos_F; + else + variables.leftLeafPos_I = updatedInfo.beam(i).shape(j).leftLeafPos; + variables.leftLeafPos_F = updatedInfo.beam(i).shape(j).leftLeafPos; + variables.rightLeafPos_I = updatedInfo.beam(i).shape(j).rightLeafPos; + variables.rightLeafPos_F = updatedInfo.beam(i).shape(j).rightLeafPos; + end + + % calculate bixel weight and derivative in function + [w,~,bixelJApVec_i,bixelJApVec_j,sumGradSq,shapeMap_F,counters] = ... + matRad_bixWeightAndGrad(calcOptions,mlcOptions,variables,vectorIndices,counters,w,bixelJApVec_vec,bixelJApVec_i,bixelJApVec_j,sumGradSq,shapeMap_F); + + % save the tempMap + shapeMap = shapeMap_I+shapeMap_F; + updatedInfo.beam(i).shape(j).shapeMap = shapeMap; + end + +end + +% save bixelWeight, apertureVector, and Jacobian between the two +updatedInfo.bixelWeights = w; +updatedInfo.apertureVector = apertureInfoVect; + +end \ No newline at end of file diff --git a/matRad/optimization/projections/matRad_DoseProjection.m b/matRad/optimization/projections/matRad_DoseProjection.m index e3001973c..f3cf28c7a 100644 --- a/matRad/optimization/projections/matRad_DoseProjection.m +++ b/matRad/optimization/projections/matRad_DoseProjection.m @@ -22,8 +22,14 @@ methods function d = computeSingleScenario(~,dij,scen,w) + if ~isfield(dij,'scaleFactor') + factor = 1; + else + factor = dij.scaleFactor; + end + if ~isempty(dij.physicalDose{scen}) - d = dij.physicalDose{scen}*w; + d = dij.physicalDose{scen} * (factor * w); else d = []; matRad_cfg = MatRad_Config.instance(); @@ -45,8 +51,14 @@ end function wGrad = projectSingleScenarioGradient(~,dij,doseGrad,scen,~) + if ~isfield(dij,'scaleFactor') + factor = 1; + else + factor = dij.scaleFactor; + end + if ~isempty(dij.physicalDose{scen}) - wGrad = (doseGrad{scen}' * dij.physicalDose{scen})'; + wGrad = factor * (doseGrad{scen}' * dij.physicalDose{scen})'; else wGrad = []; matRad_cfg = MatRad_Config.instance(); diff --git a/matRad/sequencing/matRad_arcSequencing.m b/matRad/sequencing/matRad_arcSequencing.m new file mode 100644 index 000000000..3616a108c --- /dev/null +++ b/matRad/sequencing/matRad_arcSequencing.m @@ -0,0 +1,121 @@ +function beam = matRad_arcSequencing(beam,stf,pln,weightToMU) +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% The sequencing algorithm generates an a priori unkown number of apertures. +% We only want to keep a certain number of them (numToKeep). These will be +% the ones with the highest intensity-area product. +% +% +% call +% beam = +% matRad_arcSequencing(beam) +% +% input +% beam: beam struct with shapes and weights only at the +% initGantyAngles +% +% +% output +% beam: beam struct with shapes and weights distributed to +% the correct optGantryAngles +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2015 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSES.txt. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + +machine = matRad_loadMachine(pln); + +numOfBeams = numel(stf); + +leafDir = 1; + +for i = 1:numOfBeams + + if stf(i).propVMAT.FMOBeam + + %Spread apertures to each child angle + %according to the trajectory (mean leaf position). Assume that + %shapes are already order in increased (left to right) position + leafDir = -1*leafDir; + + childrenIndex = stf(i).propVMAT.beamChildrenIndex; + if leafDir == -1 + % reverse order of shapes + childrenIndex = flipud(childrenIndex); + end + + count = 1; + numOfShapes = beam(i).numOfShapes; + + for shape = 1:numOfShapes + childIndex = childrenIndex(count); + beam(childIndex).leafDir = leafDir; + + if childIndex == i + % do not overwrite information, since we will need it for + % the remaining beams (DAO, not init) + beam(childIndex).tempNumOfShapes = 1; + beam(childIndex).tempShapes = beam(i).shapes(:,:,shape); + beam(childIndex).tempShapesWeight = beam(i).shapesWeight(shape); + beam(childIndex).fluence = beam(childIndex).tempShapes; + beam(childIndex).sum = beam(childIndex).tempShapesWeight*beam(childIndex).tempShapes; + else + % don't worry about overwriting + beam(childIndex).numOfShapes = 1; + beam(childIndex).shapes = beam(i).shapes(:,:,shape); + beam(childIndex).shapesWeight = beam(i).shapesWeight(shape); + beam(childIndex).fluence = beam(childIndex).shapes; + beam(childIndex).sum = beam(childIndex).shapesWeight*beam(childIndex).shapes; + end + + count = count+1; + end + else + % if beam isn't an FMO beam, then there is no info in the beam + % struct + continue + end +end + +for i = 1:numOfBeams + % now go through and calculate gantry rotation speed, MU rate, etc. + + if stf(i).propVMAT.FMOBeam + beam(i).numOfShapes = beam(i).tempNumOfShapes; + beam(i).shapes = beam(i).tempShapes; + beam(i).shapesWeight = beam(i).tempShapesWeight; + + beam(i).tempNumOfShapes = []; + beam(i).tempShapes = []; + beam(i).tempShapesWeight = []; + + for j = 1:stf(i).propVMAT.numOfBeamSubChildren + %Prevents matRad_sequencing2ApertureInfo from attempting to + %convert shape to aperturevec for subchildren + beam(stf(i).propVMAT.beamSubChildrenIndex(j)).numOfShapes = 0; + end + end + + if stf(i).propVMAT.DAOBeam + beam(i).gantryRot = machine.constraints.gantryRotationSpeed(2); %gantry rotation rate until next opt angle + beam(i).MURate = weightToMU.*beam(i).shapesWeight.*beam(i).gantryRot./stf(i).propVMAT.DAOAngleBordersDiff; %dose rate until next opt angle + %Rescale weight to represent only this control point; weight will be shared + %with the interpolared control points in matRad_daoVec2ApertureInfo + beam(i).shapesWeight = beam(i).shapesWeight.*stf(i).propVMAT.timeFacCurr; + end +end + +beam = rmfield(beam,{'tempShapes','tempShapesWeight','tempNumOfShapes'}); + + diff --git a/matRad/sequencing/matRad_discardApertures.m b/matRad/sequencing/matRad_discardApertures.m new file mode 100644 index 000000000..07dcf2f90 --- /dev/null +++ b/matRad/sequencing/matRad_discardApertures.m @@ -0,0 +1,108 @@ +function newBeam = matRad_discardApertures(beam,numToKeep) +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% The sequencing algorithm generates an a priori unkown number of aperture. +% We only want to keep a certain number of them (numToKeep). These will be +% the ones with the highest intensity-area product. +% +% +% call +% beam = +% matRad_discardApertures(beam,numToKeep) +% +% input +% beam: beam struct containing original shapes and +% intensities +% +% numToKeep: number of apertures to keep +% +% output +% beam: beam struct containing shapes and re-scaled +% intensities of the apertures we are keeping +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2015 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSES.txt. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + +newBeam = beam; +newBeam.shapes = zeros(size(newBeam.shapes,1),size(newBeam.shapes,2),numToKeep); +newBeam.shapesWeight = zeros(numToKeep,1); + +%Find the numToKeep apertures having the highest dose-area product +numToKeep = min(numToKeep,beam.numOfShapes); + +DAP = zeros(beam.numOfShapes,1); +comPos = zeros(beam.numOfShapes,1); + +for shape = 1:beam.numOfShapes + DAP(shape) = nnz(beam.shapes(:,:,shape)).*beam.shapesWeight(shape); + x = repmat(1:size(beam.shapes(:,:,shape),2),size(beam.shapes(:,:,shape),1),1); + comPosRow = sum(beam.shapes(:,:,shape).*x,2)./sum(beam.shapes(:,:,shape),2); + comPos(shape) = mean(comPosRow(~isnan(comPosRow),1)); +end + +%{ +% This code will sort the aperture shapes in increasing order of centre of mass. +However, some algorithms (in particular, Siochi) already sort shapes in +increasing (left to right) leaf position. So this is not necessary (and +will in fact some times change the order of the apertures generated by +Siochi). + +% sort comPos into ascending order +[comPosSorted,comPosSortInd] = sort(beam.comPos); + +if any(comPosSorted ~= beam.comPos) + tempShapes = zeros(size(beam.shapes)); + tempShapesWeight = zeros(size(beam.shapesWeight)); + tempDAP = zeros(size(beam.DAP)); + + for shape = 1:beam.numOfShapes + ind = comPosSortInd(shape); + + tempShapes(:,:,shape) = beam.shapes(:,:,ind); + tempShapesWeight(shape) = beam.shapesWeight(ind); + tempDAP(shape) = beam.DAP(ind); + end + + % now the shapes should be sorted in increasing center of mass position + beam.shapes = tempShapes; + beam.shapesWeight = tempShapesWeight; + beam.DAP = tempDAP; + beam.comPos = comPosSorted; +end +%} + +[~,comPosToDAPSort] = sort(DAP,'descend'); + +totDAP_all = sum(DAP(:)); +totDAP_keep = sum(DAP(comPosToDAPSort(1:numToKeep))); + +segmentKeep = 1; + +%Keep only those numToKeep apertures with the highest DAP +%Preserve the shapes of the apertures, but scale the weights so +%that the total DAP is kept +for shape = 1:beam.numOfShapes + if comPosToDAPSort(shape) <= numToKeep + newBeam.shapes(:,:,segmentKeep) = beam.shapes(:,:,shape); + tempNewDAP = totDAP_all*DAP(shape)/totDAP_keep; + newBeam.shapesWeight(segmentKeep) = tempNewDAP/(nnz(newBeam.shapes(:,:,segmentKeep))); %sequencing.beam.shapesWeight(sequencing.beam.segmentSortedDAP(segment)) + + segmentKeep = segmentKeep+1; + else + continue + end +end + +newBeam.numOfShapes = numToKeep; diff --git a/matRad/sequencing/matRad_engelLeafSequencing.m b/matRad/sequencing/matRad_engelLeafSequencing.m index a1d944374..9fe4b35e2 100644 --- a/matRad/sequencing/matRad_engelLeafSequencing.m +++ b/matRad/sequencing/matRad_engelLeafSequencing.m @@ -1,17 +1,17 @@ -function resultGUI = matRad_engelLeafSequencing(resultGUI,stf,dij,numOfLevels,visBool) -% multileaf collimator leaf sequencing algorithm -% for intensity modulated beams with multiple static segments accroding -% to Engel et al. 2005 Discrete Applied Mathematics +function resultGUI = matRad_engelLeafSequencing(resultGUI,stf,dij,pln,visBool) +% multileaf collimator leaf sequencing algorithm for intensity modulated +% beams with multiple static segments accroding to Engel et al. 2005 +% Discrete Applied Mathematics % % call -% resultGUI = matRad_engelLeafSequencing(resultGUI,stf,dij,numOfLevels,visBool) +% resultSequencing = matRad_engelSequencing(w,stf,pln,pln,visBool) % % input % resultGUI: resultGUI struct to which the output data will be added, if % this field is empty resultGUI struct will be created % stf: matRad steering information struct % dij: matRad's dij matrix -% numOfLevels: number of stratification levels +% pln: pln struct % visBool: toggle on/off visualization (optional) % % output @@ -34,6 +34,8 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +matRad_cfg = MatRad_Config.instance(); + % if visBool not set toogle off visualization if nargin < 5 visBool = 0; @@ -50,6 +52,10 @@ seqFig = figure('position',[xpos,ypos,sz(2),sz(1)]); end +if ~isfield(pln,'propSeq') || ~isfield(pln.propSeq,'numLevels') + pln.propSeq.numLevels = matRad_cfg.defaults.propSeq.numLevels; +end + offset = 0; for i = 1:numOfBeams @@ -92,7 +98,7 @@ % Stratification calFac = max(fluenceMx(:)); - D_k = round(fluenceMx/calFac*numOfLevels); + D_k = round(fluenceMx/calFac*pln.propSeq.numLevels); % Save the stratification in the initial intensity matrix D_0. D_0 = D_k; @@ -108,7 +114,7 @@ seqSubPlots(1) = subplot(2,2,1,'parent',seqFig); imagesc(D_k,'parent',seqSubPlots(1)); - set(seqSubPlots(1),'CLim',[0 numOfLevels],'YDir','normal'); + set(seqSubPlots(1),'CLim',[0 pln.propSeq.numLevels],'YDir','normal'); title(seqSubPlots(1),['Beam # ' num2str(i) ': max(D_0) = ' num2str(max(D_0(:))) ' - ' num2str(numel(unique(D_0))) ' intensity levels']); xlabel(seqSubPlots(1),'x - direction parallel to leaf motion ') ylabel(seqSubPlots(1),'z - direction perpendicular to leaf motion ') @@ -136,7 +142,7 @@ if visBool seqSubPlots(2) = subplot(2,2,2,'parent',seqFig); imagesc(D_k,'parent',seqSubPlots(2)); - set(seqSubPlots(2),'CLim',[0 numOfLevels],'YDir','normal'); + set(seqSubPlots(2),'CLim',[0 pln.propSeq.numLevels],'YDir','normal'); title(seqSubPlots(2),['k = ' num2str(k)]); colorbar drawnow @@ -355,11 +361,11 @@ sequencing.beam(i).numOfShapes = k; sequencing.beam(i).shapes = shapes(:,:,1:k); - sequencing.beam(i).shapesWeight = shapesWeight(1:k)/numOfLevels*calFac; + sequencing.beam(i).shapesWeight = shapesWeight(1:k)/pln.propSeq.numLevels*calFac; sequencing.beam(i).bixelIx = 1+offset:numOfRaysPerBeam+offset; sequencing.beam(i).fluence = D_0; - sequencing.w(1+offset:numOfRaysPerBeam+offset,1) = D_0(indInFluenceMx)/numOfLevels*calFac; + sequencing.w(1+offset:numOfRaysPerBeam+offset,1) = D_0(indInFluenceMx)/pln.propSeq.numLevels*calFac; offset = offset + numOfRaysPerBeam; @@ -369,7 +375,7 @@ resultGUI.wSequenced = sequencing.w; resultGUI.sequencing = sequencing; -resultGUI.apertureInfo = matRad_sequencing2ApertureInfo(sequencing,stf); +resultGUI.apertureInfo = matRad_sequencing2ApertureInfo(sequencing,stf,pln); doseSequencedDoseGrid = reshape(dij.physicalDose{1} * sequencing.w,dij.doseGrid.dimensions); % interpolate to ct grid for visualiation & analysis diff --git a/matRad/sequencing/matRad_leafTouching.m b/matRad/sequencing/matRad_leafTouching.m new file mode 100644 index 000000000..a55f61a0f --- /dev/null +++ b/matRad/sequencing/matRad_leafTouching.m @@ -0,0 +1,178 @@ +function apertureInfo = matRad_leafTouching(apertureInfo) +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% matRad function to improve instances of leaf touching by moving leaves +% from the centre to sweep with the non-touching leaves. +% +% Currently only works with VMAT, add option to work with IMRT (not as +% crucial) +% +% call +% apertureInfo = matRad_leafTouching(apertureInfo) +% +% input +% apertureInfo: matRad aperture weight and shape info struct +% +% output +% apertureInfo: matRad aperture weight and shape info struct +% +% References +% +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2015 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSES.txt. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + +%initialize +dimZ = apertureInfo.beam(1).numOfActiveLeafPairs; +numBeams = nnz([apertureInfo.propVMAT.beam.DAOBeam]); +if ~isfield(apertureInfo.beam(1).shape(1),'leftLeafPos_I') + % Each non-interpolated beam should have 1 left/right leaf position + leftLeafPoss = nan(dimZ,numBeams); + rightLeafPoss = nan(dimZ,numBeams); + gantryAngles = zeros(1,numBeams); +else + % Each non-interpolated beam should have 2 left/right leaf positions + leftLeafPoss = nan(dimZ,2*numBeams); + rightLeafPoss = nan(dimZ,2*numBeams); + gantryAngles = zeros(1,2*numBeams); +end +initBorderGantryAngles = unique([apertureInfo.propVMAT.beam.FMOAngleBorders]); +initBorderLeftLeafPoss = nan(dimZ,numel(initBorderGantryAngles)); + +l = 1; +m = 1; +%collect all leaf positions +for k = 1:numel(apertureInfo.beam) + if (k ~= 1 && apertureInfo.beam(k).gantryAngle == apertureInfo.beam(k-1).gantryAngle) || ~apertureInfo.propVMAT.beam(k).DAOBeam + continue + end + + if ~isfield(apertureInfo.beam(1).shape(1),'leftLeafPos_I') + leftLeafPoss(:,l) = apertureInfo.beam(k).shape(1).leftLeafPos; + rightLeafPoss(:,l) = apertureInfo.beam(k).shape(1).rightLeafPos; + gantryAngles(l) = apertureInfo.beam(k).gantryAngle; + + l = l+1; + else + leftLeafPoss(:,l) = apertureInfo.beam(k).shape(1).leftLeafPos_I; + rightLeafPoss(:,l) = apertureInfo.beam(k).shape(1).rightLeafPos_I; + gantryAngles(l) = apertureInfo.beam(k).doseAngleBorders(1); + + l = l+1; + + leftLeafPoss(:,l) = apertureInfo.beam(k).shape(1).leftLeafPos_F; + rightLeafPoss(:,l) = apertureInfo.beam(k).shape(1).rightLeafPos_F; + gantryAngles(l) = apertureInfo.beam(k).doseAngleBorders(2); + + l = l+1; + end + + %Only important when cleaning up instances of opposing + %leaves touching. + if apertureInfo.propVMAT.beam(k).FMOBeam + if apertureInfo.propVMAT.beam(k).leafDir == 1 + %This means that the current arc sector is moving + %in the normal direction (L-R). + initBorderLeftLeafPoss(:,m) = apertureInfo.beam(k).lim_l; + + elseif apertureInfo.propVMAT.beam(k).leafDir == -1 + %This means that the current arc sector is moving + %in the reverse direction (R-L). + initBorderLeftLeafPoss(:,m) = apertureInfo.beam(k).lim_r; + end + m = m+1; + + %end of last sector + if m == numel(initBorderGantryAngles) + %This gives ending angle of the current sector. + if apertureInfo.propVMAT.beam(k).leafDir == 1 + %This means that the current arc sector is moving + %in the normal direction (L-R), so the next arc + %sector is moving opposite + initBorderLeftLeafPoss(:,m) = apertureInfo.beam(k).lim_r; + elseif apertureInfo.propVMAT.beam(k).leafDir == -1 + %This means that the current arc sector is moving + %in the reverse direction (R-L), so the next + %arc sector is moving opposite + initBorderLeftLeafPoss(:,m) = apertureInfo.beam(k).lim_l; + end + end + end +end + +[gantryAngles,ind] = unique(gantryAngles); +leftLeafPoss = leftLeafPoss(:,ind); +rightLeafPoss = rightLeafPoss(:,ind); + +%Any time leaf pairs are touching, they are set to +%be in the middle of the field. Instead, move them +%so that they are still touching, but that they +%follow the motion of the MLCs across the field. +for row = 1:dimZ + + touchingInd = find(leftLeafPoss(row,:) == rightLeafPoss(row,:)); + + if ~exist('leftLeafPossAug','var') + %leftLeafPossAug = [reshape(mean([leftLeafPoss(:) rightLeafPoss(:)],2),size(leftLeafPoss)),borderLeftLeafPoss]; + leftLeafPossAugTemp = reshape(mean([leftLeafPoss(:) rightLeafPoss(:)],2),size(leftLeafPoss)); + + numRep = 0; + repInd = nan(size(gantryAngles)); + for j = 1:numel(gantryAngles) + if any(gantryAngles(j) == initBorderGantryAngles) + %replace leaf positions with the ones at + %the borders (eliminates repetitions) + numRep = numRep+1; + %these are the gantry angles that are + %repeated + repInd(numRep) = j; + + delInd = find(gantryAngles(j) == initBorderGantryAngles); + leftLeafPossAugTemp(:,j) = initBorderLeftLeafPoss(:,delInd); + initBorderLeftLeafPoss(:,delInd) = []; + initBorderGantryAngles(delInd) = []; + end + end + repInd(isnan(repInd)) = []; + leftLeafPossAug = [leftLeafPossAugTemp,initBorderLeftLeafPoss]; + gantryAnglesAug = [gantryAngles,initBorderGantryAngles]; + end + notTouchingInd = [setdiff(1:numBeams,touchingInd),repInd]; + notTouchingInd = unique(notTouchingInd); + %make sure to include the repeated ones in the + %interpolation! + + notTouchingIndAug = [notTouchingInd,(1+numel(gantryAngles)):(numel(gantryAngles)+numel(initBorderGantryAngles))]; + + leftLeafPoss(row,touchingInd) = interp1(gantryAnglesAug(notTouchingIndAug),leftLeafPossAug(row,notTouchingIndAug),gantryAngles(touchingInd))-0.5; + rightLeafPoss(row,touchingInd) = leftLeafPoss(row,touchingInd)+1; +end + + +%finally, set new leaf positions +for i = 1:numel(apertureInfo.beam) + apertureInfo.beam(i).shape(1).leftLeafPos = max((interp1(gantryAngles',leftLeafPoss',apertureInfo.beam(i).gantryAngle))',apertureInfo.beam(i).lim_l); + apertureInfo.beam(i).shape(1).rightLeafPos = min((interp1(gantryAngles',rightLeafPoss',apertureInfo.beam(i).gantryAngle))',apertureInfo.beam(i).lim_r); + + apertureInfo.beam(i).shape(1).leftLeafPos_I = max((interp1(gantryAngles',leftLeafPoss',apertureInfo.propVMAT.beam(i).doseAngleBorders(1)))',apertureInfo.beam(i).lim_l); + apertureInfo.beam(i).shape(1).rightLeafPos_I = min((interp1(gantryAngles',rightLeafPoss',apertureInfo.propVMAT.beam(i).doseAngleBorders(1)))',apertureInfo.beam(i).lim_r); + + apertureInfo.beam(i).shape(1).leftLeafPos_F = max((interp1(gantryAngles',leftLeafPoss',apertureInfo.propVMAT.beam(i).doseAngleBorders(2)))',apertureInfo.beam(i).lim_l); + apertureInfo.beam(i).shape(1).rightLeafPos_F = min((interp1(gantryAngles',rightLeafPoss',apertureInfo.propVMAT.beam(i).doseAngleBorders(2)))',apertureInfo.beam(i).lim_r); +end + + +end + diff --git a/matRad/sequencing/matRad_maxLeafSpeed.m b/matRad/sequencing/matRad_maxLeafSpeed.m new file mode 100644 index 000000000..939b4bebd --- /dev/null +++ b/matRad/sequencing/matRad_maxLeafSpeed.m @@ -0,0 +1,194 @@ +function apertureInfo = matRad_maxLeafSpeed(apertureInfo) +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% matRad calculation of maximum leaf speed +% +% call +% apertureInfo = matRad_maxLeafSpeed(apertureInfo) +% +% input +% apertureInfo: aperture info struct +% +% output +% apertureInfo: aperture info struct +% +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2015 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSES.txt. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + +apertureInfoVec = apertureInfo.apertureVector; + +% values of time differences of optimized gantry angles +timeDAOBorderAngles = apertureInfoVec(1+(apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2):end); + +% find values of leaf speeds of optimized gantry angles +if apertureInfo.continuousAperture + % Using the dynamic fluence calculation, we have the leaf positions in + % the vector be the leaf positions at the borders of the Dij arcs (for optimized angles only). + % Therefore we must also use the times between the borders of the Dij + % arc (for optimized angles only). + timeFac = [apertureInfo.propVMAT.beam.timeFac]'; + deleteInd = timeFac == 0; + timeFac(deleteInd) = []; + + i = [apertureInfo.propVMAT.beam.timeFacInd]'; + i(deleteInd) = []; + + j = repelem(1:apertureInfo.totalNumOfShapes,1,3); + j(deleteInd) = []; + + timeFacMatrix = sparse(i,j,timeFac,max(i),apertureInfo.totalNumOfShapes); + timeBNOptAngles = timeFacMatrix*timeDAOBorderAngles; + + % prep + leftLeafDiff = zeros(apertureInfo.propVMAT.numLeafSpeedConstraint*apertureInfo.beam(1).numOfActiveLeafPairs,1); + rightLeafDiff = zeros(apertureInfo.propVMAT.numLeafSpeedConstraint*apertureInfo.beam(1).numOfActiveLeafPairs,1); + tVec = zeros(apertureInfo.propVMAT.numLeafSpeedConstraint*apertureInfo.beam(1).numOfActiveLeafPairs,1); + maxLeafSpeed = zeros(1,max(i)); + + offset = 0; + shapeInd = 1; + + for i = 1:numel(apertureInfo.beam) + % loop over beams + n = apertureInfo.beam(i).numOfActiveLeafPairs; + + if ~isempty(apertureInfo.propVMAT.beam(i).leafConstMask) + + % get vector indices + if apertureInfo.propVMAT.beam(i).DAOBeam + % if it's a DAO beam, use own vector offset + vectorIx_LI = apertureInfo.beam(i).shape(1).vectorOffset(1) + ((1:n)-1); + vectorIx_LF = apertureInfo.beam(i).shape(1).vectorOffset(2) + ((1:n)-1); + else + % otherwise, use vector offset of previous and next + % beams + vectorIx_LI = apertureInfo.beam(apertureInfo.propVMAT.beam(i).lastDAOIndex).shape(1).vectorOffset(2) + ((1:n)-1); + vectorIx_LF = apertureInfo.beam(apertureInfo.propVMAT.beam(i).nextDAOIndex).shape(1).vectorOffset(1) + ((1:n)-1); + end + vectorIx_RI = vectorIx_LI+apertureInfo.totalNumOfLeafPairs; + vectorIx_RF = vectorIx_LF+apertureInfo.totalNumOfLeafPairs; + + % extract leaf positions, time + leftLeafPos_I = apertureInfoVec(vectorIx_LI); + rightLeafPos_I = apertureInfoVec(vectorIx_RI); + leftLeafPos_F = apertureInfoVec(vectorIx_LF); + rightLeafPos_F = apertureInfoVec(vectorIx_RF); + t = timeBNOptAngles(shapeInd); + + % determine indices + indInDiffVec = offset+(1:n); + + % insert differences, time + leftLeafDiff(indInDiffVec) = abs(leftLeafPos_F-leftLeafPos_I); + rightLeafDiff(indInDiffVec) = abs(rightLeafPos_F-rightLeafPos_I); + tVec(indInDiffVec) = t; + + % get max speed + leftLeafSpeed = abs(leftLeafPos_F-leftLeafPos_I)./t; + rightLeafSpeed = abs(leftLeafPos_F-leftLeafPos_I)./t; + maxLeafSpeed_temp = max([leftLeafSpeed; rightLeafSpeed]); + + % update max speed + if maxLeafSpeed_temp > maxLeafSpeed(shapeInd) + maxLeafSpeed(shapeInd) = maxLeafSpeed_temp; + end + + % update offset + offset = offset+n; + + % increment shapeInd only for beams which have transtion + % defined + shapeInd = shapeInd+1; + end + end +else + % value of constraints for leaves + %leftLeafPos = apertureInfoVec([1:apertureInfo.totalNumOfLeafPairs]+apertureInfo.totalNumOfShapes); + %rightLeafPos = apertureInfoVec(1+apertureInfo.totalNumOfLeafPairs+apertureInfo.totalNumOfShapes:apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2); + leftLeafPos = apertureInfoVec((1:(apertureInfo.totalNumOfLeafPairs))+apertureInfo.totalNumOfShapes); + rightLeafPos = apertureInfoVec(1+(apertureInfo.totalNumOfLeafPairs+apertureInfo.totalNumOfShapes):(apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2)); + + % Using the static fluence calculation, we have the leaf positions in + % the vector be the leaf positions at the centre of the Dij arcs (for optimized angles only). + % Therefore we must use the times between the centres of the Dij arcs (for optimized angles only). + i = sort(repmat(1:(apertureInfo.totalNumOfShapes-1),1,2)); + j = sort(repmat(1:apertureInfo.totalNumOfShapes,1,2)); + j(1) = []; + j(end) = []; + + timeFac = [apertureInfo.propVMAT.beam.timeFac]'; + timeFac(1) = []; + timeFac(end) = []; + + timeFacMatrix = sparse(i,j,timeFac,(apertureInfo.totalNumOfShapes-1),apertureInfo.totalNumOfShapes); + timeBNOptAngles = timeFacMatrix*timeDAOBorderAngles; + + leftLeafSpeed = abs(diff(reshape(leftLeafPos,apertureInfo.beam(1).numOfActiveLeafPairs,[]),1,2))./repmat(timeBNOptAngles',apertureInfo.beam(1).numOfActiveLeafPairs,1); + rightLeafSpeed = abs(diff(reshape(rightLeafPos,apertureInfo.beam(1).numOfActiveLeafPairs,[]),1,2))./repmat(timeBNOptAngles',apertureInfo.beam(1).numOfActiveLeafPairs,1); + + % values of max leaf speeds + leftMaxLeafSpeed = max(leftLeafSpeed,[],1); + rightMaxLeafSpeed = max(rightLeafSpeed,[],1); + maxLeafSpeed = max([leftMaxLeafSpeed; rightMaxLeafSpeed],[],1); +end + + +% enter into apertureInfo +l = 1; +maxMaxLeafSpeed = 0; +for i = 1:size(apertureInfo.beam,2) + if apertureInfo.propVMAT.beam(i).DAOBeam + if apertureInfo.continuousAperture + % for dynamic, we take the max leaf speed to be the actual leaf + % speed + ind = apertureInfo.propVMAT.beam(i).timeFacInd(apertureInfo.propVMAT.beam(i).timeFac ~= 0); + + apertureInfo.beam(i).maxLeafSpeed = max(maxLeafSpeed(ind)); + if apertureInfo.beam(i).maxLeafSpeed >= maxMaxLeafSpeed + maxMaxLeafSpeed = apertureInfo.beam(i).maxLeafSpeed; + end + else + % for static, we take the max leaf speed to be the max leaf + % of two speeds, one being the speed in the first half-arc, the + % second being the speed in the second half-arc (these will be + % different in general) + + if l == 1 + apertureInfo.beam(i).maxLeafSpeed = maxLeafSpeed(l); + elseif l == apertureInfo.totalNumOfShapes + apertureInfo.beam(i).maxLeafSpeed = maxLeafSpeed(l-1); + else + %apertureInfo.beam(i).maxLeafSpeed = maxLeafSpeed(l-1)*apertureInfo.beam(i).timeFac(1)+maxLeafSpeed(l)*apertureInfo.beam(i).timeFac(2); + apertureInfo.beam(i).maxLeafSpeed = max(maxLeafSpeed(l-1),maxLeafSpeed(l)); + end + + + if l < apertureInfo.totalNumOfShapes && maxLeafSpeed(l) >= maxMaxLeafSpeed + maxMaxLeafSpeed = maxLeafSpeed(l); + end + end + + + if l < apertureInfo.totalNumOfShapes && maxLeafSpeed(l) >= maxMaxLeafSpeed + maxMaxLeafSpeed = maxLeafSpeed(l); + end + + l = l+1; + end +end + +apertureInfo.maxLeafSpeed = maxMaxLeafSpeed; + diff --git a/matRad/sequencing/matRad_optDelivery.m b/matRad/sequencing/matRad_optDelivery.m new file mode 100644 index 000000000..f8b1f79c2 --- /dev/null +++ b/matRad/sequencing/matRad_optDelivery.m @@ -0,0 +1,109 @@ +function result = matRad_optDelivery(result,fast) +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% matRad: optimize VMAT delivery +% +% call +% matRad_optDelivery(result,fast) +% +% input +% result: result struct from fluence optimization/sequencing +% fast: 1 => fastest possible delivery +% 0 => mutliply delivery time by 10% +% +% output +% apertureInfo: aperture shape info struct +% +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2016 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSES.txt. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +if nargin < 3 + fast = 1; +end + +matRad_cfg = MatRad_Config.instance(); + + +%speed up delivery time, when it is permitted by constraints +%constraints to consider: doserate, leaf speed, and gantry speed + +%Do this after DAO + +apertureInfo = result.apertureInfo; + +%calculate max leaf speed +apertureInfo = matRad_maxLeafSpeed(apertureInfo); + +doInterp = false; + +for i = 1:size(apertureInfo.beam,2) + if apertureInfo.propVMAT.beam(i).DAOBeam + + %all of these should be greater than 1, since DAO respects the + %constraints + + %if one of them is less than 1, then a constraint is violated + factorMURate = apertureInfo.propVMAT.constraints.monitorUnitRate(2)/apertureInfo.beam(i).shape(1).MURate; + factorLeafSpeed = apertureInfo.propVMAT.constraints.leafSpeed(2)/apertureInfo.beam(i).maxLeafSpeed; + factorGantryRot = apertureInfo.propVMAT.constraints.gantryRotationSpeed(2)/apertureInfo.beam(i).gantryRot; + + %The constraint that is limiting the speed the most is the one + %whose factor is closest to 1 + factor = min([factorMURate factorLeafSpeed factorGantryRot]); + if ~fast + %if the limiting rate is already 10% lower than the limit, + %then do nothing (factor = 1) + %otherwise, scale rates so that the limiting rate is 10% lower + %than the limit + factor = min([1 factor*0.9]); + end + + %multiply each speed by this factor + apertureInfo.beam(i).shape.MURate = factor*apertureInfo.beam(i).shape.MURate; + apertureInfo.beam(i).maxLeafSpeed = factor*apertureInfo.beam(i).maxLeafSpeed; + apertureInfo.beam(i).gantryRot = factor*apertureInfo.beam(i).gantryRot; + apertureInfo.beam(i).time = apertureInfo.beam(i).time/factor; + + factorMURate = apertureInfo.propVMAT.constraints.monitorUnitRate(1)/apertureInfo.beam(i).shape(1).MURate; + + if factorMURate > 1 + apertureInfo.beam(i).shape(1).MURate = factorMURate*apertureInfo.beam(i).shape(1).MURate; + apertureInfo.beam(i).shape(1).weight = factorMURate*apertureInfo.beam(i).shape(1).weight; + + doInterp = true; + end + end +end + +% doInterp is set to true if, during the previous step, the dose rate +% somehow was lower than the minimum dose rate. This can happen if the +% gantry speed has to go low enough to accomodate a slowly-moving leaf. In +% this case, the weight has to increase to bring the dose rate above the +% minimum threshold. If this is done, the bixel weights will be different. +if doInterp + matRad_cfg.dispInfo('Interpolation needs to be redone due to low dose rate.\n'); +end + +%recalculate vector with new times + +%%%LOOK PAST THIS +[apertureInfo.apertureVector,~,~] = matRad_OptimizationProblemVMAT.matRad_daoApertureInfo2Vec(apertureInfo); + +%redo interpolation +apertureInfo = matRad_OptimizationProblemVMAT.matRad_daoVec2ApertureInfo(apertureInfo,apertureInfo.apertureVector); + + +result.apertureInfo = apertureInfo; + diff --git a/matRad/sequencing/matRad_sequencing2ApertureInfo.m b/matRad/sequencing/matRad_sequencing2ApertureInfo.m index 867cabd43..7098a087e 100644 --- a/matRad/sequencing/matRad_sequencing2ApertureInfo.m +++ b/matRad/sequencing/matRad_sequencing2ApertureInfo.m @@ -1,6 +1,6 @@ -function apertureInfo = matRad_sequencing2ApertureInfo(Sequencing,stf) -% matRad function to generate a shape info struct -% based on the result of multileaf collimator sequencing +function apertureInfo = matRad_sequencing2ApertureInfo(sequencing,stf,pln) +% matRad function to generate a shape info struct based on the result of +% multileaf collimator sequencing % % call % apertureInfo = matRad_sequencing2ApertureInfo(Sequencing,stf) @@ -13,7 +13,6 @@ % apertureInfo: matRad aperture weight and shape info struct % % References -% % - % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % @@ -28,6 +27,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + % MLC parameters: bixelWidth = stf(1).bixelWidth; % [mm] numOfMLCLeafPairs = 80; @@ -39,8 +39,27 @@ % initializing variables bixelIndOffset = 0; % used for creation of bixel index maps totalNumOfBixels = sum([stf(:).totalNumOfBixels]); -totalNumOfShapes = sum([Sequencing.beam.numOfShapes]); +totalNumOfShapes = sum([sequencing.beam.numOfShapes]); +weightOffset = 1; vectorOffset = totalNumOfShapes + 1; % used for bookkeeping in the vector for optimization +bixOffset = 1; %used for gradient calculations +interpGetsTransition = false; % boolean to determine if an interpolated beam is responsible for a leaf speed constraint check + +if ~isfield(pln, 'propOpt') || ~isfield(pln.propOpt,'runVMAT') + pln.propOpt.runVMAT = false; +end + +if ~isfield(pln, 'propStf') || ~isfield(pln.propStf,'continuousAperture') + pln.propStf.continuousAperture = false; +end + +if pln.propOpt.runVMAT + totalNumOfOptBixels = 0; + totalNumOfLeafPairs = 0; + apertureInfo.propVMAT.jacobT = zeros(sum([sequencing.beam.numOfShapes]),numel(sequencing.beam)); +end + +apertureInfo.jacobiScale = ones(totalNumOfShapes,1); % loop over all beams for i=1:size(stf,2) @@ -53,12 +72,12 @@ Z = rayPos_bev(3,:)'; % create ray-map - maxX = max(X); minX = min(X); + maxX = max(X); minX = min(X); maxZ = max(Z); minZ = min(Z); dimX = (maxX-minX)/stf(i).bixelWidth + 1; dimZ = (maxZ-minZ)/stf(i).bixelWidth + 1; - + rayMap = zeros(dimZ,dimX); % get indices for x and z positions @@ -67,13 +86,13 @@ % get indices in the ray-map indInRay = zPos + (xPos-1)*dimZ; - + % fill ray-map rayMap(indInRay) = 1; % create map of bixel indices bixelIndMap = NaN * ones(dimZ,dimX); - bixelIndMap(indInRay) = [1:stf(i).numOfRays] + bixelIndOffset; + bixelIndMap(indInRay) = (1:stf(i).numOfRays) + bixelIndOffset; bixelIndOffset = bixelIndOffset + stf(i).numOfRays; % store physical position of first entry in bixelIndMap @@ -90,70 +109,96 @@ lim_l(l) = (lim_lInd-1)*bixelWidth + minX - 1/2*bixelWidth; lim_r(l) = (lim_rInd-1)*bixelWidth + minX + 1/2*bixelWidth; end - + % get leaf positions for all shapes % leaf positions can be extracted from the shapes created in Sequencing - for m = 1:Sequencing.beam(i).numOfShapes + for m = 1:sequencing.beam(i).numOfShapes - % loading shape from Sequencing result - shapeMap = Sequencing.beam(i).shapes(:,:,m); - % get left and right leaf indices from shapemap - % initializing limits - leftLeafPos = NaN * ones(dimZ,1); - rightLeafPos = NaN * ones(dimZ,1); - % looping over leaf pairs - for l = 1:dimZ - leftLeafPosInd = find(shapeMap(l,:),1,'first'); - rightLeafPosInd = find(shapeMap(l,:),1,'last'); - - if isempty(leftLeafPosInd) && isempty(rightLeafPosInd) % if no bixel is open, use limits from Ray positions - leftLeafPos(l) = (lim_l(l)+lim_r(l))/2; - rightLeafPos(l) = leftLeafPos(l); - else - % the physical position [mm] can be calculated from the indices - leftLeafPos(l) = (leftLeafPosInd-1)*bixelWidth... - + minX - 1/2*bixelWidth; - rightLeafPos(l) = (rightLeafPosInd-1)*bixelWidth... - + minX + 1/2*bixelWidth; - + if isfield(sequencing.beam(i),'shapes') + + % loading shape from Sequencing result + shapeMap = sequencing.beam(i).shapes(:,:,m); + % get left and right leaf indices from shapemap + % initializing limits + leftLeafPos = NaN * ones(dimZ,1); + rightLeafPos = NaN * ones(dimZ,1); + % looping over leaf pairs + for l = 1:dimZ + leftLeafPosInd = find(shapeMap(l,:),1,'first'); + rightLeafPosInd = find(shapeMap(l,:),1,'last'); + + if isempty(leftLeafPosInd) && isempty(rightLeafPosInd) % if no bixel is open, use limits from Ray positions + leftLeafPos(l) = (lim_l(l)+lim_r(l))/2; + rightLeafPos(l) = leftLeafPos(l); + else + % the physical position [mm] can be calculated from the indices + leftLeafPos(l) = (leftLeafPosInd-1)*bixelWidth... + + minX - 1/2*bixelWidth; + rightLeafPos(l) = (rightLeafPosInd-1)*bixelWidth... + + minX + 1/2*bixelWidth; + + %Can happen in some cases in SW trajectory sampling + if leftLeafPos(l) < lim_l(l) + leftLeafPos(l) = lim_l(l); + end + if rightLeafPos(l) > lim_r(l) + rightLeafPos(l) = lim_r(l); + end + + end end + + % save data for each shape of this beam + apertureInfo.beam(i).shape(m).leftLeafPos = leftLeafPos; + apertureInfo.beam(i).shape(m).rightLeafPos = rightLeafPos; + apertureInfo.beam(i).shape(m).weight = sequencing.beam(i).shapesWeight(m); + apertureInfo.beam(i).shape(m).shapeMap = shapeMap; end - % save data for each shape of this beam - apertureInfo.beam(i).shape(m).leftLeafPos = leftLeafPos; - apertureInfo.beam(i).shape(m).rightLeafPos = rightLeafPos; - apertureInfo.beam(i).shape(m).weight = Sequencing.beam(i).shapesWeight(m); - apertureInfo.beam(i).shape(m).shapeMap = shapeMap; - apertureInfo.beam(i).shape(m).vectorOffset = vectorOffset; + if pln.propOpt.runVMAT + apertureInfo.beam(i).shape(m).MURate = sequencing.beam(i).MURate; + end - % update index for bookkeeping - vectorOffset = vectorOffset + dimZ; - - end + apertureInfo.beam(i).shape(m).jacobiScale = 1; - % z-coordinates of active leaf pairs + if pln.propOpt.runVMAT && pln.propStf.continuousAperture + apertureInfo.beam(i).shape(m).vectorOffset = [vectorOffset vectorOffset+dimZ]; + + % update index for bookkeeping + vectorOffset = vectorOffset + dimZ*nnz(stf(i).propVMAT.doseAngleDAO); + else + apertureInfo.beam(i).shape(m).vectorOffset = vectorOffset; + + % update index for bookkeeping + vectorOffset = vectorOffset + dimZ; + end + apertureInfo.beam(i).shape(m).weightOffset = weightOffset; + weightOffset = weightOffset+1; + end + + % z-coordinates of active leaf pairs % get z-coordinates from bixel positions leafPairPos = unique(Z); - + % find upmost and downmost leaf pair topLeafPairPos = maxZ; bottomLeafPairPos = minZ; topLeafPair = centralLeafPair - topLeafPairPos/bixelWidth; bottomLeafPair = centralLeafPair - bottomLeafPairPos/bixelWidth; - + % create bool map of active leaf pairs isActiveLeafPair = zeros(numOfMLCLeafPairs,1); isActiveLeafPair(topLeafPair:bottomLeafPair) = 1; - + % create MLC window % getting the dimensions of the MLC in order to be able to plot the % shapes using physical coordinates MLCWindow = [minX-bixelWidth/2 maxX+bixelWidth/2 ... - minZ-bixelWidth/2 maxZ+bixelWidth/2]; + minZ-bixelWidth/2 maxZ+bixelWidth/2]; % save data for each beam - apertureInfo.beam(i).numOfShapes = Sequencing.beam(i).numOfShapes; + apertureInfo.beam(i).numOfShapes = sequencing.beam(i).numOfShapes; apertureInfo.beam(i).numOfActiveLeafPairs = dimZ; apertureInfo.beam(i).leafPairPos = leafPairPos; apertureInfo.beam(i).isActiveLeafPair = isActiveLeafPair; @@ -163,17 +208,144 @@ apertureInfo.beam(i).bixelIndMap = bixelIndMap; apertureInfo.beam(i).posOfCornerBixel = posOfCornerBixel; apertureInfo.beam(i).MLCWindow = MLCWindow; + apertureInfo.beam(i).gantryAngle = stf(i).gantryAngle; + if pln.propOpt.runVMAT + + apertureInfo.beam(i).bixOffset = bixOffset; + bixOffset = bixOffset+apertureInfo.beam(i).numOfActiveLeafPairs; + + apertureInfo.propVMAT.beam(i).DAOBeam = stf(i).propVMAT.DAOBeam; + apertureInfo.propVMAT.beam(i).FMOBeam = stf(i).propVMAT.FMOBeam; + + apertureInfo.propVMAT.beam(i).leafDir = sequencing.beam(i).leafDir; + + apertureInfo.propVMAT.beam(i).doseAngleBorders = stf(i).propVMAT.doseAngleBorders; + apertureInfo.propVMAT.beam(i).doseAngleBorderCentreDiff = stf(i).propVMAT.doseAngleBorderCentreDiff; + apertureInfo.propVMAT.beam(i).doseAngleBordersDiff = stf(i).propVMAT.doseAngleBordersDiff; + + if apertureInfo.propVMAT.beam(i).DAOBeam + + totalNumOfOptBixels = totalNumOfOptBixels+stf(i).totalNumOfBixels; + totalNumOfLeafPairs = totalNumOfLeafPairs+apertureInfo.beam(i).numOfShapes*apertureInfo.beam(i).numOfActiveLeafPairs; + + apertureInfo.beam(i).gantryRot = sequencing.beam(i).gantryRot; + + apertureInfo.propVMAT.beam(i).DAOAngleBorders = stf(i).propVMAT.DAOAngleBorders; + apertureInfo.propVMAT.beam(i).DAOAngleBorderCentreDiff = stf(i).propVMAT.DAOAngleBorderCentreDiff; + apertureInfo.propVMAT.beam(i).DAOAngleBordersDiff = stf(i).propVMAT.DAOAngleBordersDiff; + + apertureInfo.propVMAT.beam(i).timeFacCurr = stf(i).propVMAT.timeFacCurr; + apertureInfo.propVMAT.beam(i).timeFac = stf(i).propVMAT.timeFac; + + apertureInfo.propVMAT.beam(i).lastDAOIndex = stf(i).propVMAT.lastDAOIndex; + apertureInfo.propVMAT.beam(i).nextDAOIndex = stf(i).propVMAT.nextDAOIndex; + apertureInfo.propVMAT.beam(i).DAOIndex = stf(i).propVMAT.DAOIndex; + + if apertureInfo.propVMAT.beam(i).FMOBeam + apertureInfo.propVMAT.beam(i).FMOAngleBorders = stf(i).propVMAT.FMOAngleBorders; + apertureInfo.propVMAT.beam(i).FMOAngleBorderCentreDiff = stf(i).propVMAT.FMOAngleBorderCentreDiff; + apertureInfo.propVMAT.beam(i).FMOAngleBordersDiff = stf(i).propVMAT.FMOAngleBordersDiff; + end + + if pln.propStf.continuousAperture + apertureInfo.propVMAT.beam(i).timeFacInd = stf(i).propVMAT.timeFacInd; + apertureInfo.propVMAT.beam(i).doseAngleDAO = stf(i).propVMAT.doseAngleDAO; + + apertureInfo.propVMAT.beam(i).leafConstMask = 1; + interpGetsTransition = apertureInfo.propVMAT.beam(i).timeFac(3) ~= 0; + end + + apertureInfo.propVMAT.jacobT(stf(i).propVMAT.DAOIndex,i) = stf(i).propVMAT.timeFacCurr; + + else + apertureInfo.propVMAT.beam(i).fracFromLastDAO = stf(i).propVMAT.fracFromLastDAO; + apertureInfo.propVMAT.beam(i).timeFracFromLastDAO = stf(i).propVMAT.timeFracFromLastDAO; + apertureInfo.propVMAT.beam(i).timeFracFromNextDAO = stf(i).propVMAT.timeFracFromNextDAO; + apertureInfo.propVMAT.beam(i).lastDAOIndex = stf(i).propVMAT.lastDAOIndex; + apertureInfo.propVMAT.beam(i).nextDAOIndex = stf(i).propVMAT.nextDAOIndex; + + if pln.propStf.continuousAperture + apertureInfo.propVMAT.beam(i).fracFromLastDAO_I = stf(i).propVMAT.fracFromLastDAO_I; + apertureInfo.propVMAT.beam(i).fracFromLastDAO_F = stf(i).propVMAT.fracFromLastDAO_F; + apertureInfo.propVMAT.beam(i).fracFromNextDAO_I = stf(i).propVMAT.fracFromNextDAO_I; + apertureInfo.propVMAT.beam(i).fracFromNextDAO_F = stf(i).propVMAT.fracFromNextDAO_F; + end + + apertureInfo.propVMAT.jacobT(stf(stf(i).propVMAT.lastDAOIndex).propVMAT.DAOIndex,i) = stf(stf(i).propVMAT.lastDAOIndex).propVMAT.timeFacCurr.*stf(i).propVMAT.timeFracFromLastDAO.*stf(i).propVMAT.doseAngleBordersDiff./stf(stf(i).propVMAT.lastDAOIndex).propVMAT.doseAngleBordersDiff; + apertureInfo.propVMAT.jacobT(stf(stf(i).propVMAT.nextDAOIndex).propVMAT.DAOIndex,i) = stf(stf(i).propVMAT.nextDAOIndex).propVMAT.timeFacCurr.*stf(i).propVMAT.timeFracFromNextDAO.*stf(i).propVMAT.doseAngleBordersDiff./stf(stf(i).propVMAT.lastDAOIndex).propVMAT.doseAngleBordersDiff; + + if interpGetsTransition + apertureInfo.propVMAT.beam(i).leafConstMask = 1; + end + interpGetsTransition = false; + end + end +end + +if ~isfield(pln.propOpt,'preconditioner') + pln.propOpt.preconditioner = false; end % save global data -apertureInfo.bixelWidth = bixelWidth; -apertureInfo.numOfMLCLeafPairs = numOfMLCLeafPairs; -apertureInfo.totalNumOfBixels = totalNumOfBixels; -apertureInfo.totalNumOfShapes = sum([apertureInfo.beam.numOfShapes]); -apertureInfo.totalNumOfLeafPairs = sum([apertureInfo.beam.numOfShapes]*[apertureInfo.beam.numOfActiveLeafPairs]'); +apertureInfo.continuousAperture = pln.propStf.continuousAperture; +apertureInfo.runVMAT = pln.propOpt.runVMAT; +apertureInfo.preconditioner = pln.propOpt.preconditioner; +apertureInfo.bixelWidth = bixelWidth; +apertureInfo.numOfMLCLeafPairs = numOfMLCLeafPairs; +apertureInfo.totalNumOfBixels = totalNumOfBixels; +apertureInfo.totalNumOfShapes = sum([apertureInfo.beam.numOfShapes]); -% create vectors for optimization -[apertureInfo.apertureVector, apertureInfo.mappingMx, apertureInfo.limMx] = matRad_OptimizationProblemDAO.matRad_daoApertureInfo2Vec(apertureInfo); +if isfield(sequencing,'weightToMU') + apertureInfo.weightToMU = sequencing.weightToMU; +end + +if pln.propOpt.runVMAT + + %tempStruct_beam = apertureInfo.propVMAT.beam; + %tempStruct_jacobT = apertureInfo.propVMAT.jacobT; + %apertureInfo.propVMAT.beam = tempStruct_beam; + %apertureInfo.propVMAT.jacobT = tempStruct_jacobT; + + % put constraints in apertureInfo.propVMAT + machine = matRad_loadMachine(pln); + apertureInfo.propVMAT.constraints = machine.constraints; + + apertureInfo.totalNumOfOptBixels = totalNumOfOptBixels; + apertureInfo.doseTotalNumOfLeafPairs = sum([apertureInfo.beam(:).numOfActiveLeafPairs]); + + if apertureInfo.continuousAperture + apertureInfo.totalNumOfLeafPairs = sum(reshape([apertureInfo.propVMAT.beam([apertureInfo.propVMAT.beam.DAOBeam]).doseAngleDAO],2,[]),1)*[apertureInfo.beam([apertureInfo.propVMAT.beam.DAOBeam]).numOfActiveLeafPairs]'; + + % count number of transitions + apertureInfo.propVMAT.numLeafSpeedConstraint = nnz([apertureInfo.propVMAT.beam.leafConstMask]); + apertureInfo.propVMAT.numLeafSpeedConstraintDAO = nnz([apertureInfo.propVMAT.beam([apertureInfo.propVMAT.beam.DAOBeam]).leafConstMask]); + else + apertureInfo.totalNumOfLeafPairs = totalNumOfLeafPairs; + end + + % fix instances of leaf touching + apertureInfo = matRad_leafTouching(apertureInfo); + + shapeInd = 0; + for i = 1:numel(apertureInfo.beam) + if apertureInfo.propVMAT.beam(i).DAOBeam + shapeInd = shapeInd+1; + apertureInfo.propVMAT.beam(i).timeInd = apertureInfo.totalNumOfShapes+apertureInfo.totalNumOfLeafPairs*2+shapeInd; + end + end + + % create vectors for optimization + [apertureInfo.apertureVector, apertureInfo.mappingMx, apertureInfo.limMx] = matRad_OptimizationProblemVMAT.matRad_daoApertureInfo2Vec(apertureInfo); + +else + apertureInfo.totalNumOfOptBixels = sum(stf(i).totalNumOfBixels); + apertureInfo.totalNumOfLeafPairs = sum([apertureInfo.beam.numOfShapes]*[apertureInfo.beam.numOfActiveLeafPairs]'); + apertureInfo.doseTotalNumOfLeafPairs = apertureInfo.totalNumOfLeafPairs; + + % create vectors for optimization + [apertureInfo.apertureVector, apertureInfo.mappingMx, apertureInfo.limMx] = matRad_OptimizationProblemDAO.matRad_daoApertureInfo2Vec(apertureInfo); +end end + diff --git a/matRad/sequencing/matRad_siochiLeafSequencing.m b/matRad/sequencing/matRad_siochiLeafSequencing.m index 4240a6c9a..224d7e2b2 100644 --- a/matRad/sequencing/matRad_siochiLeafSequencing.m +++ b/matRad/sequencing/matRad_siochiLeafSequencing.m @@ -1,4 +1,7 @@ -function resultGUI = matRad_siochiLeafSequencing(resultGUI,stf,dij,numOfLevels,visBool) +function resultGUI = matRad_siochiLeafSequencing(resultGUI,stf,dij,pln,visBool) +% multileaf collimator leaf sequencing algorithm for intensity modulated +% beams with multiple static segments according to Siochi (1999) +% International Journal of Radiation Oncology * Biology * Physics, % multileaf collimator leaf sequencing algorithm % for intensity modulated beams with multiple static segments according to % Siochi (1999)International Journal of Radiation Oncology * Biology * Physics, @@ -8,9 +11,9 @@ % % call % resultGUI = -% matRad_siochiLeafSequencing(resultGUI,stf,dij,numOfLevels) +% matRad_siochiLeafSequencing(resultGUI,stf,dij,pln) % resultGUI = -% matRad_siochiLeafSequencing(resultGUI,stf,dij,numOfLevels,visBool) +% matRad_siochiLeafSequencing(resultGUI,stf,dij,pln,visBool) % % input % resultGUI: resultGUI struct to which the output data will be @@ -18,7 +21,7 @@ % be created % stf: matRad steering information struct % dij: matRad's dij matrix -% numOfLevels: number of stratification levels +% pln: pln struct % visBool: toggle on/off visualization (optional) % % output @@ -41,6 +44,8 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +matRad_cfg = MatRad_Config.instance(); + % if visBool not set toogle off visualization if nargin < 5 visBool = 0; @@ -54,10 +59,24 @@ screensize = get(0,'ScreenSize'); xpos = ceil((screensize(3)-sz(2))/2); % center the figure on the screen horizontally ypos = ceil((screensize(4)-sz(1))/2); % center the figure on the screen vertically - seqFig = figure('position',[xpos,ypos,sz(2),sz(1)]); + seqFig = figure('position',[xpos,ypos,sz(2),sz(1)]); +end + +offset = 0; + +if ~isfield(pln,'propSeq') || ~isfield(pln.propSeq,'numLevels') + pln.propSeq.numLevels = matRad_cfg.defaults.propSeq.numLevels; end -offset = 0; +if ~isfield(pln, 'propOpt') || ~isfield(pln.propOpt,'runVMAT') + pln.propOpt.runVMAT = false; +end + +sequencing.runVMAT = pln.propOpt.runVMAT; + +if isfield(resultGUI,'scaleFacRx_FMO') + resultGUI.wUnsequenced = resultGUI.wUnsequenced/resultGUI.scaleFacRx_FMO; +end if ~isfield(resultGUI,'wUnsequenced') wUnsequenced = resultGUI.w; @@ -65,10 +84,36 @@ wUnsequenced = resultGUI.wUnsequenced; end +if ~isfield(dij,'weightToMU') + dij.weightToMU = 1; +end + for i = 1:numOfBeams - numOfRaysPerBeam = stf(i).numOfRays; + if pln.propOpt.runVMAT + + if ~stf(i).propVMAT.FMOBeam + sequencing.w(1+offset:numOfRaysPerBeam+offset,1) = 0; + + sequencing.beam(i).bixelIx = 1+offset:numOfRaysPerBeam+offset; + + offset = offset + numOfRaysPerBeam; + continue %if this is not a beam to be initialized, continue to next iteration without generating segments + else + numToKeep = stf(i).propVMAT.numOfBeamChildren; + end + else + + sequencing.beam(i).numOfShapes = 0; + %does this make sense to discard apertures if VMAT isn't run? + if isfield(pln.propOpt,'numApertures') + numToKeep = pln.propOpt.numApertures; %if all apertures are to be kept, this should be set to 0 + else + numToKeep = 0; + end + end + % get relevant weights for current beam wOfCurrBeams = wUnsequenced(1+offset:numOfRaysPerBeam+offset);%REVIEW OFFSET @@ -103,47 +148,78 @@ %Save weights in fluence matrix. fluenceMx(indInFluenceMx) = wOfCurrBeams; - % Stratification - calFac = max(fluenceMx(:)); - D_k = round(fluenceMx/calFac*numOfLevels); - - % Save the stratification in the initial intensity matrix D_0. - D_0 = D_k; - - % container to remember generated shapes; allocate space for 10000 - % shapes - shapes = NaN*ones(dimOfFluenceMxZ,dimOfFluenceMxX,10000); - shapesWeight = zeros(10000,1); - k = 0; - - if visBool - clf(seqFig); - colormap(seqFig,'jet'); - - seqSubPlots(1) = subplot(2,2,1,'parent',seqFig); - imagesc(D_k,'parent',seqSubPlots(1)); - set(seqSubPlots(1),'CLim',[0 numOfLevels],'YDir','normal'); - title(seqSubPlots(1),['Beam # ' num2str(i) ': max(D_0) = ' num2str(max(D_0(:))) ' - ' num2str(numel(unique(D_0))) ' intensity levels']); - xlabel(seqSubPlots(1),'x - direction parallel to leaf motion ') - ylabel(seqSubPlots(1),'z - direction perpendicular to leaf motion ') - colorbar; - drawnow + if matRad_cfg.isMatlab && license('test','image_toolbox') + temp = zeros(size(fluenceMx)); + for row = 1:dimOfFluenceMxZ + temp(row,:) = imgaussfilt(fluenceMx(row,:),1); + end + fluenceMx = temp; + clear temp end + + + %allow for possibility to repeat sequencing with higher number of + %levels if number of apertures is lower than required + notFinished = 1; + numOfLevels = pln.propSeq.numLevels; - D_k_nonZero = (D_k~=0); - [D_k_Z, D_k_X] = ind2sub([dimOfFluenceMxZ,dimOfFluenceMxX],find(D_k_nonZero)); - D_k_MinZ = min(D_k_Z); - D_k_MaxZ = max(D_k_Z); - D_k_MinX = min(D_k_X); - D_k_MaxX = max(D_k_X); - - %Decompose the port, do rod pushing - [tops, bases] = matRad_siochiDecomposePort(D_k,dimOfFluenceMxZ,dimOfFluenceMxX,D_k_MinZ,D_k_MaxZ,D_k_MinX,D_k_MaxX); - %Form segments with and without visualization - if visBool - [shapes,shapesWeight,k,D_k]=matRad_siochiConvertToSegments(shapes,shapesWeight,k,tops,bases,visBool,i,D_k,numOfLevels,seqFig,seqSubPlots); - else - [shapes,shapesWeight,k]=matRad_siochiConvertToSegments(shapes,shapesWeight,k,tops,bases); + while notFinished + % Keep looping until we have at least as many apertures as + % numToKeep. Increment numOfLevels by 1 each iteration + + % prepare sequencer + calFac = max(fluenceMx(:)); + + D_k = round(fluenceMx/calFac*numOfLevels); + + % Save the stratification in the initial intensity matrix D_0. + D_0 = D_k; + + % container to remember generated shapes; allocate space for 10000 + % shapes + shapes = NaN*ones(dimOfFluenceMxZ,dimOfFluenceMxX,10000); + shapesWeight = zeros(10000,1); + k = 0; + + if visBool + clf(seqFig); + colormap(seqFig,'jet'); + + seqSubPlots(1) = subplot(2,2,1,'parent',seqFig); + imagesc(D_k,'parent',seqSubPlots(1)); + set(seqSubPlots(1),'CLim',[0 numOfLevels],'YDir','normal'); + title(seqSubPlots(1),['Beam # ' num2str(i) ': max(D_0) = ' num2str(max(D_0(:))) ' - ' num2str(numel(unique(D_0))) ' intensity levels']); + xlabel(seqSubPlots(1),'x - direction parallel to leaf motion ') + ylabel(seqSubPlots(1),'z - direction perpendicular to leaf motion ') + colorbar; + drawnow + end + + D_k_nonZero = (D_k~=0); + [D_k_Z, D_k_X] = ind2sub([dimOfFluenceMxZ,dimOfFluenceMxX],find(D_k_nonZero)); + D_k_MinZ = min(D_k_Z); + D_k_MaxZ = max(D_k_Z); + D_k_MinX = min(D_k_X); + D_k_MaxX = max(D_k_X); + + %Decompose the port, do rod pushing + [tops, bases] = matRad_siochiDecomposePort(D_k,dimOfFluenceMxZ,dimOfFluenceMxX,D_k_MinZ,D_k_MaxZ,D_k_MinX,D_k_MaxX); + + %Form segments with and without visualization + if visBool + [shapes,shapesWeight,k,D_k]=matRad_siochiConvertToSegments(shapes,shapesWeight,k,tops,bases,visBool,i,D_k,numOfLevels,seqFig,seqSubPlots); + else + [shapes,shapesWeight,k]=matRad_siochiConvertToSegments(shapes,shapesWeight,k,tops,bases); + end + + %are there enough apertures? + if numToKeep ~= 0 && k < numToKeep + % no, therefore increment numOfLevels + numOfLevels = numOfLevels+1; + else + % yes, therefore exit out of the while loop + notFinished = 0; + end end sequencing.beam(i).numOfShapes = k; @@ -152,21 +228,67 @@ sequencing.beam(i).bixelIx = 1+offset:numOfRaysPerBeam+offset; sequencing.beam(i).fluence = D_0; sequencing.beam(i).sum = zeros(dimOfFluenceMxZ,dimOfFluenceMxX); + + if numToKeep ~= 0 + sequencing.beam(i) = matRad_discardApertures(sequencing.beam(i),numToKeep); + end - for j = 1:k - sequencing.beam(i).sum = sequencing.beam(i).sum+sequencing.beam(i).shapes(:,:,j)*sequencing.beam(i).shapesWeight(j); + if ~pln.propOpt.runVMAT + + for shape = 1:sequencing.beam(i).numOfShapes + sequencing.beam(i).sum = sequencing.beam(i).sum+sequencing.beam(i).shapes(:,:,shape)*sequencing.beam(i).shapesWeight(shape); + end + sequencing.w(1+offset:numOfRaysPerBeam+offset,1) = sequencing.beam(i).sum(indInFluenceMx); end - sequencing.w(1+offset:numOfRaysPerBeam+offset,1) = sequencing.beam(i).sum(indInFluenceMx); offset = offset + numOfRaysPerBeam; +end + +if pln.propOpt.runVMAT + + % do arc sequencing + sequencing.beam = matRad_arcSequencing(sequencing.beam,stf,pln,dij.weightToMU); + + % carry variables + sequencing.weightToMU = dij.weightToMU; + + % get apertureInfo + resultGUI.apertureInfo = matRad_sequencing2ApertureInfo(sequencing,stf,pln); + + %matRad_daoVec2ApertureInfo will interpolate subchildren gantry + %segments + resultGUI.apertureInfo = matRad_OptimizationProblemVMAT.matRad_daoVec2ApertureInfo(resultGUI.apertureInfo,resultGUI.apertureInfo.apertureVector); + + %calculate max leaf speed + resultGUI.apertureInfo = matRad_maxLeafSpeed(resultGUI.apertureInfo); + + %optimize delivery + resultGUI = matRad_optDelivery(resultGUI,0); + resultGUI.apertureInfo = matRad_maxLeafSpeed(resultGUI.apertureInfo); + + sequencing.w = resultGUI.apertureInfo.bixelWeights; + +else + sequencing.weightToMU = dij.weightToMU; + + resultGUI.apertureInfo = matRad_sequencing2ApertureInfo(sequencing,stf,pln); + + resultGUI.apertureInfo = matRad_OptimizationProblemDAO.matRad_daoVec2ApertureInfo(resultGUI.apertureInfo,resultGUI.apertureInfo.apertureVector); +end +if ~isfield(pln.propOpt,'preconditioner') + pln.propOpt.preconditioner = false; +end + +if pln.propOpt.preconditioner + % calculation preconditioning factors + resultGUI.apertureInfo = matRad_preconditionFactors(resultGUI.apertureInfo); end resultGUI.w = sequencing.w; resultGUI.wSequenced = sequencing.w; resultGUI.sequencing = sequencing; -resultGUI.apertureInfo = matRad_sequencing2ApertureInfo(sequencing,stf); doseSequencedDoseGrid = reshape(dij.physicalDose{1} * sequencing.w,dij.doseGrid.dimensions); % interpolate to ct grid for visualiation & analysis @@ -191,7 +313,9 @@ for i = minX:maxX maxTop = -1; - TnG = 1; + + TnG = 0; %FOR NOW + for j = minZ:maxZ if i == minX bases(j,i) = 1; diff --git a/matRad/sequencing/matRad_xiaLeafSequencing.m b/matRad/sequencing/matRad_xiaLeafSequencing.m index aaa893324..549311451 100644 --- a/matRad/sequencing/matRad_xiaLeafSequencing.m +++ b/matRad/sequencing/matRad_xiaLeafSequencing.m @@ -1,18 +1,18 @@ -function resultGUI = matRad_xiaLeafSequencing(resultGUI,stf,dij,numOfLevels,visBool) +function resultGUI = matRad_xiaLeafSequencing(resultGUI,stf,dij,pln,visBool) % multileaf collimator leaf sequencing algorithm % for intensity modulated beams with multiple static segments according to % Xia et al. (1998) Medical Physics % % call -% resultGUI = matRad_xiaLeafSequencing(resultGUI,stf,dij,numOfLevels) -% resultGUI = matRad_xiaLeafSequencing(resultGUI,stf,dij,numOfLevels,visBool) +% resultGUI = matRad_xiaLeafSequencing(resultGUI,stf,dij,pln) +% resultGUI = matRad_xiaLeafSequencing(resultGUI,stf,dij,pln,visBool) % % input % resultGUI: resultGUI struct to which the output data will be added, if % this field is empty resultGUI struct will be created % stf: matRad steering information struct % dij: matRad's dij matrix -% numOfLevels: number of stratification levels +% pln: pln structure % visBool: toggle on/off visualization (optional) % % output @@ -40,6 +40,8 @@ visBool = 0; end +matRad_cfg = MatRad_Config.instance(); + mode = 'rl'; % sliding window (sw) or reducing level (rl) numOfBeams = numel(stf); @@ -53,6 +55,10 @@ seqFig = figure('position',[xpos,ypos,sz(2),sz(1)]); end +if ~isfield(pln,'propSeq') || ~isfield(pln.propSeq,'numLevels') + pln.propSeq.numLevels = matRad_cfg.defaults.propSeq.numLevels; +end + offset = 0; for i = 1:numOfBeams @@ -95,7 +101,7 @@ % Stratification calFac = max(fluenceMx(:)); - D_k = round(fluenceMx/calFac*numOfLevels); + D_k = round(fluenceMx/calFac*pln.propSeq.numLevels); % Save the stratification in the initial intensity matrix D_0. D_0 = D_k; @@ -242,11 +248,11 @@ sequencing.beam(i).numOfShapes = k; sequencing.beam(i).shapes = shapes(:,:,1:k); - sequencing.beam(i).shapesWeight = shapesWeight(1:k)/numOfLevels*calFac; + sequencing.beam(i).shapesWeight = shapesWeight(1:k)/pln.propSeq.numLevels*calFac; sequencing.beam(i).bixelIx = 1+offset:numOfRaysPerBeam+offset; sequencing.beam(i).fluence = D_0; - sequencing.w(1+offset:numOfRaysPerBeam+offset,1) = D_0(indInFluenceMx)/numOfLevels*calFac; + sequencing.w(1+offset:numOfRaysPerBeam+offset,1) = D_0(indInFluenceMx)/pln.propSeq.numLevels*calFac; offset = offset + numOfRaysPerBeam; @@ -256,7 +262,7 @@ resultGUI.wSequenced = sequencing.w; resultGUI.sequencing = sequencing; -resultGUI.apertureInfo = matRad_sequencing2ApertureInfo(sequencing,stf); +resultGUI.apertureInfo = matRad_sequencing2ApertureInfo(sequencing,stf,pln); doseSequencedDoseGrid = reshape(dij.physicalDose{1} * sequencing.w,dij.doseGrid.dimensions); % interpolate to ct grid for visualiation & analysis diff --git a/matRad/steering/matRad_StfGeneratorBase.m b/matRad/steering/matRad_StfGeneratorBase.m index 31c5a11ee..a4f67649a 100644 --- a/matRad/steering/matRad_StfGeneratorBase.m +++ b/matRad/steering/matRad_StfGeneratorBase.m @@ -220,7 +220,7 @@ function assignPropertiesFromPln(this,pln,warnWhenPropertyChanged) this.initialize(); this.createPatientGeometry(); - stf = this.generateSourceGeometry(); + stf = this.generateSourceGeometry(); end end diff --git a/matRad/steering/matRad_StfGeneratorPhotonVMAT.m b/matRad/steering/matRad_StfGeneratorPhotonVMAT.m new file mode 100644 index 000000000..73e35377d --- /dev/null +++ b/matRad/steering/matRad_StfGeneratorPhotonVMAT.m @@ -0,0 +1,399 @@ +classdef matRad_StfGeneratorPhotonVMAT < matRad_StfGeneratorPhotonRayBixelAbstract + + properties (Constant) + name = 'Photon VMAT stf Generator'; + shortName = 'PhotonVMAT'; + possibleRadiationModes = {'photons'}; + end + + properties + % TODO: set defaults for these + DAOGantryAngles; + FMOGantryAngles; + startingAngle; + finishingAngle; + continuousAperture; + end + + methods + function this = matRad_StfGeneratorPhotonVMAT(pln) + if nargin < 1 + pln = []; + end + + this@matRad_StfGeneratorPhotonRayBixelAbstract(pln); + + if ~isempty(pln) + % this could probably be integrated better + pln = matRad_VMATGantryAngles(pln); + % the following were just generated by the preceding + % function call matRad_VMATGantryAngles + this.gantryAngles = pln.propStf.gantryAngles; + this.DAOGantryAngles = pln.propStf.DAOGantryAngles; + this.FMOGantryAngles = pln.propStf.FMOGantryAngles; + this.couchAngles = pln.propStf.couchAngles; + this.isoCenter = pln.propStf.isoCenter; + end + + if isempty(this.radiationMode) + this.radiationMode = 'photons'; + end + end + end + + methods (Access = protected) + function pbMargin = getPbMargin(this) + pbMargin = this.bixelWidth; + end + + function stf = generateSourceGeometry(this) + stf = this.generateSourceGeometry@matRad_StfGeneratorPhotonRayBixelAbstract(); + + matRad_cfg = MatRad_Config.instance(); + matRad_cfg.dispInfo('Apply VMAT configuration to stf...\n'); + + masterRayPosBEV = zeros(0,3); + for i = 1:numel(stf) + rayPosBEV = reshape([stf(i).ray(:).rayPos_bev]',3,stf(i).numOfRays)'; + masterRayPosBEV = union(masterRayPosBEV,rayPosBEV,'rows'); + end + + % masterRayPosBEV + x = masterRayPosBEV(:,1); + y = masterRayPosBEV(:,2); + z = masterRayPosBEV(:,3); + uniZ = unique(z); + for j = 1:numel(uniZ) + x_loc = x(z == uniZ(j)); + x_min = min(x_loc); + x_max = max(x_loc); + x = [x; (x_min:this.bixelWidth:x_max)']; + y = [y; zeros((x_max-x_min)/this.bixelWidth+1,1)]; + z = [z; uniZ(j)*ones((x_max-x_min)/this.bixelWidth+1,1)]; + end + + SAD = this.machine.meta.SAD; + + masterRayPosBEV = [x,y,z]; + masterRayPosBEV = unique(masterRayPosBEV,'rows'); + masterTargetPointBEV = [2*masterRayPosBEV(:,1) SAD*ones(size(masterRayPosBEV,1),1) 2*masterRayPosBEV(:,3)]; + + % post-processing function for VMAT + matRad_cfg.dispInfo('VMAT stf post-processing (1/2)... '); + + numDAO = 1; + DAODoseAngleBorders = zeros(2*numel(this.DAOGantryAngles),1); + offset = 1; + timeFacIndOffset = 1; + + for i = 1:length(this.gantryAngles) + + %stf(i).continuousAperture = this.continuousAperture; + + % Determine which FMO beam the current beam belongs to + [~,stf(i).propVMAT.beamParentFMOIndex] = min(abs(this.FMOGantryAngles-this.gantryAngles(i))); + stf(i).propVMAT.beamParentGantryAngle = this.FMOGantryAngles(stf(i).propVMAT.beamParentFMOIndex); + stf(i).propVMAT.beamParentIndex = find(abs(this.gantryAngles - stf(i).propVMAT.beamParentGantryAngle) < 1e-6); + + % Indicate if this beam is to be included in DOA/FMO or not. All beams + % are still considered in dose calc for objective function in DAO + stf(i).propVMAT.FMOBeam = any(abs(this.FMOGantryAngles - this.gantryAngles(i)) < 1e-6); + stf(i).propVMAT.DAOBeam = any(abs(this.DAOGantryAngles - this.gantryAngles(i)) < 1e-6); + + %% Determine different angle borders + + % doseAngleBorders are the angular borders over which dose is deposited + if i == 1 + stf(i).propVMAT.doseAngleBorders = [this.startingAngle (this.gantryAngles(i+1)+this.gantryAngles(i))/2]; + elseif i == length(this.gantryAngles) + + stf(i).propVMAT.doseAngleBorders = [(this.gantryAngles(i-1)+this.gantryAngles(i))/2 this.finishingAngle]; + else + + stf(i).propVMAT.doseAngleBorders = ([this.gantryAngles(i-1) this.gantryAngles(i+1)]+this.gantryAngles(i))/2; + end + + stf(i).propVMAT.doseAngleBorderCentreDiff = [stf(i).gantryAngle-stf(i).propVMAT.doseAngleBorders(1) stf(i).propVMAT.doseAngleBorders(2)-stf(i).gantryAngle]; + stf(i).propVMAT.doseAngleBordersDiff = sum(stf(i).propVMAT.doseAngleBorderCentreDiff); + + %Assign beam to its Parent, either as child (optimized) or subchild + %(interpolated) + if stf(i).propVMAT.DAOBeam + DAODoseAngleBorders((offset):(offset+1)) = stf(i).propVMAT.doseAngleBorders; + offset= offset+2; + + if ~isfield(stf(stf(i).propVMAT.beamParentIndex).propVMAT,'beamChildrenGantryAngles') || isempty(stf(stf(i).propVMAT.beamParentIndex).propVMAT.beamChildrenGantryAngles) + stf(stf(i).propVMAT.beamParentIndex).propVMAT.numOfBeamChildren = 0; + stf(stf(i).propVMAT.beamParentIndex).propVMAT.beamChildrenGantryAngles = nan(1000,1); + stf(stf(i).propVMAT.beamParentIndex).propVMAT.beamChildrenIndex = nan(1000,1); + end + + stf(stf(i).propVMAT.beamParentIndex).propVMAT.numOfBeamChildren = stf(stf(i).propVMAT.beamParentIndex).propVMAT.numOfBeamChildren+1; + stf(stf(i).propVMAT.beamParentIndex).propVMAT.beamChildrenGantryAngles(stf(stf(i).propVMAT.beamParentIndex).propVMAT.numOfBeamChildren) = this.gantryAngles(i); + stf(stf(i).propVMAT.beamParentIndex).propVMAT.beamChildrenIndex(stf(stf(i).propVMAT.beamParentIndex).propVMAT.numOfBeamChildren) = i; + + %optAngleBorders are the angular borders over which an optimized control point + %has influence + DAOIndex = find(abs(this.DAOGantryAngles - this.gantryAngles(i)) < 1e-8); + + if DAOIndex == 1 + stf(i).propVMAT.DAOAngleBorders = [this.startingAngle (this.DAOGantryAngles(DAOIndex+1)+this.DAOGantryAngles(DAOIndex))/2]; + + lastDAOIndex = i; + nextDAOIndex = find(abs(this.gantryAngles - this.DAOGantryAngles(DAOIndex+1)) < 1e-8); + + stf(i).propVMAT.lastDAOIndex = lastDAOIndex; + stf(i).propVMAT.nextDAOIndex = nextDAOIndex; + elseif DAOIndex == length(this.DAOGantryAngles) + stf(i).propVMAT.DAOAngleBorders = [(this.DAOGantryAngles(DAOIndex-1)+this.DAOGantryAngles(DAOIndex))/2 this.finishingAngle]; + + stf(i).propVMAT.lastDAOIndex = find(abs(this.gantryAngles - this.DAOGantryAngles(DAOIndex-1)) < 1e-8); + stf(i).propVMAT.nextDAOIndex = i; + else + stf(i).propVMAT.DAOAngleBorders = ([this.DAOGantryAngles(DAOIndex-1) this.DAOGantryAngles(DAOIndex+1)]+this.DAOGantryAngles(DAOIndex))/2; + + lastDAOIndex = i; + nextDAOIndex = find(abs(this.gantryAngles - this.DAOGantryAngles(DAOIndex+1)) < 1e-8); + + stf(i).propVMAT.lastDAOIndex = lastDAOIndex; + stf(i).propVMAT.nextDAOIndex = nextDAOIndex; + end + + stf(i).propVMAT.DAOIndex = numDAO; + numDAO = numDAO+1; + + stf(i).propVMAT.DAOAngleBorderCentreDiff = [stf(i).gantryAngle-stf(i).propVMAT.DAOAngleBorders(1) stf(i).propVMAT.DAOAngleBorders(2)-stf(i).gantryAngle]; + stf(i).propVMAT.DAOAngleBordersDiff = sum(stf(i).propVMAT.DAOAngleBorderCentreDiff); + + %This is the factor that relates the total time in the + %optimized arc sector to the total time in the current dose + %sector + stf(i).propVMAT.timeFacCurr = stf(i).propVMAT.doseAngleBordersDiff./stf(i).propVMAT.DAOAngleBordersDiff; + + if this.continuousAperture + %These are the factors that relate the total time in the + %optimized arc sector to the total time in the previous and + %next dose sectors + stf(i).propVMAT.timeFac = zeros(1,3); + + stf(i).propVMAT.timeFac(1) = (stf(i).propVMAT.DAOAngleBorderCentreDiff(1)-stf(i).propVMAT.doseAngleBorderCentreDiff(1))/stf(i).propVMAT.DAOAngleBordersDiff; + stf(i).propVMAT.timeFac(2) = stf(i).propVMAT.timeFacCurr; + stf(i).propVMAT.timeFac(3) = (stf(i).propVMAT.DAOAngleBorderCentreDiff(2)-stf(i).propVMAT.doseAngleBorderCentreDiff(2))/stf(i).propVMAT.DAOAngleBordersDiff; + + % keep entries with a non-0 timeFac + delInd = stf(i).propVMAT.timeFac == 0; + + % write timeFacInd + stf(i).propVMAT.timeFacInd = [timeFacIndOffset-1 timeFacIndOffset timeFacIndOffset+1]; + stf(i).propVMAT.timeFacInd(delInd) = 0; + + % update offset + if delInd(3) + timeFacIndOffset = timeFacIndOffset+1; + else + timeFacIndOffset = timeFacIndOffset+2; + end + + else + %These are the factors that relate the total time in the + %optimized arc sector to the total time in the previous and + %next dose sectors + stf(i).propVMAT.timeFac = zeros(1,2); + + stf(i).propVMAT.timeFac(1) = stf(i).propVMAT.DAOAngleBorderCentreDiff(1)/stf(i).propVMAT.DAOAngleBordersDiff; + stf(i).propVMAT.timeFac(2) = stf(i).propVMAT.DAOAngleBorderCentreDiff(2)/stf(i).propVMAT.DAOAngleBordersDiff; + end + + else + if ~isfield(stf(stf(i).propVMAT.beamParentIndex).propVMAT,'beamSubChildrenGantryAngles') || isempty(stf(stf(i).propVMAT.beamParentIndex).propVMAT.beamSubChildrenGantryAngles) + stf(stf(i).propVMAT.beamParentIndex).propVMAT.numOfBeamSubChildren = 0; + stf(stf(i).propVMAT.beamParentIndex).propVMAT.beamSubChildrenGantryAngles = nan(1000,1); + stf(stf(i).propVMAT.beamParentIndex).propVMAT.beamSubChildrenIndex = nan(1000,1); + end + + stf(stf(i).propVMAT.beamParentIndex).propVMAT.numOfBeamSubChildren = stf(stf(i).propVMAT.beamParentIndex).propVMAT.numOfBeamSubChildren+1; + stf(stf(i).propVMAT.beamParentIndex).propVMAT.beamSubChildrenGantryAngles(stf(stf(i).propVMAT.beamParentIndex).propVMAT.numOfBeamSubChildren) = this.gantryAngles(i); + stf(stf(i).propVMAT.beamParentIndex).propVMAT.beamSubChildrenIndex(stf(stf(i).propVMAT.beamParentIndex).propVMAT.numOfBeamSubChildren) = i; + + stf(i).propVMAT.fracFromLastDAO = (this.gantryAngles(nextDAOIndex)-this.gantryAngles(i))./(this.gantryAngles(nextDAOIndex)-this.gantryAngles(lastDAOIndex)); + stf(i).propVMAT.lastDAOIndex = lastDAOIndex; + stf(i).propVMAT.nextDAOIndex = nextDAOIndex; + end + + + if stf(i).propVMAT.FMOBeam + % FMOAngleBorders are the angular borders over which an optimized + % control point has influence + FMOIndex = find(abs(this.FMOGantryAngles - this.gantryAngles(i)) < 1e-8); + + if FMOIndex == 1 + + stf(i).propVMAT.FMOAngleBorders = [this.startingAngle (this.FMOGantryAngles(FMOIndex+1)+this.FMOGantryAngles(FMOIndex))/2]; + elseif FMOIndex == length(this.FMOGantryAngles) + + stf(i).propVMAT.FMOAngleBorders = [(this.FMOGantryAngles(FMOIndex-1)+this.FMOGantryAngles(FMOIndex))/2 this.finishingAngle]; + else + + stf(i).propVMAT.FMOAngleBorders = ([this.FMOGantryAngles(FMOIndex-1) this.FMOGantryAngles(FMOIndex+1)]+this.FMOGantryAngles(FMOIndex))/2; + end + stf(i).propVMAT.FMOAngleBorderCentreDiff = [stf(i).gantryAngle-stf(i).propVMAT.FMOAngleBorders(1) stf(i).propVMAT.FMOAngleBorders(2)-stf(i).gantryAngle]; + stf(i).propVMAT.FMOAngleBordersDiff = sum(stf(i).propVMAT.FMOAngleBorderCentreDiff); + end + + %% transformation of union of rays + + stf(i).numOfRays = size(masterRayPosBEV,1); + stf(i).numOfBixelsPerRay = ones(1,stf(i).numOfRays); + stf(i).totalNumOfBixels = sum(stf(i).numOfBixelsPerRay); + + + % source position in bev + stf(i).sourcePoint_bev = [0 -SAD 0]; + + % get (active) rotation matrix + % transpose matrix because we are working with row vectors + rotMat_vectors_T = transpose(matRad_getRotationMatrix(this.gantryAngles(i),this.couchAngles(i))); + + stf(i).sourcePoint = stf(i).sourcePoint_bev*rotMat_vectors_T; + + % Save ray and target position in lps system. + for j = 1:stf(i).numOfRays + stf(i).ray(j).rayPos_bev = masterRayPosBEV(j,:); + stf(i).ray(j).targetPoint_bev = masterTargetPointBEV(j,:); + + stf(i).ray(j).rayPos = stf(i).ray(j).rayPos_bev*rotMat_vectors_T; + stf(i).ray(j).targetPoint = stf(i).ray(j).targetPoint_bev*rotMat_vectors_T; + + stf(i).ray(j).rayCorners_SCD = (repmat([0, this.machine.meta.SCD - SAD, 0],4,1)+ (this.machine.meta.SCD/SAD) * ... + [masterRayPosBEV(j,:) + [+stf(i).bixelWidth/2,0,+stf(i).bixelWidth/2];... + masterRayPosBEV(j,:) + [-stf(i).bixelWidth/2,0,+stf(i).bixelWidth/2];... + masterRayPosBEV(j,:) + [-stf(i).bixelWidth/2,0,-stf(i).bixelWidth/2];... + masterRayPosBEV(j,:) + [+stf(i).bixelWidth/2,0,-stf(i).bixelWidth/2]])*rotMat_vectors_T; + end + + % loop over all rays to determine meta information for each ray + stf(i).totalNumOfBixels = sum(stf(i).numOfBixelsPerRay); + stf(i).numOfBixelsPerRay = ones(1,stf(i).numOfRays); + + for j = stf(i).numOfRays:-1:1 + + % find appropriate energies for particles + if strcmp(stf(i).radiationMode,'photons') + + % book keeping for photons + stf(i).ray(j).energy = this.machine.data.energy; + else + matRad_cfg.dispError('Error generating stf struct: invalid radiation modality for VMAT.'); + end + end + + matRad_progress(i,length(this.gantryAngles)); + end + + + %% final cleanup and calculation of factors we couldn't calc before + matRad_cfg.dispInfo('VMAT post-processing (2/2)... '); + + for i = 1:length(this.gantryAngles) + if stf(i).propVMAT.FMOBeam + %remove NaNs from beamChildren and beamSubChildren + if isfield(stf(i).propVMAT,'beamChildrenGantryAngles') + stf(i).propVMAT.beamChildrenGantryAngles(isnan(stf(i).propVMAT.beamChildrenGantryAngles)) = []; + stf(i).propVMAT.beamChildrenIndex(isnan(stf(i).propVMAT.beamChildrenIndex)) = []; + else + stf(i).propVMAT.numOfBeamChildren = 0; + end + if isfield(stf(i).propVMAT,'beamSubChildrenGantryAngles') + stf(i).propVMAT.beamSubChildrenGantryAngles(isnan(stf(i).propVMAT.beamSubChildrenGantryAngles)) = []; + stf(i).propVMAT.beamSubChildrenIndex(isnan(stf(i).propVMAT.beamSubChildrenIndex)) = []; + else + stf(i).propVMAT.numOfBeamSubChildren = 0; + end + end + + if stf(i).propVMAT.DAOBeam + if this.continuousAperture + + stf(i).propVMAT.doseAngleDAO = ones(1,2); + + if sum(DAODoseAngleBorders == stf(i).propVMAT.doseAngleBorders(2)) > 1 + %final dose angle is repeated + %do not count twice in optimization + stf(i).propVMAT.doseAngleDAO(2) = 0; + end + end + end + + if ~stf(i).propVMAT.FMOBeam && ~stf(i).propVMAT.DAOBeam + + % for leaf position interpolation + stf(i).propVMAT.fracFromLastDAO_I = (stf(stf(i).propVMAT.nextDAOIndex).propVMAT.doseAngleBorders(1)-stf(i).propVMAT.doseAngleBorders(1))./(stf(stf(i).propVMAT.nextDAOIndex).propVMAT.doseAngleBorders(1)-stf(stf(i).propVMAT.lastDAOIndex).propVMAT.doseAngleBorders(2)); + stf(i).propVMAT.fracFromLastDAO_F = (stf(stf(i).propVMAT.nextDAOIndex).propVMAT.doseAngleBorders(1)-stf(i).propVMAT.doseAngleBorders(2))./(stf(stf(i).propVMAT.nextDAOIndex).propVMAT.doseAngleBorders(1)-stf(stf(i).propVMAT.lastDAOIndex).propVMAT.doseAngleBorders(2)); + stf(i).propVMAT.fracFromNextDAO_I = (stf(i).propVMAT.doseAngleBorders(1)-stf(stf(i).propVMAT.lastDAOIndex).propVMAT.doseAngleBorders(2))./(stf(stf(i).propVMAT.nextDAOIndex).propVMAT.doseAngleBorders(1)-stf(stf(i).propVMAT.lastDAOIndex).propVMAT.doseAngleBorders(2)); + stf(i).propVMAT.fracFromNextDAO_F = (stf(i).propVMAT.doseAngleBorders(2)-stf(stf(i).propVMAT.lastDAOIndex).propVMAT.doseAngleBorders(2))./(stf(stf(i).propVMAT.nextDAOIndex).propVMAT.doseAngleBorders(1)-stf(stf(i).propVMAT.lastDAOIndex).propVMAT.doseAngleBorders(2)); + + % for time interpolation + stf(i).propVMAT.timeFracFromLastDAO = (stf(stf(i).propVMAT.lastDAOIndex).propVMAT.DAOAngleBorders(2)-stf(i).propVMAT.doseAngleBorders(1))./stf(i).propVMAT.doseAngleBordersDiff; + stf(i).propVMAT.timeFracFromNextDAO = (stf(i).propVMAT.doseAngleBorders(2)-stf(stf(i).propVMAT.lastDAOIndex).propVMAT.DAOAngleBorders(2))./stf(i).propVMAT.doseAngleBordersDiff; + if stf(i).propVMAT.timeFracFromLastDAO > 1 + stf(i).propVMAT.timeFracFromLastDAO = 1; + elseif stf(i).propVMAT.timeFracFromLastDAO < 0 + stf(i).propVMAT.timeFracFromLastDAO = 0; + end + if stf(i).propVMAT.timeFracFromNextDAO > 1 + stf(i).propVMAT.timeFracFromNextDAO = 1; + elseif stf(i).propVMAT.timeFracFromNextDAO < 0 + stf(i).propVMAT.timeFracFromNextDAO = 0; + end + end + + matRad_progress(i,length(this.gantryAngles)); + end + + end + end + + methods (Static) + function [available,msg] = isAvailable(pln,machine) + % see superclass for information + + if nargin < 2 + machine = matRad_loadMachine(pln); + end + + % Check superclass availability + [available,msg] = matRad_StfGeneratorPhotonRayBixelAbstract.isAvailable(pln,machine); + + if ~available + return; + else + available = false; + msg = []; + end + + %checkBasic + try + checkBasic = isfield(machine,'meta') && isfield(machine,'data'); + + %check modality + checkModality = any(strcmp(matRad_StfGeneratorPhotonVMAT.possibleRadiationModes, machine.meta.radiationMode)) && any(strcmp(matRad_StfGeneratorPhotonVMAT.possibleRadiationModes, pln.radiationMode)); + + %Sanity check compatibility + if checkModality + checkModality = strcmp(machine.meta.radiationMode,pln.radiationMode); + end + + preCheck = checkBasic && checkModality; + + if ~preCheck + return; + end + catch + msg = 'Your machine file is invalid and does not contain the basic field (meta/data/radiationMode)!'; + return; + end + + available = preCheck; + end + end +end diff --git a/matRad/steering/matRad_VMATGantryAngles.m b/matRad/steering/matRad_VMATGantryAngles.m new file mode 100644 index 000000000..a91322e72 --- /dev/null +++ b/matRad/steering/matRad_VMATGantryAngles.m @@ -0,0 +1,126 @@ +function pln = matRad_VMATGantryAngles(pln) +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% matRad determine gantry angles for VMAT +% +% call +% matRad_VMATGantryAngles(pln) +% +% input +% pln: matRad plan meta information struct +% +% output +% pln: matRad plan meta information struct +% +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2016 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSES.txt. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%should have already defined fields: +% maxGantryAngleSpacing +% maxDAOGantryAngleSpacing +% maxFMOGantryAngleSpacing +% if any of these aren't defined, use corresponding DEFAULT value + +DEFAULT_GANTRY_SPACING = 4; +DEFAULT_DAO_GANTRY_SPACING = 8; +DEFAULT_FMO_GANTRY_SPACING = 32; + +if ~isfield(pln.propStf,'maxGantryAngleSpacing') + matRad_dispToConsole(['matRad_VMATGantryAngles: Using default value pln.propStf.maxGantryAngleSpacing = ' num2str(DEFAULT_GANTRY_SPACING) ' degree. \n'],[],'warning'); + pln.propStf.maxGantryAngleSpacing = DEFAULT_GANTRY_SPACING; % upper bound for gantry angle spacing for dose calculation +end + +if ~isfield(pln.propStf,'maxDAOGantryAngleSpacing') + matRad_dispToConsole(['matRad_VMATGantryAngles: Using default value pln.propStf.maxDAOGantryAngleSpacing= ' num2str(DEFAULT_DAO_GANTRY_SPACING) ' degree. \n'],[],'warning'); + pln.propStf.maxDAOGantryAngleSpacing = DEFAULT_DAO_GANTRY_SPACING; % upper bound for gantry angle spacing for DAO +end + +if ~isfield(pln.propStf,'maxFMOGantryAngleSpacing') + matRad_dispToConsole(['matRad_VMATGantryAngles: Using default value pln.propStf.maxFMOGantryAngleSpacing= ' num2str(DEFAULT_FMO_GANTRY_SPACING) ' degree. \n'],[],'warning'); + pln.propStf.maxFMOGantryAngleSpacing = DEFAULT_FMO_GANTRY_SPACING; % upper bound for gantry angle spacing for FMO +end + +angularRange = abs(pln.propStf.finishingAngle-pln.propStf.startingAngle); + +if pln.propStf.continuousAperture + + % angularRange = gantryAngleSpacing*numGantryAngles + % ensure that gantryAngleSpacing < maxGantryAngleSpacing (as close as + % possible) + numGantryAngles = ceil(angularRange./pln.propStf.maxGantryAngleSpacing); + gantryAngleSpacing = angularRange./numGantryAngles; + + % (numDAOGantryAngles-1)*DAOGantryAngleSpacing = (numGantryAngles-1)*gantryAngleSpacing + % where + % ensure that DAOGantryAngleSpacing < maxDAOGantryAngleSpacing (as close as + % possible) + numDAOGantryAngles = ceil((numGantryAngles-1).*gantryAngleSpacing./pln.propStf.maxDAOGantryAngleSpacing)+1; + % now ensure that numGantryAngles-1 is a multiple of numDAOGantryAngles-1 so + % that they align + numGantryAngles = (numDAOGantryAngles-1).*ceil((numGantryAngles-1)./(numDAOGantryAngles-1))+1; + gantryAngleSpacing = angularRange./numGantryAngles; + DAOGantryAngleSpacing = (angularRange-gantryAngleSpacing)/(numDAOGantryAngles-1); + + % first and last gantry angles are in centre of arc + firstGantryAngle = pln.propStf.startingAngle+gantryAngleSpacing/2; + lastGantryAngle = pln.propStf.finishingAngle-gantryAngleSpacing/2; + +else + + % ensure that an integer number of DAO gantry angles will fit in angularRange degrees, + % spaced at least as close as maxDAOGantryAngleSpacing + numDAOGantryAngles = ceil(angularRange/pln.propStf.maxDAOGantryAngleSpacing); + DAOGantryAngleSpacing = angularRange/numDAOGantryAngles; + % actual number of gantry angles is numDAOGantryAngles+1; + % ensure that DAOGantryAngleSpacing is an integer multiple of gantryAngleSpacing + numGantryAngles = ceil(numDAOGantryAngles*DAOGantryAngleSpacing/pln.propStf.maxGantryAngleSpacing); + gantryAngleSpacing = angularRange/numGantryAngles; + + % first and last gantry angles are at beginning and end of arc + firstGantryAngle = pln.propStf.startingAngle; + lastGantryAngle = pln.propStf.finishingAngle; +end + +% ensure that FMOGantryAngleSpacing is an odd integer multiple of DAOGantryAngleSpacing +numApertures = floor(pln.propStf.maxFMOGantryAngleSpacing/DAOGantryAngleSpacing); +if mod(numApertures,2) == 0 + numApertures = numApertures-1; +end +FMOGantryAngleSpacing = numApertures*DAOGantryAngleSpacing; + +firstFMOGantryAngle = firstGantryAngle+DAOGantryAngleSpacing*floor(numApertures/2); +lastFMOGantryAngle = lastGantryAngle-DAOGantryAngleSpacing*floor(numApertures/2); + +% define angles +pln.propStf.gantryAngles = firstGantryAngle:gantryAngleSpacing:lastGantryAngle; +pln.propStf.DAOGantryAngles = firstGantryAngle:DAOGantryAngleSpacing:lastGantryAngle; +pln.propStf.FMOGantryAngles = firstFMOGantryAngle:FMOGantryAngleSpacing:lastFMOGantryAngle; + +% everything else +pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); +pln.propStf.couchAngles = ones(1,pln.propStf.numOfBeams) * pln.propStf.couchAngle; +pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * pln.propStf.isoCenter; + + +%[ixMember,~] = ismember(pln.propStf.gantryAngles,pln.propStf.DAOGantryAngles); +%if sum(ixMember) <= 2 + +% TODO: use updated warning message functions +% This should give the expected behaviour according to the warning message. +% if numel(pln.propStf.FMOGantryAngles) <= 2 +% matRad_dispToConsole('matRad_VMATGantryAngles: Two or fewer beam angles will be used for FMO. \n',[],'warning') +% end + +pln.propStf.generator = 'PhotonVMAT'; diff --git a/matRad/util/matRad_visApertureInfo.m b/matRad/util/matRad_visApertureInfo.m index 2647d728a..5cc1c1308 100644 --- a/matRad/util/matRad_visApertureInfo.m +++ b/matRad/util/matRad_visApertureInfo.m @@ -42,20 +42,36 @@ function matRad_visApertureInfo(apertureInfo,mode) color(:,3) = 0; color(:,2) = 0; -% loop over all beams -for i=1:numOfBeams +if apertureInfo.runVMAT + % if doing VMAT, let wMax be the max weight across ALL angles + wMax = 0; + for i=1:numOfBeams + if wMax <= apertureInfo.beam(i).shape(1).weight + wMax = apertureInfo.beam(i).shape(1).weight; + end + end +end + +for i=1:numOfBeams + + numOfShapes = numel(apertureInfo.beam(i).shape); + % open new figure for every beam figure('units','inches') - + % get the MLC dimensions for this beam minX = apertureInfo.beam(i).MLCWindow(1); - maxX = apertureInfo.beam(i).MLCWindow(2); + maxX = apertureInfo.beam(i).MLCWindow(2); - %get maximum weight - wMax = max([apertureInfo.beam(i).shape(:).weight]); + if ~apertureInfo.runVMAT + % if not VMAT, let wMax be the max weight of a particular angle + if numOfShapes; + wMax = max([apertureInfo.beam(i).shape(:).weight]); + end + end if strcmp(mode,'leafNum') - + % get the active leaf Pairs % the leaf indices have to be flipped in order to fit to the order of % the leaf positions (1st row of leafPos is lowest row in physical @@ -63,13 +79,16 @@ function matRad_visApertureInfo(apertureInfo,mode) activeLeafInd = flipud(find(apertureInfo.beam(i).isActiveLeafPair)); end - subplotColumns = ceil(apertureInfo.beam(i).numOfShapes/2); - subplotLines = ceil(apertureInfo.beam(i).numOfShapes/subplotColumns); + %subplotColumns = ceil(apertureInfo.beam(i).numOfShapes/2); + %subplotLines = ceil(apertureInfo.beam(i).numOfShapes/subplotColumns); + subplotColumns = ceil(numOfShapes/2); + subplotLines = ceil(numOfShapes/subplotColumns); + %adjust figure position set(gcf,'pos',[0 0 1.8*subplotColumns 3*subplotLines]) - % loop over all shapes of the beam - for j = 1:apertureInfo.beam(i).numOfShapes + % loop over all shapes of the beam + for j = 1:numOfShapes % creating subplots subplot(subplotLines,subplotColumns,j) @@ -78,6 +97,7 @@ function matRad_visApertureInfo(apertureInfo,mode) num2str(apertureInfo.beam(i).shape(j).weight,2)],... 'Fontsize',8) colorInd = max(ceil((apertureInfo.beam(i).shape(j).weight/wMax)*61+eps),1); + set(gca,'Color',color(colorInd,:)); hold on @@ -86,18 +106,18 @@ function matRad_visApertureInfo(apertureInfo,mode) % loop over all active leaf pairs for k = 1:apertureInfo.beam(i).numOfActiveLeafPairs fill([minX apertureInfo.beam(i).shape(j).leftLeafPos(k) ... - apertureInfo.beam(i).shape(j).leftLeafPos(k) minX],... - [apertureInfo.beam(i).leafPairPos(k)- bixelWidth/2 ... - apertureInfo.beam(i).leafPairPos(k)- bixelWidth/2 ... - apertureInfo.beam(i).leafPairPos(k)+ bixelWidth/2 ... - apertureInfo.beam(i).leafPairPos(k)+ bixelWidth/2],'b') - fill([apertureInfo.beam(i).shape(j).rightLeafPos(k) ... - maxX maxX ... - apertureInfo.beam(i).shape(j).rightLeafPos(k)],... - [apertureInfo.beam(i).leafPairPos(k)- bixelWidth/2 ... - apertureInfo.beam(i).leafPairPos(k)- bixelWidth/2 ... - apertureInfo.beam(i).leafPairPos(k)+ bixelWidth/2 ... - apertureInfo.beam(i).leafPairPos(k)+ bixelWidth/2],'b') + apertureInfo.beam(i).shape(j).leftLeafPos(k) minX],... + [apertureInfo.beam(i).leafPairPos(k)- bixelWidth/2 ... + apertureInfo.beam(i).leafPairPos(k)- bixelWidth/2 ... + apertureInfo.beam(i).leafPairPos(k)+ bixelWidth/2 ... + apertureInfo.beam(i).leafPairPos(k)+ bixelWidth/2],[0.5 0.5 0.5]) + fill([apertureInfo.beam(i).shape(j).rightLeafPos(k) ... + maxX maxX ... + apertureInfo.beam(i).shape(j).rightLeafPos(k)],... + [apertureInfo.beam(i).leafPairPos(k)- bixelWidth/2 ... + apertureInfo.beam(i).leafPairPos(k)- bixelWidth/2 ... + apertureInfo.beam(i).leafPairPos(k)+ bixelWidth/2 ... + apertureInfo.beam(i).leafPairPos(k)+ bixelWidth/2],[0.5 0.5 0.5]) end elseif strcmp(mode,'leafNum') % loop over all active leaf pairs @@ -107,28 +127,28 @@ function matRad_visApertureInfo(apertureInfo,mode) [activeLeafInd(k) - 1/2 ... activeLeafInd(k) - 1/2 ... activeLeafInd(k) + 1/2 ... - activeLeafInd(k) + 1/2],'b') + activeLeafInd(k) + 1/2],[0.5 0.5 0.5]) fill([apertureInfo.beam(i).shape(j).rightLeafPos(k) ... maxX maxX ... apertureInfo.beam(i).shape(j).rightLeafPos(k)],... [activeLeafInd(k) - 1/2 ... activeLeafInd(k) - 1/2 ... activeLeafInd(k) + 1/2 ... - activeLeafInd(k) + 1/2],'b') + activeLeafInd(k) + 1/2],[0.5 0.5 0.5]) end end - + axis tight xlabel('horiz. pos. [mm]') - + if strcmp(mode,'physical') ylabel('vert. pos. [mm]') elseif strcmp(mode,'leafNum') ylabel('leaf pair #') end - + end - + end end diff --git a/test/sequencing/test_engelLeafSequencing.m b/test/sequencing/test_engelLeafSequencing.m index 98e0378fe..f8b5819e4 100644 --- a/test/sequencing/test_engelLeafSequencing.m +++ b/test/sequencing/test_engelLeafSequencing.m @@ -12,21 +12,23 @@ % Test Case, and add them to the test-runner initTestSuite; - function [resultGUI,stf,dij] = helper_getTestData() + function [resultGUI,stf,dij,pln] = helper_getTestData() p = load('photons_testData.mat'); resultGUI = p.resultGUI; stf = p.stf; dij = p.dij; + pln = p.pln; function test_run_sequencing_basic - [resultGUI,stf,dij] = helper_getTestData(); + [resultGUI,stf,dij,pln] = helper_getTestData(); fn_old = fieldnames(resultGUI); numOfLevels = [1,10]; for levels = numOfLevels - resultGUI_sequenced = matRad_engelLeafSequencing(resultGUI,stf,dij,levels); + pln.propSeq.numLevels = levels; + resultGUI_sequenced = matRad_engelLeafSequencing(resultGUI,stf,dij,pln); fn_new = fieldnames(resultGUI_sequenced); for i = 1:numel(fn_old) diff --git a/test/sequencing/test_siochiLeafSequencing.m b/test/sequencing/test_siochiLeafSequencing.m index af6e91859..3ed020a45 100644 --- a/test/sequencing/test_siochiLeafSequencing.m +++ b/test/sequencing/test_siochiLeafSequencing.m @@ -12,21 +12,23 @@ % Test Case, and add them to the test-runner initTestSuite; - function [resultGUI,stf,dij] = helper_getTestData() +function [resultGUI,stf,dij,pln] = helper_getTestData() p = load('photons_testData.mat'); resultGUI = p.resultGUI; stf = p.stf; dij = p.dij; + pln = p.pln; function test_run_sequencing_basic - [resultGUI,stf,dij] = helper_getTestData(); + [resultGUI,stf,dij,pln] = helper_getTestData(); fn_old = fieldnames(resultGUI); - numOfLevels = [1,10]; + numOfLevels = [1,10]; for levels = numOfLevels - resultGUI_sequenced = matRad_siochiLeafSequencing(resultGUI,stf,dij,levels); + pln.propSeq.numLevels = levels; + resultGUI_sequenced = matRad_siochiLeafSequencing(resultGUI,stf,dij,pln); fn_new = fieldnames(resultGUI_sequenced); for i = 1:numel(fn_old) diff --git a/test/sequencing/test_xiaLeafSequencing.m b/test/sequencing/test_xiaLeafSequencing.m index 9d6aaf943..f8dadf48e 100644 --- a/test/sequencing/test_xiaLeafSequencing.m +++ b/test/sequencing/test_xiaLeafSequencing.m @@ -12,21 +12,23 @@ % Test Case, and add them to the test-runner initTestSuite; -function [resultGUI,stf,dij] = helper_getTestData() +function [resultGUI,stf,dij,pln] = helper_getTestData() p = load('photons_testData.mat'); resultGUI = p.resultGUI; stf = p.stf; dij = p.dij; + pln = p.pln; function test_run_sequencing_basic - [resultGUI,stf,dij] = helper_getTestData(); + [resultGUI,stf,dij,pln] = helper_getTestData(); fn_old = fieldnames(resultGUI); numOfLevels = [1,10]; for levels = numOfLevels - resultGUI_sequenced = matRad_xiaLeafSequencing(resultGUI,stf,dij,levels); + pln.propSeq.numLevels = levels; + resultGUI_sequenced = matRad_xiaLeafSequencing(resultGUI,stf,dij,pln); fn_new = fieldnames(resultGUI_sequenced); for i = 1:numel(fn_old)