diff --git a/examples/matRad_example8_photonsVMAT.m b/examples/matRad_example8_photonsVMAT.m new file mode 100644 index 000000000..d6907b02d --- /dev/null +++ b/examples/matRad_example8_photonsVMAT.m @@ -0,0 +1,121 @@ +%% 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'; +pln.machine = 'Generic'; + +pln.bioModel = 'none'; % biological RBE model, not interesting for photons +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.gantryAngles = [-180, 180]; % gantry arc anchor points +pln.propStf.couchAngles = [0, 0]; % couch angle for arcs +% pln.propStf.arcIndex = [1 1]; % assign anchor points to arcs (if more than one arc is defined) +pln.propStf.maxGantryAngleSpacing = 15; % [deg] / max gantry angle spacing for dose calculation +pln.propStf.maxDAOGantryAngleSpacing = 30; % [deg] / max gantry angle spacing for DAO +pln.propStf.maxFMOGantryAngleSpacing = 45; % [deg] / max gantry angle spacing for FMO +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 07c5ab4df..42b902a8a 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 matRad_ispropCompat(stfGen,'numOfBeams') numOfBeams = stfGen.numOfBeams; diff --git a/matRad/gui/widgets/matRad_ViewingWidget.m b/matRad/gui/widgets/matRad_ViewingWidget.m index e4775f7bd..dc84f6b6d 100644 --- a/matRad/gui/widgets/matRad_ViewingWidget.m +++ b/matRad/gui/widgets/matRad_ViewingWidget.m @@ -1159,7 +1159,15 @@ function initValues(this) if isfield(pln,'propStf') && isfield(pln.propStf,'isoCenter') isoCoordinates = matRad_world2cubeIndex(pln.propStf.isoCenter(1,:), ct); planeCenters = ceil(isoCoordinates); - this.numOfBeams=numel(pln.propStf.gantryAngles); + + 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 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..12ada1f6c --- /dev/null +++ b/matRad/matRad_doseRecalc.m @@ -0,0 +1,158 @@ +function recalc = matRad_doseRecalc(cst, pln, recalc, ct, apertureInfo, calcDoseDirect, dij) +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% matRad function to recalculate dose on a finer or equal angular +% resolution, either by interpolating aperture shapes or by reusing the +% nearest existing Dij column. +% +% call +% recalc = matRad_doseRecalc(cst,pln,recalc,ct,apertureInfo) +% recalc = matRad_doseRecalc(cst,pln,recalc,ct,apertureInfo,calcDoseDirect) +% recalc = matRad_doseRecalc(cst,pln,recalc,ct,apertureInfo,calcDoseDirect,dij) +% +% input +% cst, ct: patient data +% pln: original optimisation plan (anchor angles + propOpt) +% recalc: recalc options struct (interpNew, dijNew, +% continuousAperture, pln with recalc spacing, ...) +% apertureInfo: aperture info from the optimisation result +% calcDoseDirect: (optional, default true) use direct dose calc +% dij: (optional) Dij for back-projection when ~calcDoseDirect +% +% output +% recalc: updated struct with stf, apertureInfo, and resultGUI +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% 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; + +% Old fine angles are the DAO beams from the original optimisation. +% They live in apertureInfo, so we do not need pln.propStf for this. +oldFineAngles = [apertureInfo.beam.gantryAngle]; + +% If not interpolating new apertures, force every fine angle to be a DAO +% control point by equalising the two spacing parameters. +if ~recalc.interpNew + recalc.pln.propStf.maxDAOGantryAngleSpacing = recalc.pln.propStf.maxGantryAngleSpacing; +end + +% Generate (or load cached) stf at the recalc angular resolution. +% recalc.pln.propStf.gantryAngles contains arc anchor points; the fine +% angle grid is computed internally by matRad_StfGeneratorPhotonVMAT. +fname = sprintf('%.1f deg.mat', recalc.pln.propStf.maxGantryAngleSpacing); +if exist(fname, 'file') + load(fname, 'stf'); +else + stf = matRad_generateStf(ct, cst, recalc.pln); + save(fname, 'stf'); +end +recalc.stf = stf; +clear stf; + +% ----------------------------------------------------------------------- +% Handle angles that fall exactly equidistant between two old DAO angles. +% These need duplicate stf entries so that both neighbouring Dij columns +% can be used (one per side). +% ----------------------------------------------------------------------- +if ~recalc.interpNew || ~recalc.dijNew + + newFineAngles = [recalc.stf.gantryAngle]; + + duplicate = false(size(newFineAngles)); + for i = 1:numel(newFineAngles) + diffs = abs(newFineAngles(i) - oldFineAngles); + duplicate(i) = sum(diffs == min(diffs)) > 1; + end + + tempStf = recalc.stf; + [tempStf(:).copyInd] = deal([]); + [tempStf(:).stfCorr] = deal([]); + + j = 1; + for i = 1:numel(newFineAngles) + if duplicate(i) + % Left copy: pretend this beam sits at the previous beam angle + tempStf(j) = recalc.stf(i); + tempStf(j).gantryAngle = recalc.stf(i - 1).gantryAngle; + tempStf(j).copyInd = 1; + tempStf(j).stfCorr = false; + j = j + 1; + + % Right copy: pretend this beam sits at the next beam angle + tempStf(j) = recalc.stf(i); + tempStf(j).gantryAngle = recalc.stf(i + 1).gantryAngle; + tempStf(j).copyInd = 2; + tempStf(j).stfCorr = false; + else + tempStf(j) = recalc.stf(i); + tempStf(j).copyInd = []; + tempStf(j).stfCorr = true; + end + j = j + 1; + end + recalc.stf = tempStf; + + % ------------------------------------------------------------------- + % Dij reuse: redirect each new beam to the nearest old beam so that + % an existing Dij column can be recycled without recomputation. + % ------------------------------------------------------------------- + tempStf = recalc.stf; + for i = 1:numel(recalc.stf) + diffs = abs(recalc.stf(i).gantryAngle - oldFineAngles); + minDiff = min(diffs); + nearIdx = find(diffs == minDiff); % 1 or 2 indices into oldFineAngles + + % Find where those old angles appear in the (post-duplicate) stf + newAngles = [tempStf.gantryAngle]; + minInd1 = find(newAngles == oldFineAngles(nearIdx(1)), 1); + minInd2 = find(newAngles == oldFineAngles(nearIdx(end)), 1); + + if ~recalc.dijNew + % Replace this beam with the nearest existing beam entirely + if isempty(recalc.stf(i).copyInd) || recalc.stf(i).copyInd == 1 + recalc.stf(i) = tempStf(minInd1); + elseif recalc.stf(i).copyInd == 2 + recalc.stf(i) = tempStf(minInd2); + end + elseif ~recalc.interpNew + % Keep the beam but correct its angle if it is equidistant + if numel(nearIdx) > 1 + recalc.stf(i).gantryAngle = tempStf(i).gantryAngle; + end + end + end +end + +recalc = matRad_recalcApertureInfo(recalc, recalc.apertureInfo); + +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 be36552b5..a7ea64910 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..8caa5a2a0 --- /dev/null +++ b/matRad/matRad_recalcApertureInfo.m @@ -0,0 +1,259 @@ +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. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +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 over 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 = stf(j).gantryAngle; + 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; diff --git a/matRad/matRad_sequencing.m b/matRad/matRad_sequencing.m index 51207ada2..b85aa62ac 100644 --- a/matRad/matRad_sequencing.m +++ b/matRad/matRad_sequencing.m @@ -48,18 +48,55 @@ 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 + + if isfield(pln,'propOpt') && isfield(pln.propOpt,'preconditioner') + preconditioner = pln.propOpt.preconditioner; + else + preconditioner = false; + end + + if isfield(pln,'propOpt') && isfield(pln.propOpt,'runVMAT') + dynamic = pln.propOpt.runVMAT; + else + dynamic = false; end + + if isfield(pln,'propOpt') && isfield(pln.propOpt,'numApertures') + numApertures = pln.propOpt.numApertures; + else + numApertures = 0; + end + + if isfield(pln,'propOpt') && isfield(pln.propOpt,'continuousAperture') + continuousAperture = pln.propOpt.continuousAperture; + else + continuousAperture = false; + end + + varArgList = { ... + 'visBool',visBool, ... + 'dynamic',dynamic, ... + 'numApertures',numApertures, ... + 'continuousAperture',continuousAperture, ... + 'preconditioner',preconditioner}; + % 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.propSeq.numLevels,varArgList{:}); case 'engel' - resultGUI = matRad_engelLeafSequencing(resultGUI,stf,dij,pln.propSeq.sequencingLevel,visBool); + resultGUI = matRad_engelLeafSequencing(resultGUI,stf,dij,pln.propSeq.numLevels,varArgList{:}); case 'siochi' - resultGUI = matRad_siochiLeafSequencing(resultGUI,stf,dij,pln.propSeq.sequencingLevel,visBool); + resultGUI = matRad_siochiLeafSequencing(resultGUI,stf,dij,pln.propSeq.numLevels,varArgList{:}); 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/phantoms/builder/matRad_PhantomBuilder.m b/matRad/phantoms/builder/matRad_PhantomBuilder.m index 46a1ab0e7..a8ef012dc 100644 --- a/matRad/phantoms/builder/matRad_PhantomBuilder.m +++ b/matRad/phantoms/builder/matRad_PhantomBuilder.m @@ -1,42 +1,47 @@ classdef matRad_PhantomBuilder < handle % matRad_PhantomBuilder - % Class that helps to create radiotherapy phantoms + % Class that helps to create radiotherapy phantoms % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2023 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/LICENSE.md. No part - % of the matRad project, including this file, may be copied, modified, - % propagated, or distributed except according to the terms contained in the + % Copyright 2023 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/LICENSE.md. 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. % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% properties (Access = public) - volumes = {}; + volumes = {} end - properties (Access = private) - ct; - cst = {}; + properties (Access = private) + ct + cst = {} end methods (Access = public) - function obj = matRad_PhantomBuilder(ctDim,ctResolution,numOfCtScen) + + function obj = matRad_PhantomBuilder(ctDim, ctResolution, numOfCtScen) obj.ct = struct(); - obj.ct.cubeDim = [ctDim(2),ctDim(1),ctDim(3)]; - obj.ct.resolution.x = ctResolution(1); - obj.ct.resolution.y = ctResolution(2); - obj.ct.resolution.z = ctResolution(3); + obj.ct.cubeDim = [ctDim(2), ctDim(1), ctDim(3)]; + if isstruct(ctResolution) + obj.ct.resolution = ctResolution; + else + obj.ct.resolution.x = ctResolution(1); + obj.ct.resolution.y = ctResolution(2); + obj.ct.resolution.z = ctResolution(3); + end obj.ct.numOfCtScen = numOfCtScen; obj.ct.cubeHU{1} = ones(obj.ct.cubeDim) * -1000; end - %functions to create Targets %TODO: Option to extend volumes - function addBoxTarget(obj,name,dimensions,varargin) + % functions to create Targets %TODO: Option to extend volumes + function addBoxTarget(obj, name, dimensions, varargin) % Adds a box target % % input: @@ -50,11 +55,11 @@ function addBoxTarget(obj,name,dimensions,varargin) % of objectives % 'HU': Houndsfield unit of the volume - obj.volumes(end+1) = {matRad_PhantomVOIBox(name,'TARGET',dimensions,varargin{:})}; + obj.volumes(end + 1) = {matRad_PhantomVOIBox(name, 'TARGET', dimensions, varargin{:})}; obj.updatecst(); end - function addSphericalTarget(obj,name,radius,varargin) + function addSphericalTarget(obj, name, radius, varargin) % Adds a spherical target % % input: @@ -68,12 +73,11 @@ function addSphericalTarget(obj,name,radius,varargin) % of objectives % 'HU': Houndsfield unit of the volume - obj.volumes(end+1) = {matRad_PhantomVOISphere(name,'TARGET',radius,varargin{:})}; + obj.volumes(end + 1) = {matRad_PhantomVOISphere(name, 'TARGET', radius, varargin{:})}; obj.updatecst(); end - - function addBoxOAR(obj,name,dimensions,varargin) + function addBoxOAR(obj, name, dimensions, varargin) % Adds a box OAR % % input: @@ -87,11 +91,11 @@ function addBoxOAR(obj,name,dimensions,varargin) % of objectives % 'HU': Houndsfield unit of the volume - obj.volumes(end+1) = {matRad_PhantomVOIBox(name,'OAR',dimensions,varargin{:})}; + obj.volumes(end + 1) = {matRad_PhantomVOIBox(name, 'OAR', dimensions, varargin{:})}; obj.updatecst(); end - function addSphericalOAR(obj,name,radius,varargin) + function addSphericalOAR(obj, name, radius, varargin) % Adds a spherical OAR % % input: @@ -105,39 +109,38 @@ function addSphericalOAR(obj,name,radius,varargin) % of objectives % 'HU': Houndsfield unit of the volume - obj.volumes(end+1) ={matRad_PhantomVOISphere(name,'OAR',radius,varargin{:})}; + obj.volumes(end + 1) = {matRad_PhantomVOISphere(name, 'OAR', radius, varargin{:})}; obj.updatecst(); end - - function [ct,cst] = getctcst(obj) + function [ct, cst] = getctcst(obj) % Returns the ct and struct. The function also initializes - % the HUs in reverse order of defintion + % the HUs in reverse order of definition % % output % ct: matRad ct struct % cst: matRad cst struct - - %initialize the HU in reverse order of definition (objectives - %defined at the start will have the highest priority in case of - %overlaps) - - for i = 1:size(obj.cst,1) - vIxVOI = obj.cst{end-i+1,4}{1}; - obj.ct.cubeHU{1}(vIxVOI) = obj.volumes{1,end-i+1}.HU; % assign HU + + % initialize the HU in reverse order of definition (objectives + % defined at the start will have the highest priority in case of + % overlaps) + + for i = 1:size(obj.cst, 1) + vIxVOI = obj.cst{end - i + 1, 4}{1}; + obj.ct.cubeHU{1}(vIxVOI) = obj.volumes{1, end - i + 1}.HU; % assign HU end - + ct = obj.ct; cst = obj.cst; end - end + end methods (Access = private) - function updatecst(obj) - obj.cst = obj.volumes{end}.initializeParameters(obj.ct,obj.cst); + function updatecst(obj) + obj.cst = obj.volumes{end}.initializeParameters(obj.ct, obj.cst); end end -end \ No newline at end of file +end diff --git a/matRad/phantoms/builder/matRad_PhantomVOIBox.m b/matRad/phantoms/builder/matRad_PhantomVOIBox.m index e66c7b1fa..2895b85c1 100644 --- a/matRad/phantoms/builder/matRad_PhantomVOIBox.m +++ b/matRad/phantoms/builder/matRad_PhantomVOIBox.m @@ -18,56 +18,76 @@ % LICENSE file. % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - properties %additional property of cubic objects - boxDimensions; + properties % additional property of cubic objects + boxDimensions end methods (Access = public) - function obj = matRad_PhantomVOIBox(name,type,boxDimensions,varargin) + function obj = matRad_PhantomVOIBox(name, type, boxDimensions, varargin) p = inputParser; - addParameter(p,'objectives',{}); - addParameter(p,'offset',[0,0,0]); - addParameter(p,'HU',0); - parse(p,varargin{:}); + addParameter(p, 'objectives', {}); + addParameter(p, 'offset', [0, 0, 0]); + addParameter(p, 'HU', 0); + addParameter(p, 'coordType', 'voxel', @(x) numel(validatestring(x, {'voxel', 'mm'}))); + parse(p, varargin{:}); - obj@matRad_PhantomVOIVolume(name,type,p); %call superclass constructor + obj@matRad_PhantomVOIVolume(name, type, p); % call superclass constructor obj.boxDimensions = boxDimensions; end - function [cst] = initializeParameters(obj,ct,cst) - %add this objective to the phantomBuilders cst - - cst = initializeParameters@matRad_PhantomVOIVolume(obj,cst); - center = round(ct.cubeDim/2); - VOIHelper = zeros(ct.cubeDim); - offsets = obj.offset; - dims = obj.boxDimensions; - - xMinMax = center(2)+offsets(1) + round(dims(1)/2)*[-1,1]; - yMinMax = center(1)+offsets(2) + round(dims(2)/2)*[-1,1]; - zMinMax = center(3)+offsets(3) + round(dims(3)/2)*[-1,1]; - - %Correct if out of bounds - xMinMax(xMinMax < 1) = 1; - yMinMax(yMinMax < 1) = 1; - zMinMax(zMinMax < 1) = 1; - - xMinMax(xMinMax > ct.cubeDim(2)) = ct.cubeDim(2); - yMinMax(yMinMax > ct.cubeDim(1)) = ct.cubeDim(1); - zMinMax(zMinMax > ct.cubeDim(3)) = ct.cubeDim(3); - - for x = xMinMax(1):1:xMinMax(2) - for y = yMinMax(1):1:yMinMax(2) - for z = zMinMax(1):1:zMinMax(2) - VOIHelper(y,x,z) = 1; - end - end + function [cst] = initializeParameters(obj, ct, cst) + % add this objective to the phantomBuilders cst + ct = matRad_getWorldAxes(ct); + cst = initializeParameters@matRad_PhantomVOIVolume(obj, cst); + + % Swaps [i j k] (x-first) <-> [j i k] (y-first / MATLAB array order) + dimPerm = [0 1 0; 1 0 0; 0 0 1]; + + % Center in [j i k] (cubeDim is already in MATLAB array order) + centerPoint = (ct.cubeDim + 1) / 2; + + switch obj.coordType + case 'voxel' + ctMin = [1 1 1]; + ctMax = ct.cubeDim; % [j i k] + [y, x, z] = ndgrid(1:ct.cubeDim(1), 1:ct.cubeDim(2), 1:ct.cubeDim(3)); + + case 'mm' + % cubeIndex2worldCoords expects [i j k], outputs [x y z]; + % * dimPerm converts to [y x z] = [j i k] in world mm + centerPoint = matRad_cubeIndex2worldCoords(centerPoint, ct) * dimPerm; + halfRes = [ct.resolution.y ct.resolution.x ct.resolution.z] / 2; + ctMin = [min(ct.y) min(ct.x) min(ct.z)] - halfRes; + ctMax = [max(ct.y) max(ct.x) max(ct.z)] + halfRes; + % ct.y has nRows elements (dim1), ct.x has nCols elements (dim2) + [y, x, z] = ndgrid(ct.y, ct.x, ct.z); end - - cst{end,4}{1} = find(VOIHelper); - + % offset and boxDimensions are in [i j k]; convert to [j i k] + centerPoint = centerPoint + obj.offset * dimPerm; + dims = obj.boxDimensions * dimPerm; + + coords = [y(:) x(:) z(:)]; % [j i k] + + maxPoints = min(centerPoint + dims / 2, ctMax); + minPoints = max(centerPoint - dims / 2, ctMin); + + voiHelper = all(coords >= minPoints & coords <= maxPoints, 2); + voiHelper = reshape(voiHelper, ct.cubeDim); + + cst{end, 4}{1} = find(voiHelper); end + + end + + % Set Methods + methods + + function set.boxDimensions(obj, dims) + validateattributes(dims, {'numeric'}, {'vector', 'numel', 3, 'positive'}); + obj.boxDimensions = dims; + end + end -end \ No newline at end of file +end diff --git a/matRad/phantoms/builder/matRad_PhantomVOISphere.m b/matRad/phantoms/builder/matRad_PhantomVOISphere.m index c0e457f62..fabb98fb7 100644 --- a/matRad/phantoms/builder/matRad_PhantomVOISphere.m +++ b/matRad/phantoms/builder/matRad_PhantomVOISphere.m @@ -19,42 +19,67 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% properties - radius; + radius end methods (Access = public) - function obj = matRad_PhantomVOISphere(name,type,radius,varargin) + + function obj = matRad_PhantomVOISphere(name, type, radius, varargin) p = inputParser; - addParameter(p,'objectives',{}); - addParameter(p,'offset',[0,0,0]); - addParameter(p,'HU',0); - parse(p,varargin{:}); + addParameter(p, 'objectives', {}); + addParameter(p, 'offset', [0, 0, 0]); + addParameter(p, 'HU', 0); + addParameter(p, 'coordType', 'voxel', @(x) numel(validatestring(x, {'voxel', 'mm'}))); % numel trick to guarantee logical cast + parse(p, varargin{:}); - obj@matRad_PhantomVOIVolume(name,type,p); %call superclass constructor + obj@matRad_PhantomVOIVolume(name, type, p); % call superclass constructor obj.radius = radius; end - function [cst] = initializeParameters(obj,ct,cst) - %add this VOI to the phantomBuilders cst - - cst = initializeParameters@matRad_PhantomVOIVolume(obj,cst); - center = round([ct.cubeDim/2]); - VOIHelper = zeros(ct.cubeDim); - offsets = obj.offset; - - for x = 1:ct.cubeDim(2) - for y = 1:ct.cubeDim(1) - for z = 1:ct.cubeDim(3) - currPost = [y x z] + offsets - center; - if (sqrt(sum(currPost.^2)) < obj.radius) - VOIHelper(y,x,z) = 1; - end - end - end + function [cst] = initializeParameters(obj, ct, cst) + % add this VOI to the phantomBuilders cst + ct = matRad_getWorldAxes(ct); + cst = initializeParameters@matRad_PhantomVOIVolume(obj, cst); + + % Swaps [i j k] (x-first) <-> [j i k] (y-first / MATLAB array order) + dimPerm = [0 1 0; 1 0 0; 0 0 1]; + + % center as continuuos [j i k] + centerPoint = (ct.cubeDim + 1) / 2; + + switch obj.coordType + case 'voxel' + % Grid in [j i k]: y (rows) along dim1, x (cols) along dim2 + [y, x, z] = ndgrid(1:ct.cubeDim(1), 1:ct.cubeDim(2), 1:ct.cubeDim(3)); + + case 'mm' + % cubeIndex2worldCoords expects [j i k], outputs [x y z]; + % apply dimPerm to arrive at [y x z] = [j i k] in world mm + centerPoint = matRad_cubeIndex2worldCoords(centerPoint, ct) * dimPerm; + % ct.y has nRows elements (dim1), ct.x has nCols elements (dim2) + [y, x, z] = ndgrid(ct.y, ct.x, ct.z); end - - cst{end,4}{1} = find(VOIHelper); - + + % offset is always in [i j k]; convert to [j i k] before adding + centerPoint = centerPoint + obj.offset * dimPerm; + + % Both modes: grid and center are in [j i k] - no extra permutation needed + voiHelper = vecnorm([y(:) x(:) z(:)] - centerPoint, 2, 2) < obj.radius; + voiHelper = reshape(voiHelper, ct.cubeDim); + + cst{end, 4}{1} = find(voiHelper); + end + + end + + % Set Methods + methods + + function set.radius(obj, value) + validateattributes(value, {'numeric'}, {'scalar', 'positive'}); + obj.radius = value; + end + end -end \ No newline at end of file +end diff --git a/matRad/phantoms/builder/matRad_PhantomVOIVolume.m b/matRad/phantoms/builder/matRad_PhantomVOIVolume.m index 8f35db910..b3d87b5de 100644 --- a/matRad/phantoms/builder/matRad_PhantomVOIVolume.m +++ b/matRad/phantoms/builder/matRad_PhantomVOIVolume.m @@ -1,84 +1,96 @@ classdef (Abstract) matRad_PhantomVOIVolume < handle -% matRad_PhantomVOIVolume: Interface for VOI Volumes -% This abstract base class provides the structure of VOI Volumes. -% So far implemented: Box and spherical objectives -% -% -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% -% Copyright 2023 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/LICENSE.md. 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. -% -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + % matRad_PhantomVOIVolume: Interface for VOI Volumes + % This abstract base class provides the structure of VOI Volumes. + % So far implemented: Box and spherical objectives + % + % + % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + % + % Copyright 2023 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/LICENSE.md. 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. + % + % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% properties - name; - type; - TissueClass = 1; - alphaX = 0.1000; - betaX = 0.0500; - Priority = 1; - Visible = 1; - visibleColor = [0 0 0]; - HU = 0; - offset = [0,0,0]; %center of objective - objectives = {}; - colors = [[1,0,0];[0,1,0];[0,0,1];[1,1,0];[1,0,1];[0,1,1];[1,1,1]]; + name + type + TissueClass = 1 % mh:ignore_style + alphaX = 0.1000 + betaX = 0.0500 + Priority = 1 % mh:ignore_style + Visible = 1 % mh:ignore_style + visibleColor = [0 0 0] + HU = 0 + offset = [0, 0, 0] % center of objective + coordType = 'voxel' % 'voxel' or 'mm' - governs how dimensions and offset are interpreted + objectives = {} + colors = [[1, 0, 0]; [0, 1, 0]; [0, 0, 1]; [1, 1, 0]; [1, 0, 1]; [0, 1, 1]; [1, 1, 1]] end methods - function obj = matRad_PhantomVOIVolume(name,type,p) - %p is the input parser used in the child classes to check for additional variables - + + function obj = matRad_PhantomVOIVolume(name, type, p) + % p is the input parser used in the child classes to check for additional variables + obj.name = name; obj.type = type; obj.offset = p.Results.offset; obj.HU = p.Results.HU; + obj.coordType = p.Results.coordType; - - %idea is that DoseObjectiveFunction can be either a single objective or an - %array of objectives. If it is a single objective store it as a cell array + % idea is that DoseObjectiveFunction can be either a single objective or an + % array of objectives. If it is a single objective store it as a cell array if iscell(p.Results.objectives) obj.objectives = p.Results.objectives; - else + else obj.objectives = {p.Results.objectives}; end %} end - function cst = initializeParameters(obj,cst) - %initialize entry for this VOI in cst - nxIdx = size(cst,1)+1; - cst{nxIdx,1} = nxIdx-1; - cst{nxIdx,2} = obj.name; - cst{nxIdx,3} = obj.type; - cst{nxIdx,5}.TissueClass = obj.TissueClass; - cst{nxIdx,5}.alphaX = obj.alphaX; - cst{nxIdx,5}.betaX = obj.betaX; - cst{nxIdx,5}.Priority = nxIdx; - cst{nxIdx,5}.Visible = obj.Visible; + function cst = initializeParameters(obj, cst) + % initialize entry for this VOI in cst + nxIdx = size(cst, 1) + 1; + cst{nxIdx, 1} = nxIdx - 1; + cst{nxIdx, 2} = obj.name; + cst{nxIdx, 3} = obj.type; + cst{nxIdx, 5}.TissueClass = obj.TissueClass; + cst{nxIdx, 5}.alphaX = obj.alphaX; + cst{nxIdx, 5}.betaX = obj.betaX; + cst{nxIdx, 5}.Priority = nxIdx; + cst{nxIdx, 5}.Visible = obj.Visible; - if nxIdx <= size(obj.colors,1) - obj.visibleColor = obj.colors(nxIdx,:); + if nxIdx <= size(obj.colors, 1) + obj.visibleColor = obj.colors(nxIdx, :); end - cst{nxIdx,5}.visibleColor = obj.visibleColor; + cst{nxIdx, 5}.visibleColor = obj.visibleColor; - if ~iscell(obj.objectives) %should be redundant - DoseObjectives = {obj.objectives}; + if ~iscell(obj.objectives) % should be redundant + DoseObjectives = {obj.objectives}; else DoseObjectives = obj.objectives; end for i = 1:numel(DoseObjectives) - cst{nxIdx,6} {i}= DoseObjectives{i}; + cst{nxIdx, 6} {i} = DoseObjectives{i}; end end + + end + + % Set methods + methods + + function set.coordType(obj, cType) + obj.coordType = validatestring(cType, {'voxel', 'mm'}); + end + end -end \ No newline at end of file +end diff --git a/matRad/sequencing/matRad_arcSequencing.m b/matRad/sequencing/matRad_arcSequencing.m new file mode 100644 index 000000000..f9261023f --- /dev/null +++ b/matRad/sequencing/matRad_arcSequencing.m @@ -0,0 +1,119 @@ +function beam = matRad_arcSequencing(sequencing,stf,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(sequencing, stf, weightToMU) +% +% input +% sequencing: shape sequencing structure +% stf: matRad steering struct (beam geometry etc.) +% weightToMU: conversion factor from weight to MU +% +% +% 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. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + +numOfBeams = numel(stf); + +beam = sequencing.beam; + +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 = sequencing.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 bc1436574..b7088951c 100644 --- a/matRad/sequencing/matRad_engelLeafSequencing.m +++ b/matRad/sequencing/matRad_engelLeafSequencing.m @@ -1,18 +1,20 @@ -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,numOfLevels,varargin) +% 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,numOfLevels,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 -% visBool: toggle on/off visualization (optional) +% numOfLevels: number of intensity levels for the sequencing +% optional key-value pairs +% visBool: toggle on/off visualization (optional - default: false) +% dynamic: toggle on/off dynamic delivery (optional - default: false) % % output % resultGUI: matRad result struct containing the new dose cube @@ -34,9 +36,33 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% if visBool not set toogle off visualization -if nargin < 5 - visBool = 0; +matRad_cfg = MatRad_Config.instance(); + +p = inputParser(); +p.KeepUnmatched = true; +p.addRequired('resultGUI',@(x) isstruct(x)); +p.addRequired('stf',@(x) isstruct(x)); +p.addRequired('dij',@(x) isstruct(x)); +p.addRequired('numOfLevels',@(x) isnumeric(x) && isscalar(x) && x > 0); +p.addParameter('visBool',false,@(x) isscalar(x) && (islogical(x) || (isnumeric(x) && (x==0 || x==1)))); +p.addParameter('dynamic',false,@(x) isscalar(x) && (islogical(x) || (isnumeric(x) && (x==0 || x==1)))); +p.addParameter('continuousAperture',false,@(x) isscalar(x) && (islogical(x) || (isnumeric(x) && (x==0 || x==1)))); +p.addParameter('preconditioner',false,@(x) isscalar(x) && (islogical(x) || (isnumeric(x) && (x==0 || x==1)))); +p.parse(resultGUI,stf,dij,numOfLevels,varargin{:}); + +numOfLevels = p.Results.numOfLevels; +visBool = p.Results.visBool; +dynamic = p.Results.dynamic; +continuousAperture = p.Results.continuousAperture; +preconditioner = p.Results.preconditioner; + +if dynamic + matRad_cfg.dispWarning(['The Engel leaf sequencing implementation is not designed for dynamic delivery. ', ... + 'Using these sequences for VMAT / other dynamic delivery may fail or yield non-deliverable plans.']); +end +if continuousAperture + matRad_cfg.dispWarning(['The Engel leaf sequencing implementation is not designed for continuous aperture computation. ', ... + 'Using these sequences for continuous aperture delivery may fail or yield non-deliverable plans.']); end numOfBeams = numel(stf); @@ -352,8 +378,8 @@ clear rightIntLimit; end - if sum(wOfCurrBeams)>0 + if sum(wOfCurrBeams)>0 sequencing.beam(i).numOfShapes = k; sequencing.beam(i).shapes = shapes(:,:,1:k); sequencing.beam(i).shapesWeight = shapesWeight(1:k)/numOfLevels*calFac; @@ -371,14 +397,74 @@ sequencing.w(1+offset:numOfRaysPerBeam+offset,1) = D_0(indInFluenceMx)/numOfLevels*calFac; offset = offset + numOfRaysPerBeam; +end + +sequencing.dynamic = dynamic; +sequencing.continuousAperture = continuousAperture; +sequencing.preconditioner = preconditioner; + +machineName = unique({stf.machine}); +radiationMode = unique({stf.radiationMode}); + +if numel(machineName) > 1 || numel(radiationMode) > 1 + matRad_cfg.dispError('Mixed Sequencing currently not supported for Siochi Leaf Sequencer'); +end + +machine = load([radiationMode{1} '_' machineName{1}]); +if ~isfield(machine, 'constraints') + sequencing.constraints = struct( ... + 'gantryRotationSpeed', [0 6], ... %degree/s + 'leafSpeed', [0 60], ... %mm/s + 'monitorUnitRate', [1.25 10]); %MU/s +else + sequencing.constraints = machine.constraints; +end + +if ~isfield(dij,'weightToMU') + dij.weightToMU = 100; + matRad_cfg.dispWarning('No weight to MU scaling factor defined in dij. Assuming %.1f.',dij.weightToMU); +end + +if dynamic + + % do arc sequencing + sequencing.beam = matRad_arcSequencing(sequencing,stf,dij.weightToMU); + + % carry variables + sequencing.weightToMU = dij.weightToMU; + + % get apertureInfo + resultGUI.apertureInfo = matRad_sequencing2ApertureInfo(sequencing,stf); + + %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); + + resultGUI.apertureInfo = matRad_OptimizationProblemDAO.matRad_daoVec2ApertureInfo(resultGUI.apertureInfo,resultGUI.apertureInfo.apertureVector); +end +if preconditioner + % calculate preconditioning factors for the apertures + 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 @@ -386,7 +472,7 @@ doseSequencedDoseGrid, ... dij.ctGrid.x,dij.ctGrid.y',dij.ctGrid.z); -% if weights exists from an former DAO remove it +% if weights exists from a former DAO remove it if isfield(resultGUI,'wDao') resultGUI = rmfield(resultGUI,'wDao'); end 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..317404274 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) +% 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 -% % - % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % @@ -39,8 +38,31 @@ % 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(sequencing, 'dynamic') + sequencing.dynamic = false; +end + +if ~isfield(sequencing,'continuousAperture') + sequencing.continuousAperture = false; +end + +if ~isfield(sequencing,'preconditioner') + sequencing.preconditioner = false; +end + +if sequencing.dynamic + 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 +75,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 +89,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 +112,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 sequencing.dynamic + 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 sequencing.dynamic && sequencing.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 +211,133 @@ apertureInfo.beam(i).bixelIndMap = bixelIndMap; apertureInfo.beam(i).posOfCornerBixel = posOfCornerBixel; apertureInfo.beam(i).MLCWindow = MLCWindow; + apertureInfo.beam(i).gantryAngle = stf(i).gantryAngle; + if sequencing.dynamic + + 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 sequencing.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 sequencing.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 % 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 = sequencing.continuousAperture; +apertureInfo.runVMAT = sequencing.dynamic; +apertureInfo.preconditioner = sequencing.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 sequencing.dynamic + % put constraints in apertureInfo.propVMAT + apertureInfo.propVMAT.constraints = sequencing.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 02841d5b0..9e545bc9a 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,numOfLevels,varargin) +% 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,8 +21,10 @@ % be created % stf: matRad steering information struct % dij: matRad's dij matrix -% numOfLevels: number of stratification levels -% visBool: toggle on/off visualization (optional) +% numOfLevels: number of intensity levels for the sequencing +% optional key-value pairs +% visBool: toggle on/off visualization (optional - default: false) +% dynamic: toggle on/off dynamic delivery (optional - default: false) % % output % resultGUI: matRad result struct containing the new dose cube @@ -41,10 +46,26 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% if visBool not set toogle off visualization -if nargin < 5 - visBool = 0; -end +matRad_cfg = MatRad_Config.instance(); + +p = inputParser(); +p.KeepUnmatched = true; +p.addRequired('resultGUI',@(x) isstruct(x)); +p.addRequired('stf',@(x) isstruct(x)); +p.addRequired('dij',@(x) isstruct(x)); +p.addRequired('numOfLevels',@(x) isnumeric(x) && isscalar(x) && x > 0); +p.addParameter('visBool',false,@(x) isscalar(x) && (islogical(x) || (isnumeric(x) && (x==0 || x==1)))); +p.addParameter('dynamic',false,@(x) isscalar(x) && (islogical(x) || (isnumeric(x) && (x==0 || x==1)))); +p.addParameter('numApertures',0,@(x) isnumeric(x) && isscalar(x) && x >= 0); +p.addParameter('continuousAperture',false,@(x) isscalar(x) && (islogical(x) || (isnumeric(x) && (x==0 || x==1)))); +p.addParameter('preconditioner',false,@(x) isscalar(x) && (islogical(x) || (isnumeric(x) && (x==0 || x==1)))); +p.parse(resultGUI,stf,dij,numOfLevels,varargin{:}); + +visBool = p.Results.visBool; +dynamic = p.Results.dynamic; +numApertures = p.Results.numApertures; +continuousAperture = p.Results.continuousAperture; +preconditioner = p.Results.preconditioner; numOfBeams = numel(stf); @@ -54,10 +75,14 @@ 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; +offset = 0; + +if isfield(resultGUI,'scaleFacRx_FMO') + resultGUI.wUnsequenced = resultGUI.wUnsequenced/resultGUI.scaleFacRx_FMO; +end if ~isfield(resultGUI,'wUnsequenced') wUnsequenced = resultGUI.w; @@ -65,10 +90,32 @@ wUnsequenced = resultGUI.wUnsequenced; end +if ~isfield(dij,'weightToMU') + dij.weightToMU = 1; +end + for i = 1:numOfBeams - numOfRaysPerBeam = stf(i).numOfRays; + if dynamic + + 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; + %TODO: does this make sense to discard apertures if VMAT isn't run? + numToKeep = numApertures; + end + % get relevant weights for current beam wOfCurrBeams = wUnsequenced(1+offset:numOfRaysPerBeam+offset).* ones(size(stf(i).ray,2),1);%REVIEW OFFSET @@ -103,85 +150,184 @@ %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; + % Gaussian fluence filtering - TODO: why? + sigma = 1; + kernel = exp(-((-2:2).^2)/(2*sigma^2)); + kernel = kernel / sum(kernel); - 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 + % Apply to each row + temp = zeros(size(fluenceMx)); + for row = 1:dimOfFluenceMxZ + temp(row,:) = conv(fluenceMx(row,:), kernel, 'same'); 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); + fluenceMx = temp; + + %allow for possibility to repeat sequencing with higher number of + %levels if number of apertures is lower than required + notFinished = true; if sum(wOfCurrBeams)>0 - %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 + 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; sequencing.beam(i).shapes = shapes(:,:,1:k); sequencing.beam(i).shapesWeight = shapesWeight(1:k)/numOfLevels*calFac; sequencing.beam(i).bixelIx = 1+offset:numOfRaysPerBeam+offset; sequencing.beam(i).fluence = D_0; - sequencing.beam(i).sum = zeros(dimOfFluenceMxZ,dimOfFluenceMxX); - - for j = 1:k - sequencing.beam(i).sum = sequencing.beam(i).sum+sequencing.beam(i).shapes(:,:,j)*sequencing.beam(i).shapesWeight(j); - end - else sequencing.beam(i).numOfShapes = 1; sequencing.beam(i).shapes = zeros(dimOfFluenceMxZ,dimOfFluenceMxX); sequencing.beam(i).shapesWeight = zeros(dimOfFluenceMxZ,dimOfFluenceMxX); sequencing.beam(i).bixelIx = 1+offset:numOfRaysPerBeam+offset; sequencing.beam(i).fluence = zeros(dimOfFluenceMxZ,dimOfFluenceMxX); - sequencing.beam(i).sum = zeros(dimOfFluenceMxZ,dimOfFluenceMxX); end + + if numToKeep ~= 0 + sequencing.beam(i) = matRad_discardApertures(sequencing.beam(i),numToKeep); + end - if numOfRaysPerBeam >1 - sequencing.w(1+offset:numOfRaysPerBeam+offset,1) = sequencing.beam(i).sum(indInFluenceMx); - else - sequencing.w(1+offset:numOfRaysPerBeam+offset,1) = wOfCurrBeams(1); + sequencing.beam(i).sum = zeros(dimOfFluenceMxZ,dimOfFluenceMxX); + 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 + + if ~dynamic + if numOfRaysPerBeam > 1 + sequencing.w(1+offset:numOfRaysPerBeam+offset,1) = sequencing.beam(i).sum(indInFluenceMx); + else + sequencing.w(1+offset:numOfRaysPerBeam+offset,1) = wOfCurrBeams(1); + end + end + offset = offset + numOfRaysPerBeam; +end + +sequencing.dynamic = dynamic; +sequencing.continuousAperture = continuousAperture; +sequencing.preconditioner = preconditioner; + +machineName = unique({stf.machine}); +radiationMode = unique({stf.radiationMode}); + +if numel(machineName) > 1 || numel(radiationMode) > 1 + matRad_cfg.dispError('Mixed Sequencing currently not supported for Siochi Leaf Sequencer'); +end + +machine = load([radiationMode{1} '_' machineName{1}]); +if ~isfield(machine, 'constraints') + sequencing.constraints = struct( ... + 'gantryRotationSpeed', [0 6], ... %degree/s + 'leafSpeed', [0 60], ... %mm/s + 'monitorUnitRate', [1.25 10]); %MU/s +else + sequencing.constraints = machine.constraints; +end + +if ~isfield(dij,'weightToMU') + dij.weightToMU = 100; + matRad_cfg.dispWarning('No weight to MU scaling factor defined in dij. Assuming %.1f.',dij.weightToMU); +end +if dynamic + + % do arc sequencing + sequencing.beam = matRad_arcSequencing(sequencing,stf,dij.weightToMU); + + % carry variables + sequencing.weightToMU = dij.weightToMU; + + % get apertureInfo + resultGUI.apertureInfo = matRad_sequencing2ApertureInfo(sequencing,stf); + + %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); + + resultGUI.apertureInfo = matRad_OptimizationProblemDAO.matRad_daoVec2ApertureInfo(resultGUI.apertureInfo,resultGUI.apertureInfo.apertureVector); +end + +if preconditioner + % calculate preconditioning factors for the apertures + resultGUI.apertureInfo = matRad_preconditionFactors(resultGUI.apertureInfo); end resultGUI.w = sequencing.w; resultGUI.wSequenced = sequencing.w; resultGUI.sequencing = sequencing; -resultGUI.apertureInfo = matRad_sequencing2ApertureInfo(sequencing,stf); +%TODO: could we use calcCubes here? What about scenarios? doseSequencedDoseGrid = reshape(dij.physicalDose{1} * sequencing.w,dij.doseGrid.dimensions); % interpolate to ct grid for visualiation & analysis resultGUI.physicalDose = matRad_interp3(dij.doseGrid.x,dij.doseGrid.y',dij.doseGrid.z, ... @@ -205,7 +351,9 @@ for i = minX:maxX maxTop = -1; - TnG = 1; + + TnG = 0; %FOR NOW + for j = minZ:maxZ if i == minX bases(j,i) = 1; @@ -354,11 +502,9 @@ imagesc(D_k,'parent',seqSubPlots(2)); set(seqSubPlots(2),'CLim',[0 numOfLevels],'YDir','normal'); title(seqSubPlots(2),['k = ' num2str(k)]); - colorbar - drawnow - - axis tight - drawnow + colorbar; + axis tight; + drawnow; end end diff --git a/matRad/sequencing/matRad_xiaLeafSequencing.m b/matRad/sequencing/matRad_xiaLeafSequencing.m index 052c918c6..199039ac0 100644 --- a/matRad/sequencing/matRad_xiaLeafSequencing.m +++ b/matRad/sequencing/matRad_xiaLeafSequencing.m @@ -1,19 +1,21 @@ -function resultGUI = matRad_xiaLeafSequencing(resultGUI,stf,dij,numOfLevels,visBool) +function resultGUI = matRad_xiaLeafSequencing(resultGUI,stf,dij,numOfLevels,varargin) % 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,numOfLevels,Name,Value,...) % % 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 -% visBool: toggle on/off visualization (optional) +% numOfLevels: number of intensity levels for the sequencing +% optional key-value pairs +% visBool: toggle on/off visualization (optional - default: false) +% dynamic: toggle on/off dynamic delivery (optional - default: false) % % output % resultGUI: matRad result struct containing the new dose cube as well as @@ -36,11 +38,40 @@ % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % if visBool not set toogle off visualization -if nargin < 5 - visBool = 0; +matRad_cfg = MatRad_Config.instance(); + +p = inputParser(); +p.KeepUnmatched = true; +p.addRequired('resultGUI',@(x) isstruct(x)); +p.addRequired('stf',@(x) isstruct(x)); +p.addRequired('dij',@(x) isstruct(x)); +p.addRequired('numOfLevels',@(x) isnumeric(x) && isscalar(x) && x > 0); +p.addParameter('visBool',false,@(x) isscalar(x) && (islogical(x) || (isnumeric(x) && (x==0 || x==1)))); +p.addParameter('dynamic',false,@(x) isscalar(x) && (islogical(x) || (isnumeric(x) && (x==0 || x==1)))); +p.addParameter('continuousAperture',false,@(x) isscalar(x) && (islogical(x) || (isnumeric(x) && (x==0 || x==1)))); +p.addParameter('preconditioner',false,@(x) isscalar(x) && (islogical(x) || (isnumeric(x) && (x==0 || x==1)))); +p.parse(resultGUI,stf,dij,numOfLevels,varargin{:}); + +numOfLevels = p.Results.numOfLevels; +visBool = p.Results.visBool; +dynamic = p.Results.dynamic; +continuousAperture = p.Results.continuousAperture; +preconditioner = p.Results.preconditioner; + +if dynamic + matRad_cfg.dispWarning(['The Engel leaf sequencing implementation is not designed for dynamic delivery. ', ... + 'Using these sequences for VMAT / other dynamic delivery may fail or yield non-deliverable plans.']); +end +if continuousAperture + matRad_cfg.dispWarning(['The Engel leaf sequencing implementation is not designed for continuous aperture computation. ', ... + 'Using these sequences for continuous aperture delivery may fail or yield non-deliverable plans.']); end -mode = 'rl'; % sliding window (sw) or reducing level (rl) +if dynamic + mode = 'sw'; % sliding window +else + mode = 'rl'; % reducing level +end numOfBeams = numel(stf); @@ -255,19 +286,79 @@ sequencing.beam(i).bixelIx = 1+offset:numOfRaysPerBeam+offset; sequencing.beam(i).fluence = zeros(dimOfFluenceMxZ,dimOfFluenceMxX); end - - + sequencing.w(1+offset:numOfRaysPerBeam+offset,1) = D_0(indInFluenceMx)/numOfLevels*calFac; offset = offset + numOfRaysPerBeam; end +sequencing.dynamic = dynamic; +sequencing.continuousAperture = continuousAperture; +sequencing.preconditioner = preconditioner; + +machineName = unique({stf.machine}); +radiationMode = unique({stf.radiationMode}); + +if numel(machineName) > 1 || numel(radiationMode) > 1 + matRad_cfg.dispError('Mixed Sequencing currently not supported for Siochi Leaf Sequencer'); +end + +machine = load([radiationMode{1} '_' machineName{1}]); +if ~isfield(machine, 'constraints') + sequencing.constraints = struct( ... + 'gantryRotationSpeed', [0 6], ... %degree/s + 'leafSpeed', [0 60], ... %mm/s + 'monitorUnitRate', [1.25 10]); %MU/s +else + sequencing.constraints = machine.constraints; +end + +if ~isfield(dij,'weightToMU') + dij.weightToMU = 100; + matRad_cfg.dispWarning('No weight to MU scaling factor defined in dij. Assuming %.1f.',dij.weightToMU); +end + +if dynamic + + % do arc sequencing + sequencing.beam = matRad_arcSequencing(sequencing,stf,dij.weightToMU); + + % carry variables + sequencing.weightToMU = dij.weightToMU; + + % get apertureInfo + resultGUI.apertureInfo = matRad_sequencing2ApertureInfo(sequencing,stf); + + %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); + + resultGUI.apertureInfo = matRad_OptimizationProblemDAO.matRad_daoVec2ApertureInfo(resultGUI.apertureInfo,resultGUI.apertureInfo.apertureVector); +end + +if preconditioner + % calculate preconditioning factors for the apertures + 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 diff --git a/matRad/steering/matRad_StfGeneratorBase.m b/matRad/steering/matRad_StfGeneratorBase.m index ab011a955..0df01c4db 100644 --- a/matRad/steering/matRad_StfGeneratorBase.m +++ b/matRad/steering/matRad_StfGeneratorBase.m @@ -185,7 +185,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..dbbd2c65a --- /dev/null +++ b/matRad/steering/matRad_StfGeneratorPhotonVMAT.m @@ -0,0 +1,553 @@ +classdef matRad_StfGeneratorPhotonVMAT < matRad_StfGeneratorPhotonRayBixelAbstract + % matRad_StfGeneratorPhotonVMAT: STF generator for photon VMAT plans. + % + % gantryAngles (inherited) are interpreted as arc anchor points: the + % first and last angle define the arc start/finish; any intermediate + % angles are waypoints the arc must pass through. Two anchor points + % suffice for a simple arc, which maps cleanly to a DICOM arc export. + % + % To define multiple arcs in the future, set arcIndex so that anchors + % belonging to the same arc share the same index value (default: 1). + % + % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + % + % Copyright 2026 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/LICENSE.md. 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. + % + % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + properties (Constant) + name = 'Photon VMAT stf Generator' + shortName = 'PhotonVMAT' + possibleRadiationModes = {'photons'} + end + + properties + % Arc membership index for each anchor angle. + % Scalar 1 (default) means all anchors belong to arc 1. + % Set to e.g. [1 1 2 2] to define two separate arcs. + arcIndex = 1 + + % Maximum angular spacing between consecutive dose-calc beams [deg] + maxGantryAngleSpacing = 4 + % Maximum angular spacing between consecutive DAO control points [deg] + maxDAOGantryAngleSpacing = 8 + % Maximum angular spacing between consecutive FMO control points [deg] + maxFMOGantryAngleSpacing = 32 + + continuousAperture = false + end + + properties (Access = protected) + % Internal computed angle arrays (populated by setupArcAngles). + % These represent the full set of interpolated angles used during + % STF generation and are not intended for direct user access. + arcGantryAngles % fine dose-calc angles + arcCouchAngles % couch angle for each fine dose-calc angle + arcDAOGantryAngles % direct aperture optimisation control points + arcFMOGantryAngles % fluence map optimisation control points + arcStartAngle % arc boundary start (first anchor per arc) + arcFinishAngle % arc boundary finish (last anchor per arc) + + % Saved user-specified anchor state, stored during initialize() so + % that generateSourceGeometry() can restore it afterwards. + savedAnchorGantryAngles + savedAnchorCouchAngles + savedIsoCenter + end + + methods + + function this = matRad_StfGeneratorPhotonVMAT(pln) + if nargin < 1 + pln = []; + end + this@matRad_StfGeneratorPhotonRayBixelAbstract(pln); + if isempty(this.radiationMode) + this.radiationMode = 'photons'; + end + end + + function setDefaults(this) + this.setDefaults@matRad_StfGeneratorPhotonRayBixelAbstract(); + % Default to a full 360 deg arc defined by two anchor points. + this.gantryAngles = [-180, 180]; + this.couchAngles = [0, 0]; + end + + end + + methods (Access = protected) + + function initialize(this) + % Override to expand isoCenter and swap in fine arc angles + % before the base class runs, so that the base class sees the + % correct beam count when it replicates the isoCenter. + + % Compute fine/DAO/FMO angles from the anchor points. + this.setupArcAngles(); + + nAnchors = numel(this.gantryAngles); % current anchor count + nFine = numel(this.arcGantryAngles); % computed fine angle count + + % Save anchor state for restoration at end of generateSourceGeometry. + this.savedAnchorGantryAngles = this.gantryAngles; + this.savedAnchorCouchAngles = this.couchAngles; + this.savedIsoCenter = this.isoCenter; + + % Expand isoCenter to [nFine x 3] before the base class sees it. + % Accepted user inputs (mirroring IMRT conventions): + % [1 x 3] - one isoCenter for the whole arc (same as IMRT single-iso) + % [nAnchors x 3] - one isoCenter per anchor point + % Both are expanded here; the base class then only validates the size. + if ~isempty(this.isoCenter) + if size(this.isoCenter, 1) == 1 + % Single isoCenter: replicate for all fine angles. + this.isoCenter = repmat(this.isoCenter, nFine, 1); + elseif size(this.isoCenter, 1) == nAnchors + % One per anchor: assign each fine angle the isoCenter of + % its nearest anchor (by gantry angle). + isoFull = zeros(nFine, 3); + for k = 1:nFine + [~, ia] = min(abs(this.gantryAngles - this.arcGantryAngles(k))); + isoFull(k, :) = this.isoCenter(ia, :); + end + this.isoCenter = isoFull; + end + % If size is already [nFine x 3] or something else unexpected, + % leave it untouched and let the base class validate/warn. + end + + % Swap gantryAngles/couchAngles to fine grid so the base class + % replicates/validates isoCenter against the correct beam count. + this.lockAngleUpdate = true; + this.gantryAngles = this.arcGantryAngles; + this.couchAngles = this.arcCouchAngles; + this.lockAngleUpdate = false; + + % Base class initialize: loads machine, validates/computes isoCenter, + % builds patient geometry axes. Fine angles are active here. + this.initialize@matRad_StfGeneratorPhotonRayBixelAbstract(); + end + + function pbMargin = getPbMargin(this) + pbMargin = this.bixelWidth; + end + + function setupArcAngles(this) + % Compute internal fine/DAO/FMO angle arrays from the user- + % specified anchor points (this.gantryAngles) and arc grouping + % (this.arcIndex). Results are stored in protected properties. + % + % For each arc, the first and last anchor define the arc extent + % (startingAngle / finishingAngle). Intermediate anchors are + % currently recorded but not yet used to subdivide the spacing + % calculation (TODO: waypoint support). + + anchorGantry = this.gantryAngles; + anchorCouch = this.couchAngles; + + % Broadcast scalar arcIndex to a per-anchor vector + if isscalar(this.arcIndex) + arcIdx = this.arcIndex * ones(1, numel(anchorGantry)); + else + arcIdx = this.arcIndex; + end + + arcIds = unique(arcIdx, 'stable'); + + allGantryAngles = []; + allCouchAngles = []; + allDAOAngles = []; + allFMOAngles = []; + + for a = 1:numel(arcIds) + mask = arcIdx == arcIds(a); + anchors = anchorGantry(mask); + couch = anchorCouch(mask); + + startAngle = anchors(1); + finishAngle = anchors(end); + couchVal = couch(1); % TODO: only uniform couch angle per arc + + angularRange = abs(finishAngle - startAngle); + + if this.continuousAperture + % In continuous mode the gantry rotates between dose + % positions; first/last beams are centred half a + % spacing inside the arc boundaries. + numGantryAngles = ceil(angularRange / this.maxGantryAngleSpacing); + gantryAngleSpacing = angularRange / numGantryAngles; + + numDAOGantryAngles = ceil((numGantryAngles - 1) * gantryAngleSpacing / this.maxDAOGantryAngleSpacing) + 1; + % Align numGantryAngles so DAO angles land exactly on fine angles + numGantryAngles = (numDAOGantryAngles - 1) * ceil((numGantryAngles - 1) / (numDAOGantryAngles - 1)) + 1; + gantryAngleSpacing = angularRange / numGantryAngles; + DAOGantryAngleSpacing = (angularRange - gantryAngleSpacing) / (numDAOGantryAngles - 1); + + firstGantryAngle = startAngle + gantryAngleSpacing / 2; + lastGantryAngle = finishAngle - gantryAngleSpacing / 2; + else + % Step-and-shoot: first/last beams sit at the arc boundaries. + numDAOGantryAngles = ceil(angularRange / this.maxDAOGantryAngleSpacing); + DAOGantryAngleSpacing = angularRange / numDAOGantryAngles; + numGantryAngles = ceil(numDAOGantryAngles * DAOGantryAngleSpacing / this.maxGantryAngleSpacing); + gantryAngleSpacing = angularRange / numGantryAngles; + + firstGantryAngle = startAngle; + lastGantryAngle = finishAngle; + end + + % FMO spacing must be an odd integer multiple of the DAO spacing + numApertures = floor(this.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); + + arcAngles = firstGantryAngle:gantryAngleSpacing:lastGantryAngle; + daoAngles = firstGantryAngle:DAOGantryAngleSpacing:lastGantryAngle; + fmoAngles = firstFMOGantryAngle:FMOGantryAngleSpacing:lastFMOGantryAngle; + + allGantryAngles = [allGantryAngles, arcAngles]; + allCouchAngles = [allCouchAngles, couchVal * ones(1, numel(arcAngles))]; + allDAOAngles = [allDAOAngles, daoAngles]; + allFMOAngles = [allFMOAngles, fmoAngles]; + end + + this.arcGantryAngles = allGantryAngles; + this.arcCouchAngles = allCouchAngles; + this.arcDAOGantryAngles = allDAOAngles; + this.arcFMOGantryAngles = allFMOAngles; + + % Store arc extent boundaries for border calculations. + % TODO: per-arc tracking when multi-arc is supported. + this.arcStartAngle = anchorGantry(arcIdx == arcIds(1)); + this.arcStartAngle = this.arcStartAngle(1); + this.arcFinishAngle = anchorGantry(arcIdx == arcIds(end)); + this.arcFinishAngle = this.arcFinishAngle(end); + end + + function stf = generateSourceGeometry(this) + % Fine arc angles and isoCenter are already expanded, and + % gantryAngles/couchAngles already swapped to the fine grid by + % initialize(). Call parent to build the per-beam stf entries. + stf = this.generateSourceGeometry@matRad_StfGeneratorPhotonRayBixelAbstract(); + + matRad_cfg = MatRad_Config.instance(); + matRad_cfg.dispInfo('Apply VMAT configuration to stf...\n'); + + %% Build master ray set: union of all per-beam ray positions, + % gap-filled to ensure a contiguous beam aperture. + 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 + + 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); + nNew = (x_max - x_min) / this.bixelWidth + 1; + x = [x; (x_min:this.bixelWidth:x_max)']; + y = [y; zeros(nNew, 1)]; + z = [z; uniZ(j) * ones(nNew, 1)]; + end + + SAD = this.machine.meta.SAD; + masterRayPosBEV = unique([x, y, z], 'rows'); + masterTargetPointBEV = [2 * masterRayPosBEV(:, 1), SAD * ones(size(masterRayPosBEV, 1), 1), 2 * masterRayPosBEV(:, 3)]; + + %% VMAT post-processing pass 1: assign propVMAT fields per beam + matRad_cfg.dispInfo('VMAT stf beam type and geometry setup... '); + stf = this.prepareArcs(stf, masterRayPosBEV, masterTargetPointBEV); + + %% VMAT post-processing pass 2: derived quantities that require + % the complete propVMAT data from pass 1 + matRad_cfg.dispInfo('VMAT stf cleanup... '); + + stf = this.finalizeArcs(stf); + + % Restore object state to the user-specified anchor configuration. + this.lockAngleUpdate = true; + this.gantryAngles = this.savedAnchorGantryAngles; + this.couchAngles = this.savedAnchorCouchAngles; + this.lockAngleUpdate = false; + this.isoCenter = this.savedIsoCenter; + end + + function stf = prepareArcs(this, stf, masterRayPosBEV, masterTargetPointBEV) + nBeams = numel(stf); + numDAO = 1; + DAODoseAngleBorders = zeros(2 * numel(this.arcDAOGantryAngles), 1); + offset = 1; + timeFacIndOffset = 1; + SAD = this.machine.meta.SAD; + + for i = 1:nBeams + + %% Determine FMO parent beam + [~, stf(i).propVMAT.beamParentFMOIndex] = min(abs(this.arcFMOGantryAngles - stf(i).gantryAngle)); + stf(i).propVMAT.beamParentGantryAngle = this.arcFMOGantryAngles(stf(i).propVMAT.beamParentFMOIndex); + stf(i).propVMAT.beamParentIndex = find(abs([stf.gantryAngle] - stf(i).propVMAT.beamParentGantryAngle) < 1e-6); + + stf(i).propVMAT.FMOBeam = any(abs(this.arcFMOGantryAngles - stf(i).gantryAngle) < 1e-6); + stf(i).propVMAT.DAOBeam = any(abs(this.arcDAOGantryAngles - stf(i).gantryAngle) < 1e-6); + + %% Dose angle borders: angular range attributed to this beam + if i == 1 + stf(i).propVMAT.doseAngleBorders = [this.arcStartAngle, (stf(2).gantryAngle + stf(i).gantryAngle) / 2]; + elseif i == nBeams + stf(i).propVMAT.doseAngleBorders = [(stf(i - 1).gantryAngle + stf(i).gantryAngle) / 2, this.arcFinishAngle]; + else + stf(i).propVMAT.doseAngleBorders = ([stf(i - 1).gantryAngle, stf(i + 1).gantryAngle] + stf(i).gantryAngle) / 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); + + if stf(i).propVMAT.DAOBeam + %% DAO beam: record dose angle borders and compute DAO influence range + DAODoseAngleBorders(offset:offset + 1) = stf(i).propVMAT.doseAngleBorders; + offset = offset + 2; + + % Register as child of its FMO parent + parent = stf(i).propVMAT.beamParentIndex; + if ~isfield(stf(parent).propVMAT, 'beamChildrenGantryAngles') || isempty(stf(parent).propVMAT.beamChildrenGantryAngles) + stf(parent).propVMAT.numOfBeamChildren = 0; + stf(parent).propVMAT.beamChildrenGantryAngles = nan(1000, 1); + stf(parent).propVMAT.beamChildrenIndex = nan(1000, 1); + end + n = stf(parent).propVMAT.numOfBeamChildren + 1; + stf(parent).propVMAT.numOfBeamChildren = n; + stf(parent).propVMAT.beamChildrenGantryAngles(n) = stf(i).gantryAngle; + stf(parent).propVMAT.beamChildrenIndex(n) = i; + + % DAO influence angle borders + DAOIndex = find(abs(this.arcDAOGantryAngles - stf(i).gantryAngle) < 1e-8); + + if DAOIndex == 1 + stf(i).propVMAT.DAOAngleBorders = [this.arcStartAngle, ... + (this.arcDAOGantryAngles(DAOIndex + 1) + this.arcDAOGantryAngles(DAOIndex)) / 2]; + lastDAOIndex = i; + nextDAOIndex = find(abs([stf.gantryAngle] - this.arcDAOGantryAngles(DAOIndex + 1)) < 1e-8); + + elseif DAOIndex == numel(this.arcDAOGantryAngles) + stf(i).propVMAT.DAOAngleBorders = [ ... + (this.arcDAOGantryAngles(DAOIndex - 1) + this.arcDAOGantryAngles(DAOIndex)) / 2, ... + this.arcFinishAngle]; + lastDAOIndex = find(abs([stf.gantryAngle] - this.arcDAOGantryAngles(DAOIndex - 1)) < 1e-8); + nextDAOIndex = i; + + else + stf(i).propVMAT.DAOAngleBorders = ... + ([this.arcDAOGantryAngles(DAOIndex - 1), this.arcDAOGantryAngles(DAOIndex + 1)] + this.arcDAOGantryAngles(DAOIndex)) / 2; + lastDAOIndex = i; + nextDAOIndex = find(abs([stf.gantryAngle] - this.arcDAOGantryAngles(DAOIndex + 1)) < 1e-8); + end + + stf(i).propVMAT.lastDAOIndex = lastDAOIndex; + stf(i).propVMAT.nextDAOIndex = nextDAOIndex; + 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); + + % Time factor: fraction of DAO sector time covered by this dose sector + stf(i).propVMAT.timeFacCurr = stf(i).propVMAT.doseAngleBordersDiff / stf(i).propVMAT.DAOAngleBordersDiff; + + if this.continuousAperture + 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; + + delInd = stf(i).propVMAT.timeFac == 0; + stf(i).propVMAT.timeFacInd = [timeFacIndOffset - 1, timeFacIndOffset, timeFacIndOffset + 1]; + stf(i).propVMAT.timeFacInd(delInd) = 0; + + if delInd(3) + timeFacIndOffset = timeFacIndOffset + 1; + else + timeFacIndOffset = timeFacIndOffset + 2; + end + else + 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 + %% Non-DAO beam: register as sub-child of FMO parent and record interpolation fraction + parent = stf(i).propVMAT.beamParentIndex; + if ~isfield(stf(parent).propVMAT, 'beamSubChildrenGantryAngles') || isempty(stf(parent).propVMAT.beamSubChildrenGantryAngles) + stf(parent).propVMAT.numOfBeamSubChildren = 0; + stf(parent).propVMAT.beamSubChildrenGantryAngles = nan(1000, 1); + stf(parent).propVMAT.beamSubChildrenIndex = nan(1000, 1); + end + n = stf(parent).propVMAT.numOfBeamSubChildren + 1; + stf(parent).propVMAT.numOfBeamSubChildren = n; + stf(parent).propVMAT.beamSubChildrenGantryAngles(n) = stf(i).gantryAngle; + stf(parent).propVMAT.beamSubChildrenIndex(n) = i; + + stf(i).propVMAT.fracFromLastDAO = (stf(nextDAOIndex).gantryAngle - stf(i).gantryAngle) / ... + (stf(nextDAOIndex).gantryAngle - stf(lastDAOIndex).gantryAngle); + stf(i).propVMAT.lastDAOIndex = lastDAOIndex; + stf(i).propVMAT.nextDAOIndex = nextDAOIndex; + end + + %% FMO beam: compute FMO influence angle borders + if stf(i).propVMAT.FMOBeam + FMOIndex = find(abs(this.arcFMOGantryAngles - stf(i).gantryAngle) < 1e-8); + + if FMOIndex == 1 + stf(i).propVMAT.FMOAngleBorders = [this.arcStartAngle, ... + (this.arcFMOGantryAngles(FMOIndex + 1) + this.arcFMOGantryAngles(FMOIndex)) / 2]; + elseif FMOIndex == numel(this.arcFMOGantryAngles) + stf(i).propVMAT.FMOAngleBorders = [ ... + (this.arcFMOGantryAngles(FMOIndex - 1) + this.arcFMOGantryAngles(FMOIndex)) / 2, ... + this.arcFinishAngle]; + else + stf(i).propVMAT.FMOAngleBorders = ... + ([this.arcFMOGantryAngles(FMOIndex - 1), this.arcFMOGantryAngles(FMOIndex + 1)] + this.arcFMOGantryAngles(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 + + %% Assign union ray set to this beam and apply rotation + stf(i).numOfRays = size(masterRayPosBEV, 1); + stf(i).numOfBixelsPerRay = ones(1, stf(i).numOfRays); + stf(i).totalNumOfBixels = stf(i).numOfRays; + + stf(i).sourcePoint_bev = [0, -SAD, 0]; + rotMat_vectors_T = transpose(matRad_getRotationMatrix(stf(i).gantryAngle, stf(i).couchAngle)); + stf(i).sourcePoint = stf(i).sourcePoint_bev * rotMat_vectors_T; + + 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 = masterRayPosBEV(j, :) * rotMat_vectors_T; + stf(i).ray(j).targetPoint = masterTargetPointBEV(j, :) * 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; + stf(i).ray(j).energy = this.machine.data.energy; + end + + matRad_progress(i, nBeams); + end + end + + function stf = finalizeArcs(this, stf) + nBeams = numel(stf); + for i = 1:nBeams + % Remove NaN padding from child/sub-child angle lists + if stf(i).propVMAT.FMOBeam + 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 && this.continuousAperture + stf(i).propVMAT.doseAngleDAO = ones(1, 2); + if sum(DAODoseAngleBorders == stf(i).propVMAT.doseAngleBorders(2)) > 1 + % Final dose angle is shared - count it only once + stf(i).propVMAT.doseAngleDAO(2) = 0; + end + end + + if ~stf(i).propVMAT.FMOBeam && ~stf(i).propVMAT.DAOBeam + % Leaf position interpolation fractions + lastBorder = stf(stf(i).propVMAT.lastDAOIndex).propVMAT.doseAngleBorders(2); + nextBorder = stf(stf(i).propVMAT.nextDAOIndex).propVMAT.doseAngleBorders(1); + span = nextBorder - lastBorder; + + stf(i).propVMAT.fracFromLastDAO_I = (nextBorder - stf(i).propVMAT.doseAngleBorders(1)) / span; + stf(i).propVMAT.fracFromLastDAO_F = (nextBorder - stf(i).propVMAT.doseAngleBorders(2)) / span; + stf(i).propVMAT.fracFromNextDAO_I = (stf(i).propVMAT.doseAngleBorders(1) - lastBorder) / span; + stf(i).propVMAT.fracFromNextDAO_F = (stf(i).propVMAT.doseAngleBorders(2) - lastBorder) / span; + + % Time interpolation fractions (clamped to [0, 1]) + lastDAOBorder2 = stf(stf(i).propVMAT.lastDAOIndex).propVMAT.DAOAngleBorders(2); + stf(i).propVMAT.timeFracFromLastDAO = min(max((lastDAOBorder2 - stf(i).propVMAT.doseAngleBorders(1)) / ... + stf(i).propVMAT.doseAngleBordersDiff, 0), 1); + stf(i).propVMAT.timeFracFromNextDAO = min(max((stf(i).propVMAT.doseAngleBorders(2) - lastDAOBorder2) / ... + stf(i).propVMAT.doseAngleBordersDiff, 0), 1); + end + + matRad_progress(i, nBeams); + end + end + + end + + methods (Static) + + function [available, msg] = isAvailable(pln, machine) + if nargin < 2 + machine = matRad_loadMachine(pln); + end + + [available, msg] = matRad_StfGeneratorPhotonRayBixelAbstract.isAvailable(pln, machine); + if ~available + return + else + available = false; + msg = []; + end + + try + checkBasic = isfield(machine, 'meta') && isfield(machine, 'data'); + checkModality = any(strcmp(matRad_StfGeneratorPhotonVMAT.possibleRadiationModes, machine.meta.radiationMode)) && ... + any(strcmp(matRad_StfGeneratorPhotonVMAT.possibleRadiationModes, pln.radiationMode)); + 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 fields (meta/data/radiationMode)!'; + return + end + + available = preCheck; + end + + end +end 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/helper_createTestCt.m b/test/helper_createTestCt.m new file mode 100644 index 000000000..4dd14dae0 --- /dev/null +++ b/test/helper_createTestCt.m @@ -0,0 +1,64 @@ +function [ct] = helper_createTestCt(cubeDim, resolution, varargin) +% helper_createTestCt Creates a minimal matRad ct struct for testing. +% +% call +% [ct] = helper_createTestCt() +% [ct] = helper_createTestCt(cubeDim) +% [ct] = helper_createTestCt(cubeDim, resolution) +% +% inputs +% cubeDim [nRows nCols nSlices], default [10 12 8] +% (unequal dimensions help catch x/y coordinate-swap bugs) +% resolution scalar (isotropic mm), 1x3 vector [x y z mm], or struct +% with fields .x .y .z; default struct(x=1, y=2, z=3) +% (unequal resolutions help catch axis-scaling bugs) +% Options (name-value pairs) +% 'createCoordinateArrays' (logical) if true, adds ct.x ct.y ct +% ct.z arrays in world mm coordinates; default false +% 'datatype' (string) numeric class for ct.cubeHU; default 'double +% 'HUvalue' (numeric scalar) value to fill ct.cubeHU with; default 0 +% +% outputs +% ct matRad ct struct with cubeDim and resolution populated + +p = inputParser; + +p.addOptional('cubeDim', [10 12 8], @(x) isnumeric(x) && isvector(x) && numel(x) == 3 && all(x > 0) && all(mod(x, 1) == 0)); +p.addOptional('resolution', [1 2 3], @(x) (isstruct(x) && all(isfield(x, {'x', 'y', 'z'}))) || ... + (isnumeric(x) && (isscalar(x) || (isvector(x) && numel(x) == 3)))); +p.addParameter('createCoordinateArrays', false, @(x) islogical(x) && isscalar(x)); +p.addParameter('datatype', 'double', @(x) ischar(x) || (isstring(x) && isscalar(x))); +p.addParameter('HUvalue', 0, @(x) isnumeric(x) && isscalar(x)); + +args = varargin; +if nargin >= 2 + args = [{resolution}, args]; +end + +if nargin >= 1 + args = [{cubeDim}, args]; +end + +p.parse(args{:}); + +ct.cubeDim = p.Results.cubeDim; + +if isstruct(p.Results.resolution) + ct.resolution = p.Results.resolution; +elseif isscalar(p.Results.resolution) + ct.resolution.x = p.Results.resolution; + ct.resolution.y = p.Results.resolution; + ct.resolution.z = p.Results.resolution; +else + ct.resolution.x = p.Results.resolution(1); + ct.resolution.y = p.Results.resolution(2); + ct.resolution.z = p.Results.resolution(3); +end + +if p.Results.createCoordinateArrays + ct = matRad_getWorldAxes(ct); +end + +ct.cubeHU{1} = p.Results.HUvalue * ones(ct.cubeDim, p.Results.datatype); + +end diff --git a/test/phantoms/test_VOIBox.m b/test/phantoms/test_VOIBox.m new file mode 100644 index 000000000..e63450854 --- /dev/null +++ b/test/phantoms/test_VOIBox.m @@ -0,0 +1,209 @@ +function test_suite = test_VOIBox +% The output should always be test_suite, and the function name the same as +% your file name + +test_functions = localfunctions(); +initTestSuite; + +%% Constructor Tests + +function test_constructorDefaults +box = matRad_PhantomVOIBox('MyBox', 'OAR', [4 6 8]); +assertEqual(box.name, 'MyBox'); +assertEqual(box.type, 'OAR'); +assertEqual(box.boxDimensions, [4 6 8]); +assertEqual(box.coordType, 'voxel'); +assertEqual(box.offset, [0 0 0]); +assertEqual(box.HU, 0); + +function test_constructorCustomParams +box = matRad_PhantomVOIBox('MyBox', 'OAR', [4 6 8], ... + 'coordType', 'mm', 'offset', [1 2 3], 'HU', 200); +assertEqual(box.coordType, 'mm'); +assertEqual(box.offset, [1 2 3]); +assertEqual(box.HU, 200); + +function test_constructorInvalidInputs +% Non-positive dimensions +assertExceptionThrown(@() matRad_PhantomVOIBox('MyBox', 'OAR', [-1 2 3])); +assertExceptionThrown(@() matRad_PhantomVOIBox('MyBox', 'OAR', [0 2 3])); +% Wrong number of elements +assertExceptionThrown(@() matRad_PhantomVOIBox('MyBox', 'OAR', [4 6])); +assertExceptionThrown(@() matRad_PhantomVOIBox('MyBox', 'OAR', [4 6 8 10])); +% Non-numeric +assertExceptionThrown(@() matRad_PhantomVOIBox('MyBox', 'OAR', 'big')); +% Invalid coordType +assertExceptionThrown(@() matRad_PhantomVOIBox('MyBox', 'OAR', [4 6 8], 'coordType', 'invalid')); + +%% set-method Validation + +function test_setMethodsValidation +box = matRad_PhantomVOIBox('MyBox', 'OAR', [4 6 8]); +% coordType: valid update and rejection +box.coordType = 'mm'; +assertEqual(box.coordType, 'mm'); +assertExceptionThrown(@() helper_assignmentTest(box, 'coordType', 'cube')); +% boxDimensions: rejection of bad values +assertExceptionThrown(@() helper_assignmentTest(box, 'boxDimensions', [-1 2 3])); +assertExceptionThrown(@() helper_assignmentTest(box, 'boxDimensions', [0 2 3])); +assertExceptionThrown(@() helper_assignmentTest(box, 'boxDimensions', [4 6])); + +%% initializeParameters - CST structure + +function test_initializeParametersCstStructure +ct = helper_createTestCt(); +cst = {}; +box = matRad_PhantomVOIBox('TestBox', 'OAR', [4 6 8]); +cst = box.initializeParameters(ct, cst); +assertEqual(size(cst, 1), 1); +assertEqual(cst{1, 2}, 'TestBox'); +assertEqual(cst{1, 3}, 'OAR'); + +%% initializeParameters - voxel mode + +function test_initializeParametersVoxelCoversAll +% A box larger than the CT must be clipped to all voxels. +ct = helper_createTestCt(); +cst = {}; +box = matRad_PhantomVOIBox('TestBox', 'OAR', [1000 1000 1000]); +cst = box.initializeParameters(ct, cst); +assertEqual(numel(cst{1, 4}{1}), prod(ct.cubeDim)); + +function test_initializeParametersVoxelAllIndicesValid +ct = helper_createTestCt(); +cst = {}; +box = matRad_PhantomVOIBox('TestBox', 'OAR', [4 6 2]); +cst = box.initializeParameters(ct, cst); +idx = cst{1, 4}{1}; +assertTrue(numel(idx) > 0); +assertTrue(all(idx >= 1) && all(idx <= prod(ct.cubeDim))); + +%% initializeParameters - voxel mode axis-permutation tests +% +% Strategy: odd non-square cubeDim=[9 11 7] puts the geometric centre +% exactly at (row=5, col=6, slice=4). boxDimensions is in [i j k] order: +% dims(1) = x / col extent, dims(2) = y / row extent, dims(3) = z extent. +% +% dims=[3 1 1]: 3 wide in x (cols 5-7), 1 in y (row 5), 1 in z (slice 4). +% dims=[1 3 1]: 1 in x (col 6), 3 in y (rows 4-6), 1 in z (slice 4). +% The two resulting voxel sets are completely disjoint, so any x/y swap fails. + +function test_initializeParametersVoxelAxisPermutation +cubeDim = [9 11 7]; +ct = helper_createTestCt(cubeDim, 1); +cRow = 5; +cCol = 6; +cSlice = 4; % (cubeDim+1)/2 + +% --- 1x1x1 box: exactly the centre voxel ---------------------------- +cst = {}; +box = matRad_PhantomVOIBox('B', 'OAR', [1 1 1]); +cst = box.initializeParameters(ct, cst); +assertEqual(cst{1, 4}{1}, sub2ind(cubeDim, cRow, cCol, cSlice)); + +% --- [3 1 1]: 3 voxels along x / col -------------------------------- +% Expected: row=5, cols 5-7, slice=4 +cst = {}; +box = matRad_PhantomVOIBox('B', 'OAR', [3 1 1]); +cst = box.initializeParameters(ct, cst); +expected = [sub2ind(cubeDim, cRow, cCol - 1, cSlice), ... + sub2ind(cubeDim, cRow, cCol, cSlice), ... + sub2ind(cubeDim, cRow, cCol + 1, cSlice)]; +assertEqual(cst{1, 4}{1}, sort(expected)'); + +% --- [1 3 1]: 3 voxels along y / row -------------------------------- +% Expected: rows 4-6, col=6, slice=4 +cst = {}; +box = matRad_PhantomVOIBox('B', 'OAR', [1 3 1]); +cst = box.initializeParameters(ct, cst); +expected = [sub2ind(cubeDim, cRow - 1, cCol, cSlice), ... + sub2ind(cubeDim, cRow, cCol, cSlice), ... + sub2ind(cubeDim, cRow + 1, cCol, cSlice)]; +assertEqual(cst{1, 4}{1}, sort(expected)'); + +function test_initializeParametersVoxelOffsetAxis +% offset=[1 0 0] is in [i j k]: x/col direction -> col advances by 1. +% offset=[0 1 0] is y/row direction -> row advances by 1. +% A 1x1x1 box isolates a single voxel so the shift is unambiguous. +cubeDim = [9 11 7]; +ct = helper_createTestCt(cubeDim, 1); +cRow = 5; +cCol = 6; +cSlice = 4; + +% Offset in x (col) +cst = {}; +box = matRad_PhantomVOIBox('B', 'OAR', [1 1 1], 'offset', [1 0 0]); +cst = box.initializeParameters(ct, cst); +assertEqual(cst{1, 4}{1}, sub2ind(cubeDim, cRow, cCol + 1, cSlice)); + +% Offset in y (row) +cst = {}; +box = matRad_PhantomVOIBox('B', 'OAR', [1 1 1], 'offset', [0 1 0]); +cst = box.initializeParameters(ct, cst); +assertEqual(cst{1, 4}{1}, sub2ind(cubeDim, cRow + 1, cCol, cSlice)); + +%% initializeParameters - mm mode axis-permutation tests +% +% Same cubeDim=[9 11 7] with anisotropic resolution res={x=2,y=3,z=5}. +% The geometric centre maps to world coords (ct.x(6), ct.y(5), ct.z(4)). +% +% dims=[4 0.1 0.1]: extends +-2 mm in x (one full voxel each side) -> cols 5-7. +% dims=[0.1 6 0.1]: extends +-3 mm in y (one full voxel each side) -> rows 4-6. +% Any axis swap produces the opposite column/row expansion and fails. + +function test_initializeParametersMmAxisPermutation +cubeDim = [9 11 7]; +res = struct('x', 2, 'y', 3, 'z', 5); +ct = helper_createTestCt(cubeDim, res); +cRow = 5; +cCol = 6; +cSlice = 4; + +% Tiny box: only the centre voxel +cst = {}; +box = matRad_PhantomVOIBox('B', 'OAR', [0.5 0.5 0.5], 'coordType', 'mm'); +cst = box.initializeParameters(ct, cst); +assertEqual(cst{1, 4}{1}, sub2ind(cubeDim, cRow, cCol, cSlice)); + +% dims=[2*res.x, narrow, narrow]: 3 columns, 1 row, 1 slice +cst = {}; +box = matRad_PhantomVOIBox('B', 'OAR', [2 * res.x 0.1 0.1], 'coordType', 'mm'); +cst = box.initializeParameters(ct, cst); +expected = [sub2ind(cubeDim, cRow, cCol - 1, cSlice), ... + sub2ind(cubeDim, cRow, cCol, cSlice), ... + sub2ind(cubeDim, cRow, cCol + 1, cSlice)]; +assertEqual(cst{1, 4}{1}, sort(expected)'); + +% dims=[narrow, 2*res.y, narrow]: 1 column, 3 rows, 1 slice +cst = {}; +box = matRad_PhantomVOIBox('B', 'OAR', [0.1 2 * res.y 0.1], 'coordType', 'mm'); +cst = box.initializeParameters(ct, cst); +expected = [sub2ind(cubeDim, cRow - 1, cCol, cSlice), ... + sub2ind(cubeDim, cRow, cCol, cSlice), ... + sub2ind(cubeDim, cRow + 1, cCol, cSlice)]; +assertEqual(cst{1, 4}{1}, sort(expected)'); + +function test_initializeParametersMmOffsetAxis +% A step of res.x in the [i j k] x-direction must advance the column; +% a step of res.y in the y-direction must advance the row. +cubeDim = [9 11 7]; +res = struct('x', 2, 'y', 3, 'z', 5); +ct = helper_createTestCt(cubeDim, res); +cRow = 5; +cCol = 6; +cSlice = 4; + +% Offset in x (col) +cst = {}; +box = matRad_PhantomVOIBox('B', 'OAR', [0.5 0.5 0.5], 'coordType', 'mm', ... + 'offset', [res.x 0 0]); +cst = box.initializeParameters(ct, cst); +assertEqual(cst{1, 4}{1}, sub2ind(cubeDim, cRow, cCol + 1, cSlice)); + +% Offset in y (row) +cst = {}; +box = matRad_PhantomVOIBox('B', 'OAR', [0.5 0.5 0.5], 'coordType', 'mm', ... + 'offset', [0 res.y 0]); +cst = box.initializeParameters(ct, cst); +assertEqual(cst{1, 4}{1}, sub2ind(cubeDim, cRow + 1, cCol, cSlice)); diff --git a/test/phantoms/test_VOISphere.m b/test/phantoms/test_VOISphere.m new file mode 100644 index 000000000..1af3cb242 --- /dev/null +++ b/test/phantoms/test_VOISphere.m @@ -0,0 +1,186 @@ +function test_suite = test_VOISphere +% The output should always be test_suite, and the function name the same as +% your file name + +test_functions = localfunctions(); +initTestSuite; + +%% Constructor Tests + +function test_constructorDefaults +% All default properties from a minimal construction +sphere = matRad_PhantomVOISphere('MySphere', 'OAR', 5); +assertEqual(sphere.name, 'MySphere'); +assertEqual(sphere.type, 'OAR'); +assertEqual(sphere.radius, 5); +assertEqual(sphere.coordType, 'voxel'); +assertEqual(sphere.offset, [0 0 0]); +assertEqual(sphere.HU, 0); + +function test_constructorCustomParams +% Custom name-value arguments are stored correctly +sphere = matRad_PhantomVOISphere('MySphere', 'OAR', 5, ... + 'coordType', 'mm', 'offset', [1 2 3], 'HU', 200); +assertEqual(sphere.coordType, 'mm'); +assertEqual(sphere.offset, [1 2 3]); +assertEqual(sphere.HU, 200); + +function test_constructorInvalidInputs +% Invalid radius values +assertExceptionThrown(@() matRad_PhantomVOISphere('MySphere', 'OAR', -1)); +assertExceptionThrown(@() matRad_PhantomVOISphere('MySphere', 'OAR', 0)); +assertExceptionThrown(@() matRad_PhantomVOISphere('MySphere', 'OAR', [1 2])); +assertExceptionThrown(@() matRad_PhantomVOISphere('MySphere', 'OAR', 'big')); +% Invalid coordType +assertExceptionThrown(@() matRad_PhantomVOISphere('MySphere', 'OAR', 5, 'coordType', 'invalid')); + +%% set-method Validation + +function test_setMethodsValidation +sphere = matRad_PhantomVOISphere('MySphere', 'OAR', 5); +% coordType: valid update and rejection of invalid value +sphere.coordType = 'mm'; +assertEqual(sphere.coordType, 'mm'); +assertExceptionThrown(@() helper_assignmentTest(sphere, 'coordType', 'cube')); +% radius: rejection of non-positive and non-scalar values +assertExceptionThrown(@() helper_assignmentTest(sphere, 'radius', -3)); +assertExceptionThrown(@() helper_assignmentTest(sphere, 'radius', 0)); +assertExceptionThrown(@() helper_assignmentTest(sphere, 'radius', [1 2 3])); + +%% initializeParameters Tests + +function test_initializeParametersCstStructure +% A single call must add exactly one correctly labelled CST entry +ct = helper_createTestCt(); +cst = {}; +sphere = matRad_PhantomVOISphere('TestSphere', 'OAR', 3); +cst = sphere.initializeParameters(ct, cst); +assertEqual(size(cst, 1), 1); +assertEqual(cst{1, 2}, 'TestSphere'); +assertEqual(cst{1, 3}, 'OAR'); + +function test_initializeParametersVoxelMode +ct = helper_createTestCt(); +cst = {}; +radius = 3; +sphere = matRad_PhantomVOISphere('TestSphere', 'OAR', radius); +cst = sphere.initializeParameters(ct, cst); +voxelIndices = cst{1, 4}{1}; + +% The implementation centers at (cubeDim+1)/2 in [y x z] order. +% Round to the nearest integer voxel for the membership check. +centerPoint = round((ct.cubeDim + 1) / 2); % [y x z] center voxel +centerLinIx = sub2ind(ct.cubeDim, centerPoint(1), centerPoint(2), centerPoint(3)); +assertTrue(ismember(centerLinIx, voxelIndices)); + +% Corner [1 1 1] is > 7 voxels from the center -> excluded for radius=3 +assertFalse(ismember(sub2ind(ct.cubeDim, 1, 1, 1), voxelIndices)); + +% Voxel count should be close to the continuous sphere volume (4/3)*pi*r^3 +expectedVol = (4 / 3) * pi * radius^3; +assertTrue(numel(voxelIndices) > 0.7 * expectedVol); +assertTrue(numel(voxelIndices) < 1.3 * expectedVol); + +% All returned indices must be valid linear indices for the cube +assertTrue(all(voxelIndices >= 1) && all(voxelIndices <= prod(ct.cubeDim))); + +function test_initializeParametersVoxelLargeRadiusCoversAll +% Center is at (cubeDim+1)/2 = [5.5 6.5 4.5]; the farthest corner is +% sqrt(4.5^2+5.5^2+3.5^2) = ~7.92 voxels away, so radius=9 must capture +% every voxel in the grid. +ct = helper_createTestCt(); +cst = {}; +sphere = matRad_PhantomVOISphere('TestSphere', 'OAR', 9); +cst = sphere.initializeParameters(ct, cst); +assertEqual(numel(cst{1, 4}{1}), prod(ct.cubeDim)); + +function test_initializeParametersMmMode +% A larger mm radius must yield strictly more voxels, all within bounds. +% Using non-isotropic resolution means the two radii map to clearly +% different physical volumes even across the coarsest (z=3mm) axis. +ct = helper_createTestCt(); +cst = {}; +sphereSmall = matRad_PhantomVOISphere('S1', 'OAR', 5, 'coordType', 'mm'); +sphereLarge = matRad_PhantomVOISphere('S2', 'OAR', 10, 'coordType', 'mm'); +cst1 = sphereSmall.initializeParameters(ct, cst); +cst2 = sphereLarge.initializeParameters(ct, cst); +nSmall = numel(cst1{1, 4}{1}); +nLarge = numel(cst2{1, 4}{1}); +assertTrue(nSmall > 0); +assertTrue(nLarge > nSmall); +assertTrue(all(cst1{1, 4}{1} >= 1) && all(cst1{1, 4}{1} <= prod(ct.cubeDim))); + +function test_initializeParametersLargerRadiusMoreVoxels +% Increasing the radius must strictly grow the voxel set (voxel mode) +ct = helper_createTestCt(); +cst = {}; +sphereSmall = matRad_PhantomVOISphere('Small', 'OAR', 2); +sphereLarge = matRad_PhantomVOISphere('Large', 'OAR', 4); +cst1 = sphereSmall.initializeParameters(ct, cst); +cst2 = sphereLarge.initializeParameters(ct, cst); +assertTrue(numel(cst2{1, 4}{1}) > numel(cst1{1, 4}{1})); + +%% Permutation / axis-direction tests +% +% Strategy: odd non-square cubeDim=[9 11 7] makes (cubeDim+1)/2=[5 6 4] +% land exactly on voxel (row=5, col=6, slice=4). A radius < 1 voxel +% selects only that single voxel, so an x/y axis swap produces a +% *different linear index* that assertEqual catches immediately. +% +% Offset convention: [i j k] = [x/col, y/row, z/slice]. + +function test_initializeParametersVoxelAxisPermutation +% In voxel mode an offset of [1 0 0] must advance the column (x/i), +% and [0 1 0] must advance the row (y/j). A coordinate swap would +% exchange the two, producing the wrong linear index. +cubeDim = [9 11 7]; +ct = helper_createTestCt(cubeDim, 1); +cRow = 5; +cCol = 6; +cSlice = 4; % (cubeDim+1)/2 +r = 0.6; % < 1 voxel: isolates the single centre voxel + +% No offset: centre voxel only +cst = {}; +cst = matRad_PhantomVOISphere('S', 'OAR', r).initializeParameters(ct, cst); +assertEqual(cst{1, 4}{1}, sub2ind(cubeDim, cRow, cCol, cSlice)); + +% Offset +1 in i (x / col): col must increase by 1, row unchanged +cst = {}; +cst = matRad_PhantomVOISphere('S', 'OAR', r, 'offset', [1 0 0]).initializeParameters(ct, cst); +assertEqual(cst{1, 4}{1}, sub2ind(cubeDim, cRow, cCol + 1, cSlice)); + +% Offset +1 in j (y / row): row must increase by 1, col unchanged +cst = {}; +cst = matRad_PhantomVOISphere('S', 'OAR', r, 'offset', [0 1 0]).initializeParameters(ct, cst); +assertEqual(cst{1, 4}{1}, sub2ind(cubeDim, cRow + 1, cCol, cSlice)); + +function test_initializeParametersMmAxisPermutation +% Same idea in mm mode. An offset of exactly one voxel spacing in x +% must advance the column index; one spacing in y must advance the row. +% Anisotropic resolution (x!=y) makes a coordinate swap detectable even +% if voxel counts happen to be equal. +cubeDim = [9 11 7]; +res = struct('x', 2, 'y', 3, 'z', 5); +ct = helper_createTestCt(cubeDim, res); +cRow = 5; +cCol = 6; +cSlice = 4; % (cubeDim+1)/2 +r = 0.8; % < min(res) = 2 mm: isolates centre voxel + +% No offset: centre voxel only +cst = {}; +cst = matRad_PhantomVOISphere('S', 'OAR', r, 'coordType', 'mm').initializeParameters(ct, cst); +assertEqual(cst{1, 4}{1}, sub2ind(cubeDim, cRow, cCol, cSlice)); + +% Offset one x-spacing in i (x / col): col must increase by 1 +cst = {}; +cst = matRad_PhantomVOISphere('S', 'OAR', r, 'coordType', 'mm', ... + 'offset', [res.x 0 0]).initializeParameters(ct, cst); +assertEqual(cst{1, 4}{1}, sub2ind(cubeDim, cRow, cCol + 1, cSlice)); + +% Offset one y-spacing in j (y / row): row must increase by 1 +cst = {}; +cst = matRad_PhantomVOISphere('S', 'OAR', r, 'coordType', 'mm', ... + 'offset', [0 res.y 0]).initializeParameters(ct, cst); +assertEqual(cst{1, 4}{1}, sub2ind(cubeDim, cRow + 1, cCol, cSlice)); diff --git a/test/sequencing/test_engelLeafSequencing.m b/test/sequencing/test_engelLeafSequencing.m index a07546c29..2eb25be66 100644 --- a/test/sequencing/test_engelLeafSequencing.m +++ b/test/sequencing/test_engelLeafSequencing.m @@ -12,20 +12,21 @@ % 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 + for levels = numOfLevels resultGUI_sequenced = matRad_engelLeafSequencing(resultGUI,stf,dij,levels); fn_new = fieldnames(resultGUI_sequenced); diff --git a/test/sequencing/test_siochiLeafSequencing.m b/test/sequencing/test_siochiLeafSequencing.m index a68b3ee47..2c9860b81 100644 --- a/test/sequencing/test_siochiLeafSequencing.m +++ b/test/sequencing/test_siochiLeafSequencing.m @@ -12,20 +12,21 @@ % 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 + for levels = numOfLevels resultGUI_sequenced = matRad_siochiLeafSequencing(resultGUI,stf,dij,levels); fn_new = fieldnames(resultGUI_sequenced); diff --git a/test/sequencing/test_xiaLeafSequencing.m b/test/sequencing/test_xiaLeafSequencing.m index a4fabcc2d..28dfd3d32 100644 --- a/test/sequencing/test_xiaLeafSequencing.m +++ b/test/sequencing/test_xiaLeafSequencing.m @@ -12,20 +12,21 @@ % 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 + for levels = numOfLevels resultGUI_sequenced = matRad_xiaLeafSequencing(resultGUI,stf,dij,levels); fn_new = fieldnames(resultGUI_sequenced);