From e867f80b59c13b8b9a1fa735a49cbc64db5b0cee Mon Sep 17 00:00:00 2001 From: JenHardt <84377305+JenHardt@users.noreply.github.com> Date: Fri, 27 Jun 2025 17:27:12 +0200 Subject: [PATCH 01/20] first very rough draft of sequecing class with SiochiLeaf Implementation --- matRad/sequencing/matRad_SequencingBase.m | 21 ++ matRad/sequencing/matRad_SequencingIons.m | 24 ++ .../matRad_SequencingPhotonsAbstract.m | 60 ++++ .../matRad_SequencingPhotonsSiochiLeaf.m | 272 ++++++++++++++++++ 4 files changed, 377 insertions(+) create mode 100644 matRad/sequencing/matRad_SequencingBase.m create mode 100644 matRad/sequencing/matRad_SequencingIons.m create mode 100644 matRad/sequencing/matRad_SequencingPhotonsAbstract.m create mode 100644 matRad/sequencing/matRad_SequencingPhotonsSiochiLeaf.m diff --git a/matRad/sequencing/matRad_SequencingBase.m b/matRad/sequencing/matRad_SequencingBase.m new file mode 100644 index 000000000..303fb3eec --- /dev/null +++ b/matRad/sequencing/matRad_SequencingBase.m @@ -0,0 +1,21 @@ +classdef matRad_SequencingBase + %UNTITLED2 Summary of this class goes here + % Detailed explanation goes here + + properties (Constant, Abstract) + name; %Descriptive Name + shortName; %Short name for referencing + possibleRadiationModes; %Possible radiation modes for the respective StfGenerator + end + + properties (Access = public) + radiationMode; %Radiation Mode + visBool = true % vis bool + end + + methods + function this = matRad_SequencingBase() + + end + end +end \ No newline at end of file diff --git a/matRad/sequencing/matRad_SequencingIons.m b/matRad/sequencing/matRad_SequencingIons.m new file mode 100644 index 000000000..e686d8983 --- /dev/null +++ b/matRad/sequencing/matRad_SequencingIons.m @@ -0,0 +1,24 @@ +classdef matRad_SequencingIons < matRad_SequencingBase + %UNTITLED2 Summary of this class goes here + % Detailed explanation goes here + + properties (Constant) + name = 'Particle IMPT Scanning Sequencing'; + shortName = 'SequencingParticle'; + possibleRadiationModes = {'protons','helium','carbon'}; + end + + methods + function obj = matRad_SequencingIons(inputArg1,inputArg2) + %UNTITLED2 Construct an instance of this class + % Detailed explanation goes here + obj.Property1 = inputArg1 + inputArg2; + end + + function outputArg = method1(obj,inputArg) + %METHOD1 Summary of this method goes here + % Detailed explanation goes here + outputArg = obj.Property1 + inputArg; + end + end +end \ No newline at end of file diff --git a/matRad/sequencing/matRad_SequencingPhotonsAbstract.m b/matRad/sequencing/matRad_SequencingPhotonsAbstract.m new file mode 100644 index 000000000..83d1f7953 --- /dev/null +++ b/matRad/sequencing/matRad_SequencingPhotonsAbstract.m @@ -0,0 +1,60 @@ +classdef (Abstract) matRad_SequencingPhotonsAbstract < matRad_SequencingBase + + %UNTITLED Summary of this class goes here + % Detailed explanation goes her + properties + numOfLevels; + end + + methods + + + function [D_0,D_k, shapes,calFac, indInMx] = initBeam(this,stf, wCurr) + + numOfRaysPerBeam = stf.numOfRays; + X = ones(numOfRaysPerBeam,1)*NaN; + Z = ones(numOfRaysPerBeam,1)*NaN; + + for j = 1:stf.numOfRays + X(j) = stf.ray(j).rayPos_bev(:,1); + Z(j) = stf.ray(j).rayPos_bev(:,3); + end + + % sort bixels into matrix + minX = min(X); + maxX = max(X); + minZ = min(Z); + maxZ = max(Z); + + dimOfFluenceMxX = (maxX-minX)/stf.bixelWidth + 1; + dimOfFluenceMxZ = (maxZ-minZ)/stf.bixelWidth + 1; + + %Create the fluence matrix. + fluenceMx = zeros(dimOfFluenceMxZ,dimOfFluenceMxX); + + % Calculate X and Z position of every fluence's matrix spot z axis = + % axis of leaf movement! + xPos = (X-minX)/stf.bixelWidth+1; + zPos = (Z-minZ)/stf.bixelWidth+1; + + % Make subscripts for fluence matrix + indInMx = zPos + (xPos-1)*dimOfFluenceMxZ; + + %Save weights in fluence matrix. + fluenceMx(indInMx) = wCurr; + + % Stratification + calFac = max(fluenceMx(:)); + D_k = round(fluenceMx/calFac*this.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); + + end + + + end +end \ No newline at end of file diff --git a/matRad/sequencing/matRad_SequencingPhotonsSiochiLeaf.m b/matRad/sequencing/matRad_SequencingPhotonsSiochiLeaf.m new file mode 100644 index 000000000..d904ddc17 --- /dev/null +++ b/matRad/sequencing/matRad_SequencingPhotonsSiochiLeaf.m @@ -0,0 +1,272 @@ +classdef matRad_SequencingPhotonsSiochiLeaf < matRad_SequencingPhotonsAbstract + + %UNTITLED Summary of this class goes here + % Detailed explanation goes here + + properties (Constant) + name = 'Photons Sochi Leaf Sequenceer'; + shortName = 'Sochi Leaf'; + possibleRadiationModes = {'photons'}; + end + + methods + + + function sequence = sequence(this,w,stf) + + + offset = 0; + + for i = 1:numel(stf) + + [D_0,D_k, shapes,calFac,indInMx] = this.initBeam(stf(i),w(1+offset:stf(i).numOfRays+offset)); + + shapesWeight = zeros(10000,1); + k = 0; + + + %Decompose the port, do rod pushing + [tops, bases] = this.decomposePort(D_k); + %Form segments + [shapes,shapesWeight,k]=this.convertToSegments(shapes,shapesWeight,k,tops,bases); + + sequence.beam(i).numOfShapes = k; + sequence.beam(i).shapes = shapes(:,:,1:k); + sequence.beam(i).shapesWeight = shapesWeight(1:k)/this.numOfLevels*calFac; + sequence.beam(i).bixelIx = 1+offset:stf(i).numOfRays+offset; + sequence.beam(i).fluence = D_0; + sequence.beam(i).sum = zeros(size(D_0)); + + for j = 1:k + sequence.beam(i).sum = sequence.beam(i).sum+sequence.beam(i).shapes(:,:,j)*sequence.beam(i).shapesWeight(j); + end + sequence.w(1+offset:stf(i).numOfRays+offset,1) = sequence.beam(i).sum(indInMx); + + offset = offset + stf(i).numOfRays; + + end + + if this.visBool + this.plotSegments(sequence) + end + end + + + function [tops, bases] = decomposePort(~,map) + %Returns tops and bases of a fluence matrix "map" for Siochi leaf + %sequencing algorithm (rod pushing part). Accounts for collisions and + %tongue and groove (Tng) effects. + + [dimZ,dimX] = size(map); + map_nonZero = (map~=0); + + [D_k_Z, D_k_X] = ind2sub([dimZ,dimX], find(map_nonZero)); + minZ = min(D_k_Z); + maxZ = max(D_k_Z); + minX = min(D_k_X); + maxX = max(D_k_X); + + tops = zeros(dimZ, dimX); + bases = zeros(dimZ, dimX); + + for i = minX:maxX + maxTop = -1; + TnG = 1; + for j = minZ:maxZ + if i == minX + bases(j,i) = 1; + tops(j,i) = bases(j,i)+map(j,i)-1; + else %assign trial base positions + if map(j,i) >= map(j,i-1) %current rod >= previous, match the bases + bases(j,i) = bases(j,i-1); + tops(j,i) = bases(j,i)+map(j,i)-1; + else %current rod maxTop + maxTop = tops(j,i); + maxRow = j; + end + end + + %Correct for collision and tongue and groove error + while(TnG) + %go from maxRow down checking for TnG. This occurs when a shorter + %rod is "peeking over" a longer one in the direction transverse to + %the leaf motion. To fix this, match either the tops or bases of + %the rods. + for j = (maxRow-1):-1:minZ + if map(j,i) < map(j+1,i) + if tops(j,i) > tops(j+1,i) + tops(j+1,i) = tops(j,i); + bases(j+1,i) = tops(j+1,i)-map(j+1,i)+1; + elseif bases(j,i) < bases(j+1,i) + bases(j,i) = bases(j+1,i); + tops(j,i) = bases(j,i)+map(j,i)-1; + end + else + if tops(j,i) < tops(j+1,i) + tops(j,i) = tops(j+1,i); + bases(j,i) = tops(j,i)-map(j,i)+1; + elseif bases(j,i) > bases(j+1,i) + bases(j+1,i) = bases(j,i); + tops(j+1,i) = bases(j+1,i)+map(j+1,i)-1; + end + end + end + %go from maxRow up checking for TnG + for j = (maxRow+1):maxZ + if map(j,i) < map(j-1,i) + if tops(j,i) > tops(j-1,i) + tops(j-1,i) = tops(j,i); + bases(j-1,i) = tops(j-1,i)-map(j-1,i)+1; + elseif bases(j,i) < bases(j-1,i) + bases(j,i) = bases(j-1,i); + tops(j,i) = bases(j,i)+map(j,i)-1; + end + else + if tops(j,i) < tops(j-1,i) + tops(j,i) = tops(j-1,i); + bases(j,i) = tops(j,i)-map(j,i)+1; + elseif bases(j,i) > bases(j-1,i) + bases(j-1,i) = bases(j,i); + tops(j-1,i) = bases(j-1,i)+map(j-1,i)-1; + end + end + end + %now check if all TnG conditions have been removed + TnG = 0; + for j = (minZ+1):maxZ + if map(j,i) < map(j-1,i); + if tops(j,i) > tops(j-1,i) + TnG = 1; + elseif bases(j,i) < bases(j-1,i) + TnG = 1; + end + else + if tops(j,i) < tops(j-1,i) + TnG = 1; + elseif bases(j,i) > bases(j-1,i) + TnG = 1; + end + end + end + end + end + end + + function [shapes,shapesWeight,k] = convertToSegments(this, shapes,shapesWeight,k,tops,bases) + %Convert tops and bases to shape matrices. These are taken as to be the + %shapes of uniform level/elevation after the rods are pushed. + + + levels = max(tops(:)); + + for level = 1:levels + %check if slab is new + if this.differentSlab(tops,bases,level) + k = k+1; %increment number of unique slabs + shape_k = (bases <= level).*(level <= tops); %shape of current slab + shapes(:,:,k) = shape_k; + end + shapesWeight(k) = shapesWeight(k)+1; %if slab is not unique, this increments weight again + end + end + + function diffSlab = differentSlab(~,tops,bases,level) + + %Returns 1 if slab level is different than slab level-1 0 otherwise + + if level == 1 %first slab is automatically different + diffSlab = 1; + else + shapeLevel = (bases <= level).*(level <= tops); %shape of slab with current level + shapeLevel_1 = (bases <= level-1).*(level-1 <= tops); %shape of slab with previous level + diffSlab = ~isequal(shapeLevel,shapeLevel_1); %tests if slabs are equal; isequaln was not giving correct results + end + end + + function plotSegments(this,sequencing) + % create the sequencing figure + sz = [800 1000]; % figure size + 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)]); + + for i = 1:numel(sequencing) + + D_0 = sequencing.beam(i).fluence; + + clf(seqFig); + colormap(seqFig,'jet'); + + seqSubPlots(1) = subplot(2,2,1,'parent',seqFig); + imagesc(sequencing.beam(i).fluence,'parent',seqSubPlots(1)); + set(seqSubPlots(1),'CLim',[0 this.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 + + %show the leaf positions + D_k = sequencing.beam(i).fluence; + for k = 1:sequencing.beam(i).numOfShapes + shape_k = sequencing.beam(i).shapes(:,:,k); + [dimZ,dimX] = size(sequencing.beam(i).fluence); + seqSubPlots(4) = subplot(2,2,3.5,'parent',seqFig); + imagesc(shape_k,'parent',seqSubPlots(4)); + hold(seqSubPlots(4),'on'); + set(seqSubPlots(4),'YDir','normal') + xlabel(seqSubPlots(4),'x - direction parallel to leaf motion ') + ylabel(seqSubPlots(4),'z - direction perpendicular to leaf motion ') + title(seqSubPlots(4),['beam # ' num2str(i) ' shape # ' num2str(k) ' d_k = ' num2str(sequencing.beam(i).shapesWeight(k))]); + for j = 1:dimZ + leftLeafIx = find(shape_k(j,:)>0,1,'first'); + rightLeafIx = find(shape_k(j,:)>0,1,'last'); + if leftLeafIx > 1 + plot(seqSubPlots(4),[.5 leftLeafIx-.5],j-[.5 .5] ,'w','LineWidth',2) + plot(seqSubPlots(4),[.5 leftLeafIx-.5],j+[.5 .5] ,'w','LineWidth',2) + plot(seqSubPlots(4),[ leftLeafIx-.5 leftLeafIx-.5],j+[.5 -.5] ,'w','LineWidth',2) + end + if rightLeafIx 1 + sequencerHandle = sequencerHandle{1}; + end + sequencer = sequencerHandle(pln); + if warnDefault + matRad_cfg.dispWarning('Using default sequencer %s!', sequencer.name); + end + elseif ~isempty(classList) + sequencerHandle = classList(1).handle; + sequencer = sequencerHandle(pln); + matRad_cfg.dispWarning('Default sequencer not available! Using %s.', sequencer.name); + else + matRad_cfg.dispError('Default sequencer not found!'); + end + end + + if isempty(sequencer) + matRad_cfg.dispError('No suitable sequencer found!'); + end + + end + + function classList = getAvailableSequencers(pln,optionalPaths) + + matRad_cfg = MatRad_Config.instance(); + + %Parse inputs + if nargin < 2 + optionalPaths = {fileparts(mfilename("fullpath"))}; + else + if ~(iscellstr(optionalPaths) && all(optionalPaths)) + matRad_cfg.dispError('Invalid path array!'); + end + + optionalPaths = horzcat(fileparts(mfilename("fullpath")),optionalPaths); + end + + if nargin < 1 + pln = []; + else + if ~(isstruct(pln) || isempty(pln)) + matRad_cfg.dispError('Invalid pln!'); + end + end + + %Get available, valid classes through call to matRad helper function + %for finding subclasses + persistent allAvailableSequencers lastOptionalPaths + + %First we do a sanity check if persistently stored metaclasses are valid + if ~matRad_cfg.isOctave && ~isempty(allAvailableSequencers) && ~all(cellfun(@isvalid,allAvailableSequencers)) + matRad_cfg.dispWarning('Found invalid Sequencing Sequencers, updating cache.'); + allAvailableSequencers = []; + end + + if isempty(allAvailableSequencers) || (~isempty(lastOptionalPaths) && ~isequal(lastOptionalPaths, optionalPaths)) + lastOptionalPaths = optionalPaths; + allAvailableSequencers = matRad_findSubclasses(mfilename('class'),'folders',optionalPaths,'includeAbstract',false); + end + + availableSequencers = allAvailableSequencers; + + %Now filter for pln + ix = []; + + if nargin >= 1 && ~isempty(pln) + machine = matRad_loadMachine(pln); + machineMode = machine.meta.radiationMode; + + for cIx = 1:length(availableSequencers) + mc = availableSequencers{cIx}; + availabilityFuncStr = [mc.Name '.isAvailable']; + %availabilityFunc = str2func(availabilityFuncStr); %str2func does not seem to work on static class functions in Octave 5.2.0 + try + %available = availabilityFunc(pln,machine); + available = eval([availabilityFuncStr '(pln,machine)']); + catch + available = false; + mpList = mc.PropertyList; + if matRad_cfg.isMatlab + loc = find(arrayfun(@(x) strcmp('possibleRadiationModes',x.Name),mpList)); + propValue = mpList(loc).DefaultValue; + else + loc = find(cellfun(@(x) strcmp('possibleRadiationModes',x.Name),mpList)); + propValue = mpList{loc}.DefaultValue; + end + + if any(strcmp(propValue, pln.radiationMode)) + % get radiation mode from the in pln proposed basedata machine file + % add current class to return lists if the + % radiation mode is compatible + if(any(strcmp(propValue, machineMode))) + available = true; + + end + end + end + if available + ix = [ix cIx]; + end + end + + availableSequencers = availableSequencers (ix); + end + + classList = matRad_identifyClassesByConstantProperties(availableSequencers,'shortName','defaults',matRad_cfg.defaults.propSeq.sequencer,'additionalPropertyNames',{'name'}); + + end + + function [available,msg] = isAvailable(pln,machine) + + matRad_cfg = MatRad_Config.instance(); + matRad_cfg.dispError('This is an Abstract Base class! Function needs to be called for instantiable subclasses!'); + end + end + end \ No newline at end of file diff --git a/matRad/sequencing/matRad_SequencingIons.m b/matRad/sequencing/matRad_SequencingIons.m index e686d8983..69950e6f4 100644 --- a/matRad/sequencing/matRad_SequencingIons.m +++ b/matRad/sequencing/matRad_SequencingIons.m @@ -8,17 +8,34 @@ possibleRadiationModes = {'protons','helium','carbon'}; end - methods - function obj = matRad_SequencingIons(inputArg1,inputArg2) - %UNTITLED2 Construct an instance of this class - % Detailed explanation goes here - obj.Property1 = inputArg1 + inputArg2; - end - - function outputArg = method1(obj,inputArg) - %METHOD1 Summary of this method goes here - % Detailed explanation goes here - outputArg = obj.Property1 + inputArg; + methods (Static) + function [available,msg] = isAvailable(pln,machine) + % see superclass for information + + if nargin < 2 + machine = matRad_loadMachine(pln); + end + %checkBasic + available = isfield(machine,'meta') && isfield(machine,'data'); + + available = available && any(isfield(machine.meta,{'machine','radiationMode'})); + + if ~available + msg = 'Your machine file is invalid and does not contain the basic field (meta/data/radiationMode)!'; + else + msg = []; + end + + %check modality + checkModality = any(strcmp(matRad_SequencingIons.possibleRadiationModes, machine.meta.radiationMode)) && any(strcmp(matRad_SequencingIons.possibleRadiationModes, pln.radiationMode)); + + %Sanity check compatibility + if checkModality + checkModality = strcmp(machine.meta.radiationMode,pln.radiationMode); + end + + available = available && checkModality; + end end end \ No newline at end of file diff --git a/matRad/sequencing/matRad_SequencingPhotonsAbstract.m b/matRad/sequencing/matRad_SequencingPhotonsAbstract.m index 83d1f7953..99fe496d2 100644 --- a/matRad/sequencing/matRad_SequencingPhotonsAbstract.m +++ b/matRad/sequencing/matRad_SequencingPhotonsAbstract.m @@ -3,7 +3,8 @@ %UNTITLED Summary of this class goes here % Detailed explanation goes her properties - numOfLevels; + numOfMLCLeafPairs = 80; + sequencingLevel = 5; end methods @@ -45,7 +46,7 @@ % Stratification calFac = max(fluenceMx(:)); - D_k = round(fluenceMx/calFac*this.numOfLevels); + D_k = round(fluenceMx/calFac*this.sequencingLevel); % Save the stratification in the initial intensity matrix D_0. D_0 = D_k; @@ -55,6 +56,358 @@ end + function resultGUI = updateResultGUI(this,resultGUI,sequence, stf,dij) + resultGUI.w = sequence.w; + resultGUI.wSequenced = sequence.w; + resultGUI.sequencing = sequence; + resultGUI.apertureInfo = this.sequencing2ApertureInfo(sequence,stf); + + doseSequencedDoseGrid = reshape(dij.physicalDose{1} * sequence.w,dij.doseGrid.dimensions); + % interpolate to ct grid for visualiation & analysis + resultGUI.physicalDose = matRad_interp3(dij.doseGrid.x,dij.doseGrid.y',dij.doseGrid.z, ... + doseSequencedDoseGrid, ... + dij.ctGrid.x,dij.ctGrid.y',dij.ctGrid.z); + + % if weights exists from an former DAO remove it + if isfield(resultGUI,'wDao') + resultGUI = rmfield(resultGUI,'wDao'); + end + end + + function apertureInfo = sequencing2ApertureInfo(this,sequence,stf) + % MLC parameters: + bixelWidth = stf(1).bixelWidth; % [mm] + % 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(this.numOfMLCLeafPairs/2); + + % initializing variables + bixelIndOffset = 0; % used for creation of bixel index maps + totalNumOfBixels = sum([stf(:).totalNumOfBixels]); + totalNumOfShapes = sum([sequence.beam.numOfShapes]); + vectorOffset = totalNumOfShapes + 1; % used for bookkeeping in the vector for optimization + + % loop over all beams + for i=1:size(stf,2) + + %% 1. read stf and create maps (Ray & Bixelindex) + + % get x- and z-coordinates of bixels + rayPos_bev = reshape([stf(i).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(i).bixelWidth + 1; + dimZ = (maxZ-minZ)/stf(i).bixelWidth + 1; + + rayMap = zeros(dimZ,dimX); + + % get indices for x and z positions + xPos = (X-minX)/stf(i).bixelWidth + 1; + zPos = (Z-minZ)/stf(i).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(i).numOfRays] + bixelIndOffset; + bixelIndOffset = bixelIndOffset + stf(i).numOfRays; + + % store physical position of first entry in bixelIndMap + posOfCornerBixel = [minX 0 minZ]; + + % get leaf limits from the leaf map + lim_l = NaN * ones(dimZ,1); + lim_r = NaN * ones(dimZ,1); + % looping oder leaf pairs + for l = 1:dimZ + lim_lInd = find(rayMap(l,:),1,'first'); + lim_rInd = find(rayMap(l,:),1,'last'); + % the physical position [mm] can be calculated from the indices + lim_l(l) = (lim_lInd-1)*bixelWidth + minX - 1/2*bixelWidth; + lim_r(l) = (lim_rInd-1)*bixelWidth + minX + 1/2*bixelWidth; + end + + % get leaf positions for all shapes + % leaf positions can be extracted from the shapes created in Sequencing + for m = 1:sequence.beam(i).numOfShapes + + % loading shape from Sequencing result + shapeMap = sequence.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; + + 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 = sequence.beam(i).shapesWeight(m); + apertureInfo.beam(i).shape(m).shapeMap = shapeMap; + apertureInfo.beam(i).shape(m).vectorOffset = vectorOffset; + + % update index for bookkeeping + vectorOffset = vectorOffset + dimZ; + + 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(this.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]; + + % save data for each beam + apertureInfo.beam(i).numOfShapes = sequence.beam(i).numOfShapes; + apertureInfo.beam(i).numOfActiveLeafPairs = dimZ; + apertureInfo.beam(i).leafPairPos = leafPairPos; + apertureInfo.beam(i).isActiveLeafPair = isActiveLeafPair; + apertureInfo.beam(i).centralLeafPair = centralLeafPair; + apertureInfo.beam(i).lim_l = lim_l; + apertureInfo.beam(i).lim_r = lim_r; + apertureInfo.beam(i).bixelIndMap = bixelIndMap; + apertureInfo.beam(i).posOfCornerBixel = posOfCornerBixel; + apertureInfo.beam(i).MLCWindow = MLCWindow; + + end + + % save global data + apertureInfo.bixelWidth = bixelWidth; + apertureInfo.numOfMLCLeafPairs = this.numOfMLCLeafPairs; + apertureInfo.totalNumOfBixels = totalNumOfBixels; + apertureInfo.totalNumOfShapes = sum([apertureInfo.beam.numOfShapes]); + apertureInfo.totalNumOfLeafPairs = sum([apertureInfo.beam.numOfShapes]*[apertureInfo.beam.numOfActiveLeafPairs]'); + + % create vectors for optimization + [apertureInfo.apertureVector, apertureInfo.mappingMx, apertureInfo.limMx] = matRad_OptimizationProblemDAO.matRad_daoApertureInfo2Vec(apertureInfo); + end + function [pln,stf] = aperture2collimation(this,pln,stf,sequence,apertureInfo) + + bixelWidth = apertureInfo.bixelWidth; + leafWidth = bixelWidth; + convResolution = 0.5; %[mm] + + %The collimator limits are infered here from the apertureInfo. This could + %be handled differently by explicitly storing collimator info in the base + %data? + symmetricMLClimits = vertcat(apertureInfo.beam.MLCWindow); + symmetricMLClimits = max(abs(symmetricMLClimits)); + fieldWidth = 2*max(symmetricMLClimits); + + %modify basic pln variables + pln.propStf.bixelWidth = 'field'; + pln.propStf.collimation.convResolution = 0.5; %[mm] + pln.propStf.collimation.fieldWidth = fieldWidth; + pln.propStf.collimation.leafWidth = leafWidth; + + % + %[bixelFieldX,bixelFieldY] = ndgrid(-fieldWidth/2:bixelWidth:fieldWidth/2,-fieldWidth/2:leafWidth:fieldWidth/2); + [convFieldX,convFieldY] = meshgrid(-fieldWidth/2:convResolution:fieldWidth/2); + + %TODO: Not used in calcPhotonDose but imported from DICOM + %pln.propStf.collimation.Devices ... + %pln.propStf.collimation.numOfFields + %pln.propStf.collimation.beamMeterset + + for iBeam = 1:numel(stf) + stfTmp = stf(iBeam); + beamSequencing = sequence.beam(iBeam); + beamAperture = apertureInfo.beam(iBeam); + + stfTmp.bixelWidth = 'field'; + + nShapes = beamSequencing.numOfShapes; + + stfTmp.numOfRays = 1;% + stfTmp.numOfBixelsPerRay = nShapes; + stfTmp.totalNumOfBixels = nShapes; + + ray = struct(); + ray.rayPos_bev = [0 0 0]; + ray.targetPoint_bev = [0 stfTmp.SAD 0]; + ray.weight = 1; + ray.energy = stfTmp.ray(1).energy; + ray.beamletCornersAtIso = stfTmp.ray(1).beamletCornersAtIso; + ray.rayCorners_SCD = stfTmp.ray(1).rayCorners_SCD; + + %ray.shape = beamSequencing.sum; + shapeTotalF = zeros(size(convFieldX)); + + ray.shapes = struct(); + for iShape = 1:nShapes + currShape = beamAperture.shape(iShape); + activeLeafPairPosY = beamAperture.leafPairPos; + F = zeros(size(convFieldX)); + if this.visMode + hF = figure; imagesc(F); title(sprintf('Beam %d, Shape %d',iBeam,iShape)); hold on; + end + for iLeafPair = 1:numel(activeLeafPairPosY) + posY = activeLeafPairPosY(iLeafPair); + ixY = convFieldY >= posY-leafWidth/2 & convFieldY < posY + leafWidth/2; + ixX = convFieldX >= currShape.leftLeafPos(iLeafPair) & convFieldX < currShape.rightLeafPos(iLeafPair); + ix = ixX & ixY; + F(ix) = 1; + if visBool + figure(hF); imagesc(F); drawnow; pause(0.1); + end + end + + if visBool + pause(1); close(hF); + end + + F = F*currShape.weight; + shapeTotalF = shapeTotalF + F; + + ray.shapes(iShape).convFluence = F; + ray.shapes(iShape).shapeMap = currShape.shapeMap; + ray.shapes(iShape).weight = currShape.weight; + ray.shapes(iShape).leftLeafPos = currShape.leftLeafPos; + ray.shapes(iShape).rightLeafPos = currShape.rightLeafPos; + ray.shapes(iShape).leafPairCenterPos = activeLeafPairPosY; + end + + ray.shape = shapeTotalF; + ray.weight = ones(1,nShapes); + ray.collimation = pln.propStf.collimation; + stfTmp.ray = ray; + + stf(iBeam) = stfTmp; + end + end + + function plotSegments(this,sequencing) + % create the sequencing figure + sz = [800 1000]; % figure size + 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)]); + + for i = 1:numel(sequencing) + + D_0 = sequencing.beam(i).fluence; + + clf(seqFig); + colormap(seqFig,'jet'); + + seqSubPlots(1) = subplot(2,2,1,'parent',seqFig); + imagesc(sequencing.beam(i).fluence,'parent',seqSubPlots(1)); + set(seqSubPlots(1),'CLim',[0 this.sequencingLevel],'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 + + %show the leaf positions + D_k = sequencing.beam(i).fluence; + for k = 1:sequencing.beam(i).numOfShapes + shape_k = sequencing.beam(i).shapes(:,:,k); + [dimZ,dimX] = size(sequencing.beam(i).fluence); + seqSubPlots(4) = subplot(2,2,3.5,'parent',seqFig); + imagesc(shape_k,'parent',seqSubPlots(4)); + hold(seqSubPlots(4),'on'); + set(seqSubPlots(4),'YDir','normal') + xlabel(seqSubPlots(4),'x - direction parallel to leaf motion ') + ylabel(seqSubPlots(4),'z - direction perpendicular to leaf motion ') + title(seqSubPlots(4),['beam # ' num2str(i) ' shape # ' num2str(k) ' d_k = ' num2str(sequencing.beam(i).shapesWeight(k))]); + for j = 1:dimZ + leftLeafIx = find(shape_k(j,:)>0,1,'first'); + rightLeafIx = find(shape_k(j,:)>0,1,'last'); + if leftLeafIx > 1 + plot(seqSubPlots(4),[.5 leftLeafIx-.5],j-[.5 .5] ,'w','LineWidth',2) + plot(seqSubPlots(4),[.5 leftLeafIx-.5],j+[.5 .5] ,'w','LineWidth',2) + plot(seqSubPlots(4),[ leftLeafIx-.5 leftLeafIx-.5],j+[.5 -.5] ,'w','LineWidth',2) + end + if rightLeafIx 0) + + %calculate the difference matrix diffMat + diffMat = diff([zeros(size(D_k,1),1) D_k zeros(size(D_k,1),1)],[],2); + + %calculate complexities + c = sum(max(0,diffMat),2); %TNMU-row-complexity + com = max(c); %TNMU complexity + g = com - c; %row complexity gap + + %initialize segment + segment = zeros(size(D_k)); + + k = k + 1; + + + %loop over all rows + for j=1:size(D_0,1) + + %determine essential intervals + data(j).left(1) = 0; %left interval limit, actual for an empty interval + data(j).right(1) = 0; %right interal limit, actual for an empty interval + data(j).v(1) = g(j); %greatest number such that the inequalities (6) resp. (7) is satisfied with u=v + data(j).w(1) = inf; %smallest number in the interval + data(j).u(1) = data(j).v(1); %min(v,w) + + [~, pos, ~] = find(diffMat(j,:) > 0); % indices of all positive elements in the j. row of diffmat + [~, neg, ~] = find(diffMat(j,:) < 0); % indices of all negative elements in the j. row of diffMat + + n=2; + + %loop over the positive elements in the j. row of diffmat -> + %possible left interval limits + for m=1:size(pos,2) + + %loop over the negative elements in the j. row of diffMat -> + %possible right interval limit + for l=1:size(neg,2) + + %take only intervals I=[l,r] with l<=r + if pos(m) <= neg(l)-1 + + %set interval limits + data(j).left(n) = pos(m); + data(j).right(n) = neg(l)-1; + + %calculate v according to Lemma 8 + if g(j) <= abs( diffMat(j,pos(m)) + diffMat(j,neg(l)) ) + data(j).v(n) = min( diffMat(j,pos(m)), -diffMat(j,neg(l)) ) + g(j); + else + data(j).v(n) = ( diffMat(j, pos(m)) - diffMat(j, neg(l)) + g(j)) / 2; + end + + %calculate w and u according to equality (11) and + %(12) + data(j).w(n) = min(D_k(j,pos(m):(neg(l)-1))); + data(j).u(n) = min(data(j).v(n), data(j).w(n)); + + n = n+1; + end + end + end + + u(j) = max(data(j).u); + + end + + %calculate u_max from theorem 9 + d_k = min(u); + + %loop over all rows + for j=1:size(D_0,1) + + %find all possible (and essential) intervals + candidate = find(data(j).u >= d_k); + + %calculate the potential of the possible intervals + + %initialize p as -Inf + data(j).p(1:length(data(j).left)) = -Inf; + + %loop over all possible intervals + for s=1:size(candidate,2) + + if (s==1 && data(j).left(candidate(s)) == 0) + data(j).p(candidate(1)) = 0; + + + else + %calculate p1 according to equality (17) + if (d_k == diffMat(j, data(j).left(candidate(s))) && d_k ~= D_k(j, data(j).left(candidate(s)))) + p1 = 1; + + else + p1 = 0; + + end + + %calculate p2 according to equalitiy (18) + % if data(j).right(candidate(s)) < size(D_0, 2) + + if (d_k == -diffMat(j, data(j).right(candidate(s))+1) && d_k ~= D_k(j, data(j).right(candidate(s)))) + p2 = 1; + else + p2 = 0; + end + + % else + % + % if d_k == -diffMat(j, data(j).right(candidate(s))+1) + % p2 = 1; + % else + % p2 = 0; + % end + % + % end + + %calculate p3 according to equality (19) + p3 = size(find(D_k(j, data(j).left(candidate(s)):data(j).right(candidate(s))) == d_k),2); + + data(j).p(candidate(s)) = p1 + p2+ p3; + + end + + end + + %determinate intervals with maximum potential + maxPot = find(data(j).p == max(data(j).p)); + + %if several intervals have maximum potential, select + %the interval which has maximum length + if size(maxPot,2) > 1 + + for t=1:size(maxPot,2) + if t==1 && data(j).left(maxPot(t)) == 0 + data(j).l(1) = 0; + else + data(j).l(maxPot(t)) = data(j).right(maxPot(t)) - data(j).left(maxPot(t)) + 1; + end + end + + %data(j).l(maxPot) = data(j).right(maxPot) - data(j).left(maxPot) + 1; + + maxLength = find(data(j).l == max(data(j).l)); + + %left and right interval limits of the selected + %interval + leftIntLimit(j) = data(j).left(maxLength(1)); + rightIntLimit(j) = data(j).right(maxLength(1)); + + + else + + %left and right interval limits of the selected + %interval + leftIntLimit(j) = data(j).left(maxPot); + rightIntLimit(j) = data(j).right(maxPot); + + + end + + %create segment associated by the selected interval + if leftIntLimit(j) ~= 0 + + segment(j,leftIntLimit(j):rightIntLimit(j)) = 1; + + end + + end + + %write the segment in shape_k + shape_k = segment; + + %save shape_k in container + shapes(:,:,k) = shape_k; + + %save the calculated MU + shapesWeight(k) = d_k; + + %calculate new matrix, the diference matrix and complexities + D_k = D_k - d_k*shape_k; + + %delete variables + clear data; + clear segment; + clear u; + clear leftIntLimit; + clear rightIntLimit; + + end + + sequence.beam(i).numOfShapes = k; + sequence.beam(i).shapes = shapes(:,:,1:k); + sequence.beam(i).shapesWeight = shapesWeight(1:k)/this.sequencingLevel*calFac; + sequence.beam(i).bixelIx = 1+offset:stf(i).numOfRays+offset; + sequence.beam(i).fluence = D_0; + + sequence.w(1+offset:stf(i).numOfRays+offset,1) = D_0(indInMx)/this.sequencingLevel*calFac; + + offset = offset + stf(i).numOfRays; + + end + if this.visMode + this.plotSegments(sequence) + end + end + end + methods (Static) + function [available,msg] = isAvailable(pln,machine) + % see superclass for information + + if nargin < 2 + machine = matRad_loadMachine(pln); + end + + % Check superclass availability + [available,msg] = matRad_SequencingPhotonsAbstract.isAvailable(pln,machine); + + if ~available + return; + else + available = false; + msg = []; + end + + %checkBasic + try + checkBasic = isfield(machine,'meta') && isfield(machine,'data'); + + %check modality + checkModality = any(strcmp(matRad_SequencingPhotonsEngelLeaf.possibleRadiationModes, machine.meta.radiationMode)) && any(strcmp(matRad_SequencingPhotonsEngelLeaf.possibleRadiationModes, pln.radiationMode)); + + %Sanity check compatibility + if checkModality + checkModality = strcmp(machine.meta.radiationMode,pln.radiationMode); + end + + preCheck = checkBasic && checkModality; + + if ~preCheck + return; + end + catch + msg = 'Your machine file is invalid and does not contain the basic field (meta/data/radiationMode)!'; + return; + end + + available = preCheck; + end + end +end \ No newline at end of file diff --git a/matRad/sequencing/matRad_SequencingPhotonsSiochiLeaf.m b/matRad/sequencing/matRad_SequencingPhotonsSiochiLeaf.m index d904ddc17..04fb60596 100644 --- a/matRad/sequencing/matRad_SequencingPhotonsSiochiLeaf.m +++ b/matRad/sequencing/matRad_SequencingPhotonsSiochiLeaf.m @@ -4,9 +4,10 @@ % Detailed explanation goes here properties (Constant) - name = 'Photons Sochi Leaf Sequenceer'; - shortName = 'Sochi Leaf'; + name = 'Photons Siochi Leaf Sequenceer'; + shortName = 'siochi'; possibleRadiationModes = {'photons'}; + end methods @@ -23,8 +24,7 @@ shapesWeight = zeros(10000,1); k = 0; - - + %Decompose the port, do rod pushing [tops, bases] = this.decomposePort(D_k); %Form segments @@ -32,7 +32,7 @@ sequence.beam(i).numOfShapes = k; sequence.beam(i).shapes = shapes(:,:,1:k); - sequence.beam(i).shapesWeight = shapesWeight(1:k)/this.numOfLevels*calFac; + sequence.beam(i).shapesWeight = shapesWeight(1:k)/this.sequencingLevel*calFac; sequence.beam(i).bixelIx = 1+offset:stf(i).numOfRays+offset; sequence.beam(i).fluence = D_0; sequence.beam(i).sum = zeros(size(D_0)); @@ -46,7 +46,7 @@ end - if this.visBool + if this.visMode this.plotSegments(sequence) end end @@ -194,79 +194,48 @@ end end - function plotSegments(this,sequencing) - % create the sequencing figure - sz = [800 1000]; % figure size - 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)]); - - for i = 1:numel(sequencing) - - D_0 = sequencing.beam(i).fluence; - - clf(seqFig); - colormap(seqFig,'jet'); - - seqSubPlots(1) = subplot(2,2,1,'parent',seqFig); - imagesc(sequencing.beam(i).fluence,'parent',seqSubPlots(1)); - set(seqSubPlots(1),'CLim',[0 this.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 - - %show the leaf positions - D_k = sequencing.beam(i).fluence; - for k = 1:sequencing.beam(i).numOfShapes - shape_k = sequencing.beam(i).shapes(:,:,k); - [dimZ,dimX] = size(sequencing.beam(i).fluence); - seqSubPlots(4) = subplot(2,2,3.5,'parent',seqFig); - imagesc(shape_k,'parent',seqSubPlots(4)); - hold(seqSubPlots(4),'on'); - set(seqSubPlots(4),'YDir','normal') - xlabel(seqSubPlots(4),'x - direction parallel to leaf motion ') - ylabel(seqSubPlots(4),'z - direction perpendicular to leaf motion ') - title(seqSubPlots(4),['beam # ' num2str(i) ' shape # ' num2str(k) ' d_k = ' num2str(sequencing.beam(i).shapesWeight(k))]); - for j = 1:dimZ - leftLeafIx = find(shape_k(j,:)>0,1,'first'); - rightLeafIx = find(shape_k(j,:)>0,1,'last'); - if leftLeafIx > 1 - plot(seqSubPlots(4),[.5 leftLeafIx-.5],j-[.5 .5] ,'w','LineWidth',2) - plot(seqSubPlots(4),[.5 leftLeafIx-.5],j+[.5 .5] ,'w','LineWidth',2) - plot(seqSubPlots(4),[ leftLeafIx-.5 leftLeafIx-.5],j+[.5 -.5] ,'w','LineWidth',2) - end - if rightLeafIx 0 + + k = k + 1; + + + %Rounded off integer. Equation 7. + m = floor(log2(L_k)); + + % Convert m=1 if is less than 1. This happens when L_k belong to ]0,2[ + if m < 1 + m = 1; + end + + %Calculate the delivery intensity unit. Equation 6. + d_k = floor(2^(m-1)); + + % Opening matrix. + openingMx = D_k >= d_k; + + dimOfFluenceMxZ = size(shapes,1); + + switch this.mode + case 'sw' % sliding window technique! + for j = 1:dimOfFluenceMxZ + openIx = find(openingMx(j,:) == 1,1,'first'); + if ~isempty(openIx) + closeIx = find(openingMx(j,openIx+1:end) == 0,1,'first'); + if ~isempty(closeIx) + openingMx(j,openIx+closeIx:end) = 0; + end + end + + end + case 'rl' % reducing levels technique! + for j = 1:dimOfFluenceMxZ + [maxVal,maxIx] = max(openingMx(j,:) .* D_k(j,:)); + if maxVal > 0 + closeIx = maxIx + find(openingMx(j,maxIx+1:end) == 0,1,'first'); + if ~isempty(closeIx) + openingMx(j,closeIx:end) = 0; + end + openIx = find(openingMx(j,1:maxIx-1) == 0,1,'last'); + if ~isempty(openIx) + openingMx(j,1:openIx) = 0; + end + end + + end + otherwise + matRad_cfg.dispError('unknown sequencing mode') + end + + shape_k = openingMx * d_k; + + shapes(:,:,k) = shape_k; + shapesWeight(k) = d_k; + D_k = D_k - shape_k; + + L_k = max(D_k(:)); % eq 5 + + end + + sequence.beam(i).numOfShapes = k; + sequence.beam(i).shapes = shapes(:,:,1:k); + sequence.beam(i).shapesWeight = shapesWeight(1:k)/this.sequencingLevel*calFac; + sequence.beam(i).bixelIx = 1+offset:stf(i).numOfRays+offset; + sequence.beam(i).fluence = D_0; + + sequence.w(1+offset:stf(i).numOfRays+offset,1) = D_0(indInMx)/this.sequencingLevel*calFac; + + offset = offset + stf(i).numOfRays; + + end + if this.visMode + this.plotSegments(sequence) + end + end + end + methods (Static) + function [available,msg] = isAvailable(pln,machine) + % see superclass for information + + if nargin < 2 + machine = matRad_loadMachine(pln); + end + + % Check superclass availability + [available,msg] = matRad_SequencingPhotonsAbstract.isAvailable(pln,machine); + + if ~available + return; + else + available = false; + msg = []; + end + + %checkBasic + try + checkBasic = isfield(machine,'meta') && isfield(machine,'data'); + + %check modality + checkModality = any(strcmp(matRad_SequencingPhotonsXiaLeaf.possibleRadiationModes, machine.meta.radiationMode)) && any(strcmp(matRad_SequencingPhotonsXiaLeaf.possibleRadiationModes, pln.radiationMode)); + + %Sanity check compatibility + if checkModality + checkModality = strcmp(machine.meta.radiationMode,pln.radiationMode); + end + + preCheck = checkBasic && checkModality; + + if ~preCheck + return; + end + catch + msg = 'Your machine file is invalid and does not contain the basic field (meta/data/radiationMode)!'; + return; + end + + available = preCheck; + end + end +end \ No newline at end of file From dcb8749be1fd29d4c66ec978c24f0a0f1a748633 Mon Sep 17 00:00:00 2001 From: JenHardt <84377305+JenHardt@users.noreply.github.com> Date: Fri, 29 Aug 2025 17:06:25 +0200 Subject: [PATCH 03/20] continuationof sequencing --- examples/matRad_example10_4DphotonRobust.m | 5 +- examples/matRad_example11_helium.m | 2 - ..._example13_fitAnalyticalParticleBaseData.m | 2 - examples/matRad_example14_spotRemoval.m | 2 - examples/matRad_example16_photonMC_MLC.m | 9 +- examples/matRad_example17_biologicalModels.m | 2 - examples/matRad_example18_FREDMC.m | 2 - ..._example19_CT_sCT_DVH_difference_photons.m | 6 - examples/matRad_example1_phantom.m | 4 - examples/matRad_example2_photons.m | 6 - examples/matRad_example3_photonsDAO.m | 39 ++- examples/matRad_example4_photonsMC.m | 2 - examples/matRad_example5_protons.m | 2 - examples/matRad_example9_4DDoseCalcMinimal.m | 3 +- matRad/4D/matRad_acc4dDose.m | 76 +++++ matRad/4D/matRad_calc4dDose.m | 95 +------ matRad/MatRad_Config.m | 2 +- matRad/gui/widgets/matRad_PlanWidget.m | 14 - matRad/gui/widgets/matRad_WorkflowWidget.m | 14 +- matRad/matRad_sequencing.m | 22 +- matRad/scenarios/matRad_ScenarioModel.m | 4 + matRad/sequencing/matRad_SequencingBase.m | 13 +- matRad/sequencing/matRad_SequencingIons.m | 230 ++++++++++++++- .../matRad_SequencingPhotonsAbstract.m | 263 +++++++++--------- .../matRad_SequencingPhotonsEngelLeaf.m | 1 + .../matRad_SequencingPhotonsSiochiLeaf.m | 1 + .../matRad_SequencingPhotonsXiaLeaf.m | 1 + 27 files changed, 520 insertions(+), 302 deletions(-) create mode 100644 matRad/4D/matRad_acc4dDose.m diff --git a/examples/matRad_example10_4DphotonRobust.m b/examples/matRad_example10_4DphotonRobust.m index 479622758..bfc6f5683 100644 --- a/examples/matRad_example10_4DphotonRobust.m +++ b/examples/matRad_example10_4DphotonRobust.m @@ -177,8 +177,6 @@ pln.propStf.bixelWidth = 5; pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0); -pln.propOpt.runDAO = 0; -pln.propOpt.runSequencing = 0; % dose calculation settings pln.propDoseCalc.doseGrid.resolution.x = 5; % [mm] @@ -231,7 +229,8 @@ totalPhaseMatrix = ones(dij.totalNumOfBixels,ct.numOfCtScen)/ct.numOfCtScen; % the total phase matrix determines a mapping what fluence will be delivered in the which phase totalPhaseMatrix = bsxfun(@times,totalPhaseMatrix,resultGUIrobust.w); % equally distribute the fluence over all fluences -[resultGUIrobust4D, timeSequence] = matRad_calc4dDose(ct, pln, dij, stf, cst, resultGUIrobust,totalPhaseMatrix); +resultGUIrobust4D = matRad_calc4dDose(dij, pln, stf,resultGUIrobust,totalPhaseMatrix); +resultGUIrobust4D = matRad_acc4dDose( dij, pln, ct, cst,resultGUIrobust4D, 'DDM'); %% Visualize results diff --git a/examples/matRad_example11_helium.m b/examples/matRad_example11_helium.m index c02b2541b..054cdf0a8 100644 --- a/examples/matRad_example11_helium.m +++ b/examples/matRad_example11_helium.m @@ -55,8 +55,6 @@ pln.propStf.bixelWidth = 5; pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0); -pln.propOpt.runDAO = 0; -pln.propOpt.runSequencing = 0; % dose calculation settings pln.propDoseCalc.doseGrid.resolution.x = 5; % [mm] diff --git a/examples/matRad_example13_fitAnalyticalParticleBaseData.m b/examples/matRad_example13_fitAnalyticalParticleBaseData.m index ba2d165e0..9b70febc6 100644 --- a/examples/matRad_example13_fitAnalyticalParticleBaseData.m +++ b/examples/matRad_example13_fitAnalyticalParticleBaseData.m @@ -151,8 +151,6 @@ pln.propOpt.optimizer = 'IPOPT'; pln.propOpt.bioOptimization = 'none'; % none: physical optimization; const_RBExDose; constant RBE of 1.1; % LEMIV_effect: effect-based optimization; LEMIV_RBExDose: optimization of RBE-weighted dose -pln.propOpt.runDAO = false; % 1/true: run DAO, 0/false: don't / will be ignored for particles -pln.propOpt.runSequencing = false; % 1/true: run sequencing, 0/false: don't / will be ignored for particles and also triggered by runDAO below % retrieve scenarios for dose calculation and optimziation pln.multScen = matRad_multScen(ct,'nomScen'); diff --git a/examples/matRad_example14_spotRemoval.m b/examples/matRad_example14_spotRemoval.m index bf8db495e..29207185d 100644 --- a/examples/matRad_example14_spotRemoval.m +++ b/examples/matRad_example14_spotRemoval.m @@ -54,8 +54,6 @@ pln.propStf.bixelWidth = 3; pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0); -pln.propOpt.runDAO = 0; -pln.propOpt.runSequencing = 0; % dose calculation settings pln.propDoseCalc.doseGrid.resolution.x = 5; % [mm] diff --git a/examples/matRad_example16_photonMC_MLC.m b/examples/matRad_example16_photonMC_MLC.m index a746c8543..858586753 100644 --- a/examples/matRad_example16_photonMC_MLC.m +++ b/examples/matRad_example16_photonMC_MLC.m @@ -42,9 +42,6 @@ pln.propStf.bixelWidth = 10; pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0); -% Enable sequencing and direct aperture optimization (DAO). -pln.propOpt.runSequencing = 1; -pln.propOpt.runDAO = 1; quantityOpt = 'physicalDose'; modelName = 'none'; @@ -74,11 +71,11 @@ % 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. -resultGUI = matRad_siochiLeafSequencing(resultGUI,stf,dij,5,1); -[pln,stf] = matRad_aperture2collimation(pln,stf,resultGUI.sequencing,resultGUI.apertureInfo); +resultGUI = matRad_sequencing(resultGUI,stf,pln, dij); + %% Aperture visualization % Use a matrad function to visualize the resulting aperture shapes -matRad_visApertureInfo(resultGUI.apertureInfo) +matRad_visApertureInfo(resultGUI.sequencing.apertureInfo) %% Plot the Resulting Dose Slice % Just let's plot the transversal iso-center dose slice slice = matRad_world2cubeIndex(pln.propStf.isoCenter(1,:),ct); diff --git a/examples/matRad_example17_biologicalModels.m b/examples/matRad_example17_biologicalModels.m index 712c5e4cf..3d6137bd8 100644 --- a/examples/matRad_example17_biologicalModels.m +++ b/examples/matRad_example17_biologicalModels.m @@ -15,8 +15,6 @@ pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0); -pln.propOpt.runDAO = 0; -pln.propSeq.runSequencing = 0; % dose calculation settings pln.propDoseCalc.doseGrid.resolution.x = 8; diff --git a/examples/matRad_example18_FREDMC.m b/examples/matRad_example18_FREDMC.m index 3ed101830..427bb8512 100644 --- a/examples/matRad_example18_FREDMC.m +++ b/examples/matRad_example18_FREDMC.m @@ -34,8 +34,6 @@ pln.propStf.bixelWidth = 5; pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0); -pln.propOpt.runDAO = 0; -pln.propSeq.runSequencing = 0; % dose calculation settings pln.propDoseCalc.doseGrid.resolution.x = 5; % [mm] diff --git a/examples/matRad_example19_CT_sCT_DVH_difference_photons.m b/examples/matRad_example19_CT_sCT_DVH_difference_photons.m index 0502f88c2..dc28671ba 100644 --- a/examples/matRad_example19_CT_sCT_DVH_difference_photons.m +++ b/examples/matRad_example19_CT_sCT_DVH_difference_photons.m @@ -215,12 +215,6 @@ pln.propDoseCalc.doseGrid.resolution.y = 7; % [mm] pln.propDoseCalc.doseGrid.resolution.z = 7; % [mm] -%% -% Enable sequencing and disable direct aperture optimization (DAO) for now. -% A DAO optimization is shown in a seperate example. -pln.propSeq.runSequencing = 1; -pln.propOpt.runDAO = 0; - %% Generate Beam Geometry STF % The steering file struct comprises the complete beam geometry along with % ray position, pencil beam positions and energies, source to axis distance (SAD) etc. diff --git a/examples/matRad_example1_phantom.m b/examples/matRad_example1_phantom.m index 2f16d265c..6b3b3c24f 100644 --- a/examples/matRad_example1_phantom.m +++ b/examples/matRad_example1_phantom.m @@ -110,10 +110,6 @@ pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0); -% Settings for Optimization -pln.propOpt.runDAO = 0; -pln.propOpt.runSequencing = 0; - % dose calculation settings pln.propDoseCalc.doseGrid.resolution.x = 3; % [mm] pln.propDoseCalc.doseGrid.resolution.y = 3; % [mm] diff --git a/examples/matRad_example2_photons.m b/examples/matRad_example2_photons.m index 8523795c5..e4e5fbfbb 100644 --- a/examples/matRad_example2_photons.m +++ b/examples/matRad_example2_photons.m @@ -132,12 +132,6 @@ pln.propDoseCalc.doseGrid.resolution.y = 3; % [mm] pln.propDoseCalc.doseGrid.resolution.z = 3; % [mm] -%% -% Enable sequencing and disable direct aperture optimization (DAO) for now. -% A DAO optimization is shown in a seperate example. -pln.propSeq.runSequencing = 1; -pln.propOpt.runDAO = 0; - %% % and et voila our treatment plan structure is ready. Lets have a look: diff --git a/examples/matRad_example3_photonsDAO.m b/examples/matRad_example3_photonsDAO.m index 15c350e48..7809b304d 100644 --- a/examples/matRad_example3_photonsDAO.m +++ b/examples/matRad_example3_photonsDAO.m @@ -37,8 +37,8 @@ pln.machine = 'Generic'; pln.numOfFractions = 30; -pln.propStf.gantryAngles = [0:72:359]; -pln.propStf.couchAngles = [0 0 0 0 0]; +pln.propStf.gantryAngles = [0:90:359]; +pln.propStf.couchAngles = [0 0 0 0 ]; pln.propStf.bixelWidth = 5; pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0); @@ -47,9 +47,9 @@ pln.multScen = 'nomScen'; % dose calculation settings -pln.propDoseCalc.doseGrid.resolution.x = 3; % [mm] -pln.propDoseCalc.doseGrid.resolution.y = 3; % [mm] -pln.propDoseCalc.doseGrid.resolution.z = 3; % [mm] +pln.propDoseCalc.doseGrid.resolution.x = 5; % [mm] +pln.propDoseCalc.doseGrid.resolution.y = 5; % [mm] +pln.propDoseCalc.doseGrid.resolution.z = 5; % [mm] % We can also use other solver for optimization than IPOPT. matRad % currently supports fmincon from the MATLAB Optimization Toolbox. First we @@ -63,11 +63,6 @@ end pln.propOpt.quantityOpt = 'physicalDose'; -%% -% Enable sequencing and direct aperture optimization (DAO). -pln.propSeq.runSequencing = true; -pln.propOpt.runDAO = true; - %% Generate Beam Geometry STF stf = matRad_generateStf(ct,cst,pln); @@ -84,23 +79,37 @@ % treatment. Once the optimization has finished, trigger once the GUI to % visualize the optimized dose cubes. resultGUI = matRad_fluenceOptimization(dij,cst,pln); -matRadGUI; +%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. -resultGUI = matRad_sequencing(resultGUI,stf,dij,pln); + + +%% some testing of sequencing +pln.propSeq.sequencer = 'siochi'; +pln.propSeq.sequencingLevel = 10; +resultGUI_SIOCHI = matRad_sequencing(resultGUI,stf,pln, dij); +resultGUI_SIOCHI_OLD = matRad_siochiLeafSequencing(resultGUI,stf,dij,pln.propSeq.sequencingLevel,0); + +pln.propSeq.sequencer = 'xia'; +resultGUI_XIA = matRad_sequencing(resultGUI,stf,pln, dij); +resultGUI_XIA_OLD = matRad_xiaLeafSequencing(resultGUI,stf,dij,pln.propSeq.sequencingLevel,0); + +pln.propSeq.sequencer = 'engel'; +resultGUI_ENGEL = matRad_sequencing(resultGUI,stf,pln, dij); +resultGUI_ENGEL_OLD = matRad_engelLeafSequencing(resultGUI,stf,dij,pln.propSeq.sequencingLevel,0); %% DAO - Direct Aperture Optimization % The Direct Aperture Optimization is an optimization approach where we % directly optimize aperture shapes and weights. -resultGUI = matRad_directApertureOptimization(dij,cst,resultGUI.apertureInfo,resultGUI,pln); +resultGUI_SIOCHI_DAO = matRad_directApertureOptimization(dij,cst,resultGUI_SIOCHI.sequencing.apertureInfo,resultGUI,pln); %% Aperture visualization % Use a matrad function to visualize the resulting aperture shapes -matRad_visApertureInfo(resultGUI.apertureInfo); +matRad_visApertureInfo(resultGUI_SIOCHI_DAO.sequencing.apertureInfo); %% Indicator Calculation and display of DVH and QI -resultGUI = matRad_planAnalysis(resultGUI,ct,cst,stf,pln); +#resultGUI = matRad_planAnalysis(resultGUI,ct,cst,stf,pln); diff --git a/examples/matRad_example4_photonsMC.m b/examples/matRad_example4_photonsMC.m index 4e292b422..d2d88e432 100644 --- a/examples/matRad_example4_photonsMC.m +++ b/examples/matRad_example4_photonsMC.m @@ -43,8 +43,6 @@ pln.propStf.bixelWidth = 10; pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0); -pln.propSeq.runSequencing = 0; -pln.propOpt.runDAO = 0; % dose calculation settings % We can choose a different dose calculation engine, here "ompMC", by diff --git a/examples/matRad_example5_protons.m b/examples/matRad_example5_protons.m index b1be3bbcb..d61b1a1f7 100644 --- a/examples/matRad_example5_protons.m +++ b/examples/matRad_example5_protons.m @@ -63,8 +63,6 @@ pln.propStf.bixelWidth = 5; pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0); -pln.propOpt.runDAO = 0; -pln.propSeq.runSequencing = 0; % dose calculation settings pln.propDoseCalc.doseGrid.resolution.x = 3; % [mm] diff --git a/examples/matRad_example9_4DDoseCalcMinimal.m b/examples/matRad_example9_4DDoseCalcMinimal.m index 2ba1f209c..e3a578834 100644 --- a/examples/matRad_example9_4DDoseCalcMinimal.m +++ b/examples/matRad_example9_4DDoseCalcMinimal.m @@ -71,7 +71,8 @@ %% % calc 4D dose % make sure that the correct pln, dij and stf are loeaded in the workspace -[resultGUI, timeSequence] = matRad_calc4dDose(ct, pln, dij, stf, cst, resultGUI); +resultGUI = matRad_calc4dDose(dij, pln,stf,resultGUI); +resultGUI = matRad_acc4dDose( dij, pln, ct, cst,resultGUI, 'DDM'); % plot the result in comparison to the static dose slice = matRad_world2cubeIndex(pln.propStf.isoCenter(1,:),ct); diff --git a/matRad/4D/matRad_acc4dDose.m b/matRad/4D/matRad_acc4dDose.m new file mode 100644 index 000000000..1a9276e83 --- /dev/null +++ b/matRad/4D/matRad_acc4dDose.m @@ -0,0 +1,76 @@ +function resultGUI = matRad_acc4dDose( dij, pln, ct, cst,resultGUI, accType) +% wrapper for the whole 4D dose calculation pipeline and calculated dose +% accumulation +% +% call +% ct = matRad_calc4dDose(ct, pln, dij, stf, cst, resultGUI) +% +% input +% ct : ct cube +% pln: matRad plan meta information struct +% dij: matRad dij struct +% stf: matRad steering information struct +% cst: matRad cst struct +% resultGUI: struct containing optimized fluence vector +% totalPhaseMatrix optional intput for totalPhaseMatrix +% accType: witch algorithim for dose accumulation +% output +% resultGUI: structure containing phase dose, RBE weighted dose, etc +% timeSequence: timing information about the irradiation +% +% References +% - +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2018 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_cfg = MatRad_Config.instance(); + +if ~isa(pln.bioModel,'matRad_BiologicalModel') + pln.bioModel = matRad_BiologicalModel.validate(pln.bioModel,pln.radiationMode); +end + +% accumulation +resultGUI.accPhysicalDose = matRad_doseAcc(ct,resultGUI.phaseDose, cst, accType); +if isa(pln.bioModel,'matRad_ConstantRBE') + + resultGUI.accRBExDose = matRad_doseAcc(ct,resultGUI.phaseRBExDose, cst, accType); + +elseif isa(pln.bioModel,'matRad_LQBasedModel') + + resultGUI.accAlphaDose = matRad_doseAcc(ct,resultGUI.phaseAlphaDose, cst,accType); + resultGUI.accSqrtBetaDose = matRad_doseAcc(ct,resultGUI.phaseSqrtBetaDose, cst, accType); + + % only compute where we have biologically defined tissue + ix = (ax{1} ~= 0); + + resultGUI.accEffect = resultGUI.accAlphaDose + resultGUI.accSqrtBetaDose.^2; + + resultGUI.accRBExDose = zeros(ct.cubeDim); + resultGUI.accRBExDose(ix) = ((sqrt(ax{1}(ix).^2 + 4 .* bx{1}(ix) .* resultGUI.accEffect(ix)) - ax{1}(ix))./(2.*bx{1}(ix))); +end + +for beamIx = 1:dij.numOfBeams + resultGUI.(['accPhysicalDose_beam', num2str(beamIx)])= matRad_doseAcc(ct,resultGUI.(['phaseDose_beam', num2str(beamIx)]), cst, accType); + if isa(pln.bioModel,'matRad_ConstantRBE') + resultGUI.(['accRBExDose_beam', num2str(beamIx)]) = matRad_doseAcc(ct,resultGUI.(['phaseRBExDose_beam', num2str(beamIx)]), cst, accType); + elseif isa(pln.bioModel,'matRad_LQBasedModel') + resultGUI.(['accAlphaDose_beam', num2str(beamIx)]) = matRad_doseAcc(ct,resultGUI.(['phaseAlphaDose_beam', num2str(beamIx)]), cst, accType); + resultGUI.(['accSqrtBetaDose_beam', num2str(beamIx)]) = matRad_doseAcc(ct,resultGUI.(['phaseAlphaDose_beam', num2str(beamIx)]), cst, accType); + resultGUI.(['accEffect_beam', num2str(beamIx)]) = resultGUI.(['accAlphaDose_beam', num2str(beamIx)]) + resultGUI.(['accSqrtBetaDose_beam', num2str(beamIx)]).^2; + resultGUI.(['accRBExDose_beam', num2str(beamIx)]){i} = zeros(ct.cubeDim); + resultGUI.(['accRBExDose_beam', num2str(beamIx)]){i} = ((sqrt(ax{i}(ix).^2 + 4 .* bx{i}(ix) .* resultGUI.(['accEffect_beam', num2str(beamIx)]){i}(ix)) - ax{i}(ix))./(2.*bx{i}(ix))); + end +end + + +end \ No newline at end of file diff --git a/matRad/4D/matRad_calc4dDose.m b/matRad/4D/matRad_calc4dDose.m index 5aea56558..5cb890f10 100644 --- a/matRad/4D/matRad_calc4dDose.m +++ b/matRad/4D/matRad_calc4dDose.m @@ -1,4 +1,4 @@ -function [resultGUI, timeSequence] = matRad_calc4dDose(ct, pln, dij, stf, cst, resultGUI, totalPhaseMatrix,accType) +function resultGUI = matRad_calc4dDose(dij, pln,stf,resultGUI, totalPhaseMatrix) % wrapper for the whole 4D dose calculation pipeline and calculated dose % accumulation % @@ -6,24 +6,18 @@ % ct = matRad_calc4dDose(ct, pln, dij, stf, cst, resultGUI) % % input -% ct : ct cube -% pln: matRad plan meta information struct % dij: matRad dij struct -% stf: matRad steering information struct -% cst: matRad cst struct % resultGUI: struct containing optimized fluence vector % totalPhaseMatrix optional intput for totalPhaseMatrix -% accType: witch algorithim for dose accumulation % output % resultGUI: structure containing phase dose, RBE weighted dose, etc -% timeSequence: timing information about the irradiation % % References % - % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2018 the matRad development team. +% Copyright 2025 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 @@ -34,42 +28,22 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% matRad_cfg = MatRad_Config.instance(); -if ~exist('accType','var') - accType = 'DDM'; -end - -if ~exist('totalPhaseMatrix','var') - % make a time sequence for when each bixel is irradiated, the sequence - % follows the backforth spot scanning - timeSequence = matRad_makeBixelTimeSeq(stf, resultGUI); - - % prepare a phase matrix - motion = 'linear'; % the assumed motion type - timeSequence = matRad_makePhaseMatrix(timeSequence, ct.numOfCtScen, ct.motionPeriod, motion); - - resultGUI.bioModel = pln.bioModel; - - % the total phase matrix determines what beamlet will be administered in what ct phase - totalPhaseMatrix = vertcat(timeSequence.phaseMatrix); -else - timeSequence = []; +if ~exist('totalPhaseMatrix','var') + sequencer = matRad_SequencingBase.getSequencerFromPln(pln); + sequence = sequencer.sequence(resultGUI.w,stf); + sequence = sequencer.makePhaseMatrix(sequence,pln.multScen.numOfCtScen, pln.multScen.motionPeriod); + resultGUI.sequencing = sequence; + totalPhaseMatrix = vertcat(sequence.phaseMatrix); end -% Get Biological Model -if ~isfield(pln,'bioModel') - pln.bioModel = 'none'; -end + if ~isa(pln.bioModel,'matRad_BiologicalModel') pln.bioModel = matRad_BiologicalModel.validate(pln.bioModel,pln.radiationMode); end -if isa(pln.bioModel,'matRad_LQBasedModel') - [ax,bx] = matRad_getPhotonLQMParameters(cst,numel(resultGUI.physicalDose)); -end - % compute all phases -for i = 1:ct.numOfCtScen +for i = 1:size(totalPhaseMatrix,2) tmpResultGUI = matRad_calcCubes(totalPhaseMatrix(:,i),dij,i); @@ -82,10 +56,10 @@ elseif isa(pln.bioModel,'matRad_LQBasedModel') resultGUI.phaseAlphaDose{i} = tmpResultGUI.alpha .* tmpResultGUI.physicalDose; resultGUI.phaseSqrtBetaDose{i} = sqrt(tmpResultGUI.beta) .* tmpResultGUI.physicalDose; - ix = ax{i} ~=0; + ix = dij.ax{i} ~=0; resultGUI.phaseEffect{i} = resultGUI.phaseAlphaDose{i} + resultGUI.phaseSqrtBetaDose{i}.^2; resultGUI.phaseRBExDose{i} = zeros(ct.cubeDim); - resultGUI.phaseRBExDose{i}(ix) = ((sqrt(ax{i}(ix).^2 + 4 .* bx{i}(ix) .* resultGUI.phaseEffect{i}(ix)) - ax{i}(ix))./(2.*bx{i}(ix))); + resultGUI.phaseRBExDose{i}(ix) = ((sqrt(dij.ax{i,1}(ix).^2 + 4 .* dij.bx{i,1}(ix) .* resultGUI.phaseEffect{i}(ix)) -dij.ax{i,1}(ix))./(2.*dij.bx{i,1}(ix))); else matRad_cfg.dispError('Unsupported biological model %s!',pln.bioModel.model); end @@ -100,10 +74,10 @@ elseif isa(pln.bioModel,'matRad_LQBasedModel') resultGUI.(['phaseAlphaDose_beam', num2str(beamIx)]){i} = tmpResultGUI.(['alpha_beam', num2str(beamIx)]).*tmpResultGUI.(['physicalDose_beam', num2str(beamIx)]); resultGUI.(['phaseSqrtBetaDose_beam', num2str(beamIx)]){i} = sqrt(tmpResultGUI.(['beta_beam', num2str(beamIx)])).*tmpResultGUI.(['physicalDose_beam', num2str(beamIx)]); - ix = ax{i} ~=0; + ix = dij.ax{i} ~=0; resultGUI.(['phaseEffect_beam', num2str(beamIx)]){i} = resultGUI.(['phaseAlphaDose_beam', num2str(beamIx)]){i} + resultGUI.(['phaseSqrtBetaDose_beam', num2str(beamIx)]){i}.^2; resultGUI.(['phaseRBExDose_beam', num2str(beamIx)]){i} = zeros(ct.cubeDim); - resultGUI.(['phaseRBExDose_beam', num2str(beamIx)]){i} = ((sqrt(ax{i}(ix).^2 + 4 .* bx{i}(ix) .* resultGUI.(['phaseEffect_beam', num2str(beamIx)]){i}(ix)) - ax{i}(ix))./(2.*bx{i}(ix))); + resultGUI.(['phaseRBExDose_beam', num2str(beamIx)]){i} = ((sqrt(dij.ax{i,1}(ix).^2 + 4 .* dij.bx{i,1}(ix) .* resultGUI.(['phaseEffect_beam', num2str(beamIx)]){i}(ix)) - dij.ax{i,1}(ix))./(2.*dij.bx{i,1}(ix))); else matRad_cfg.dispError('Unsupported biological model %s!',pln.bioModel.model); end @@ -111,47 +85,6 @@ end end -% accumulation -resultGUI.accPhysicalDose = matRad_doseAcc(ct,resultGUI.phaseDose, cst, accType); - -if ~isa(pln.bioModel,'matRad_EmptyBiologicalModel') - if isa(pln.bioModel,'matRad_ConstantRBE') - - resultGUI.accRBExDose = matRad_doseAcc(ct,resultGUI.phaseRBExDose, cst, accType); - - elseif isa(pln.bioModel,'matRad_LQBasedModel') - - resultGUI.accAlphaDose = matRad_doseAcc(ct,resultGUI.phaseAlphaDose, cst,accType); - resultGUI.accSqrtBetaDose = matRad_doseAcc(ct,resultGUI.phaseSqrtBetaDose, cst, accType); - - % only compute where we have biologically defined tissue - ix = (ax{1} ~= 0); - - resultGUI.accEffect = resultGUI.accAlphaDose + resultGUI.accSqrtBetaDose.^2; - - resultGUI.accRBExDose = zeros(ct.cubeDim); - resultGUI.accRBExDose(ix) = ((sqrt(ax{1}(ix).^2 + 4 .* bx{1}(ix) .* resultGUI.accEffect(ix)) - ax{1}(ix))./(2.*bx{1}(ix))); - else - matRad_cfg.dispError('Unsupported biological model %s!',pln.bioModel.model); - end -end - -for beamIx = 1:dij.numOfBeams - resultGUI.(['accPhysicalDose_beam', num2str(beamIx)])= matRad_doseAcc(ct,resultGUI.(['phaseDose_beam', num2str(beamIx)]), cst, accType); - if ~isa(pln.bioModel,'matRad_EmptyBiologicalModel') - if isa(pln.bioModel,'matRad_ConstantRBE') - resultGUI.(['accRBExDose_beam', num2str(beamIx)]) = matRad_doseAcc(ct,resultGUI.(['phaseRBExDose_beam', num2str(beamIx)]), cst, accType); - elseif isa(pln.bioModel,'matRad_LQBasedModel') - resultGUI.(['accAlphaDose_beam', num2str(beamIx)]) = matRad_doseAcc(ct,resultGUI.(['phaseAlphaDose_beam', num2str(beamIx)]), cst, accType); - resultGUI.(['accSqrtBetaDose_beam', num2str(beamIx)]) = matRad_doseAcc(ct,resultGUI.(['phaseAlphaDose_beam', num2str(beamIx)]), cst, accType); - resultGUI.(['accEffect_beam', num2str(beamIx)]) = resultGUI.(['accAlphaDose_beam', num2str(beamIx)]) + resultGUI.(['accSqrtBetaDose_beam', num2str(beamIx)]).^2; - resultGUI.(['accRBExDose_beam', num2str(beamIx)]){i} = zeros(ct.cubeDim); - resultGUI.(['accRBExDose_beam', num2str(beamIx)]){i} = ((sqrt(ax{i}(ix).^2 + 4 .* bx{i}(ix) .* resultGUI.(['accEffect_beam', num2str(beamIx)]){i}(ix)) - ax{i}(ix))./(2.*bx{i}(ix))); - else - matRad_cfg.dispError('Unsupported biological model %s!',pln.bioModel.model); - end - end -end end diff --git a/matRad/MatRad_Config.m b/matRad/MatRad_Config.m index a465cc7f2..31a3de1e9 100644 --- a/matRad/MatRad_Config.m +++ b/matRad/MatRad_Config.m @@ -245,7 +245,7 @@ function setDefaultProperties(obj) obj.defaults.propOpt.clearUnusedVoxels = false; %Sequencing Options - obj.defaults.propSeq.sequencer = {'siochi', 'IMRT'}; + obj.defaults.propSeq.sequencer = {'siochi', 'IMPT'}; diff --git a/matRad/gui/widgets/matRad_PlanWidget.m b/matRad/gui/widgets/matRad_PlanWidget.m index 03cf3139e..24c600195 100644 --- a/matRad/gui/widgets/matRad_PlanWidget.m +++ b/matRad/gui/widgets/matRad_PlanWidget.m @@ -904,18 +904,6 @@ set(handles.popMenuMultScen,'Value',ix); end - if strcmp(pln.radiationMode,'photons') && isfield(pln.propOpt,'runDAO') - set(handles.btnRunDAO,'Value',pln.propOpt.runDAO); - else - set(handles.btnRunDAO,'Value', 0 ); - end - - if isfield(pln, 'propSeq') && isfield(pln.propSeq, 'sequencingLevel') - set(handles.btnRunSequencing,'Value',pln.propSeq.runSequencing); - set(handles.editSequencingLevel,'String',num2str(pln.propSeq.sequencingLevel)); - else - set(handles.btnRunSequencing,'Value', 0 ); - end if isfield (pln.propOpt, 'conf3D') set(handles.radiobutton3Dconf,'Value',pln.propOpt.conf3D); @@ -1061,9 +1049,7 @@ function updatePlnInWorkspace(this,hObject,evtData) end contents = get(handles.popUpMenuSequencer,'String'); pln.propSeq.sequencer = contents{get(handles.popUpMenuSequencer,'Value')}; - pln.propSeq.runSequencing = logical(get(handles.btnRunSequencing,'Value')); pln.propSeq.sequencingLevel = this.parseStringAsNum(get(handles.editSequencingLevel,'String'),false); - pln.propOpt.runDAO = logical(get(handles.btnRunDAO,'Value')); pln.propOpt.conf3D = logical(get(handles.radiobutton3Dconf,'Value')); diff --git a/matRad/gui/widgets/matRad_WorkflowWidget.m b/matRad/gui/widgets/matRad_WorkflowWidget.m index e7416a803..1a5979ee7 100644 --- a/matRad/gui/widgets/matRad_WorkflowWidget.m +++ b/matRad/gui/widgets/matRad_WorkflowWidget.m @@ -450,6 +450,8 @@ function btnOptimize_Callback(this, hObject, eventdata) % eventdata reserved - to be defined in a future version of MATLAB % handles structure with handles and user data (see GUIDATA) handles = this.handles; + runDAO = this.widgetHandle.Parent.Children(9).Children(13).Value; + runSeq = this.widgetHandle.Parent.Children(9).Children(18).Value; try % indicate that matRad is busy % change mouse pointer to hour glass @@ -491,7 +493,7 @@ function btnOptimize_Callback(this, hObject, eventdata) assignin('base','resultGUI',resultGUI); - if ~pln.propOpt.runDAO || ~strcmp(pln.radiationMode,'photons') + if ~runDAO|| ~strcmp(pln.radiationMode,'photons') CheckOptimizerStatus(this,usedOptimizer,'Fluence') end @@ -508,7 +510,7 @@ function btnOptimize_Callback(this, hObject, eventdata) try %% sequencing - resultGUI = matRad_sequencing(resultGUI,evalin('base','stf'),dij,pln); + resultGUI = matRad_sequencing(resultGUI,evalin('base','stf'),pln,dij); assignin('base','resultGUI',resultGUI); @@ -523,20 +525,20 @@ function btnOptimize_Callback(this, hObject, eventdata) try %% DAO - if strcmp(pln.radiationMode,'photons') && pln.propOpt.runDAO + if strcmp(pln.radiationMode,'photons') && runDAO showWarning(this,['Observe: You are running direct aperture optimization' filesep 'This is experimental code that has not been thoroughly debugged - especially in combination with constrained optimization.']); % was assigned to handles WHY ? [resultGUI,usedOptimizer] = matRad_directApertureOptimization(evalin('base','dij'),evalin('base','cst'),... - resultGUI.apertureInfo,resultGUI,pln); + resultGUI.sequencing.apertureInfo,resultGUI,pln); assignin('base','resultGUI',resultGUI); % check IPOPT status and return message for GUI user CheckOptimizerStatus(this,usedOptimizer,'DAO'); end - if strcmp(pln.radiationMode,'photons') && (pln.propSeq.runSequencing || pln.propOpt.runDAO) + if strcmp(pln.radiationMode,'photons') && runSeq|| runDAO - matRad_visApertureInfo(resultGUI.apertureInfo); + matRad_visApertureInfo(resultGUI.sequencing.apertureInfo); end catch ME diff --git a/matRad/matRad_sequencing.m b/matRad/matRad_sequencing.m index 019724ee0..cad3cbf9f 100644 --- a/matRad/matRad_sequencing.m +++ b/matRad/matRad_sequencing.m @@ -1,4 +1,4 @@ -function resultGUI = matRad_sequencing(resultGUI,stf,dij,pln,visBool) +function resultGUI = matRad_sequencing(resultGUI,stf,pln,dij,visMode) % matRad inverse planning wrapper function % % call @@ -30,16 +30,26 @@ % LICENSE file. % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - +matRad_cfg = MatRad_Config.instance(); + sequencer = matRad_SequencingBase.getSequencerFromPln(pln); -if nargin == 5 - sequencer.visMode = visMode; - +% Handle optional inputs +if nargin == 5 && ~isempty(visMode) + sequencer.visMode = visMode; +end +if nargin < 4 || isempty(dij) + dij = []; end sequence = sequencer.sequence(resultGUI.w,stf); -resultGUI = sequencer.updateResultGUI(resultGUI,sequence,stf,dij); +if ~isempty(dij) + resultGUI = matRad_calcCubes(sequence.w,dij); +else + + matRad_cfg.dispWarning('Dose not recalcaulted with sequenced fluence'); +end +resultGUI.sequencing = sequence; end diff --git a/matRad/scenarios/matRad_ScenarioModel.m b/matRad/scenarios/matRad_ScenarioModel.m index f84a3dfc3..f1e41d251 100644 --- a/matRad/scenarios/matRad_ScenarioModel.m +++ b/matRad/scenarios/matRad_ScenarioModel.m @@ -48,6 +48,7 @@ numOfCtScen; % total number of CT scenarios used numOfAvailableCtScen; % total number of CT scenarios existing in ct structure ctScenIx; % map of all ct scenario indices per scenario + motionPeriod = Inf % motion period of 4D CT, if 4D CT % these parameters will be filled according to the choosen scenario type @@ -77,6 +78,9 @@ else this.numOfCtScen = ct.numOfCtScen; this.numOfAvailableCtScen = ct.numOfCtScen; + if isfield(ct,'motionPeriod') + this.motionPeriod = ct.motionPeriod; + end end this.ctScenProb = [(1:this.numOfCtScen)', ones(this.numOfCtScen,1)./this.numOfCtScen]; %Equal probability to be in each phase of the 4D ct diff --git a/matRad/sequencing/matRad_SequencingBase.m b/matRad/sequencing/matRad_SequencingBase.m index 8c9b83fa0..6de20f5fb 100644 --- a/matRad/sequencing/matRad_SequencingBase.m +++ b/matRad/sequencing/matRad_SequencingBase.m @@ -1,4 +1,4 @@ -classdef matRad_SequencingBase < handle +classdef (Abstract) matRad_SequencingBase < handle %UNTITLED2 Summary of this class goes here % Detailed explanation goes here @@ -138,6 +138,17 @@ function assignPropertiesFromPln(this,pln,warnWhenPropertyChanged) end end + function sequence = sequence(this,w,stf) + + matRad_cfg = MatRad_Config.instance(); + matRad_cfg.dispError('This is an Abstract Base class! Function needs to be called for instantiable subclasses!'); + end + + function resultGUI = updateResultGUI(this,sequence,varargin) + + matRad_cfg = MatRad_Config.instance(); + matRad_cfg.dispError('This is an Abstract Base class! Function needs to be called for instantiable subclasses!'); + end end methods (Static) function sequencer = getSequencerFromPln(pln, warnDefault) diff --git a/matRad/sequencing/matRad_SequencingIons.m b/matRad/sequencing/matRad_SequencingIons.m index 69950e6f4..f9f225797 100644 --- a/matRad/sequencing/matRad_SequencingIons.m +++ b/matRad/sequencing/matRad_SequencingIons.m @@ -3,11 +3,195 @@ % Detailed explanation goes here properties (Constant) - name = 'Particle IMPT Scanning Sequencing'; - shortName = 'SequencingParticle'; - possibleRadiationModes = {'protons','helium','carbon'}; + name = 'Particle IMPT Scanning Sequencing'; + shortName = 'IMPT'; + possibleRadiationModes = {'protons','helium','carbon'}; + weightPencilBeam = 1e6 end + properties + esTime = 3 * 10^6; % [\mu s] time required for synchrotron to recharge it' spill + spillRechargeTime = 2 * 10^6; % [\mu s] number of particles generated in each spill + spillSize = 4 * 10 ^ 10; + scanSpeed = 10; % [m/s] speed of synchrotron's lateral scanning in an IES + spillIntensity = 4 * 10 ^ 8; % number of particles per second + end + + methods + + function sequence = sequence(this,w,stf) + + sequence = this.calcSpotOrder(stf); + sequence = this.calcSpotTime(sequence,w,stf); + end + + function resultGUI = updateResultGUI(~,sequence,~,~) + resultGUI.sequencing = sequence; + end + + function sequence = calcSpotOrder(~,stf) + + sequence = struct; + + wOffset = 0; + % first loop loops over all bixels to store their position and ray number in each IES + for i = 1:length(stf) + + usedEnergies = unique([stf(i).ray(:).energy]); + usedEnergiesSorted = sort(usedEnergies, 'descend'); + + sequence(i).orderToSTF = zeros(stf(i).totalNumOfBixels, 1); + sequence(i).orderToSS = zeros(stf(i).totalNumOfBixels, 1); + sequence(i).time = zeros(stf(i).totalNumOfBixels, 1); + sequence(i).e = zeros(stf(i).totalNumOfBixels, 1); + + + for e = 1:length(usedEnergies) % looping over IES's + + s = 1; + + for j = 1:stf(i).numOfRays % looping over all rays + + % find the rays which are active in current IES + if(any(stf(i).ray(j).energy == usedEnergiesSorted(e))) + + x = stf(i).ray(j).rayPos_bev(1); + y = stf(i).ray(j).rayPos_bev(3); + + sequence(i).IES(e).x(s) = x; % store x position + sequence(i).IES(e).y(s) = y; % store y position + sequence(i).IES(e).wIndex(s) = wOffset + ... + sum(stf(i).numOfBixelsPerRay(1:(j-1))) + ... + find(stf(i).ray(j).energy == usedEnergiesSorted(e)); % store index + + s = s + 1; + + end + end + end + + wOffset = wOffset + sum(stf(i).numOfBixelsPerRay); + + end + + end + + function sequence = calcSpotTime(this,sequence,w,stf) + steerTime = [stf.bixelWidth] * (10 ^ 3)/ this.scanSpeed; % [\mu s] + % after storing all the required information, + % same loop over all bixels will put each bixel in it's order + + spillUsage= 0; + offset = 0; + + for i = 1:length(stf) + + usedEnergies = unique([stf(i).ray(:).energy]); + + t = 0; + orderCount = 1; + + for e = 1: length(usedEnergies) + + % sort the y positions from high to low (backforth is up do down) + y_sorted = sort(unique(sequence(i).IES(e).y), 'descend'); + x_sorted = sort(sequence(i).IES(e).x, 'ascend'); + + for k = 1:length(y_sorted) + + y = y_sorted(k); + % find indexes corresponding to current y position + % in other words, number of bixels in the current row + ind_y = find(sequence(i).IES(e).y == y); + + % since backforth fasion is zig zag like, flip the order every + % second row + if ~rem(k,2) + ind_y = fliplr(ind_y); + end + + % loop over all the bixels in the row + for is = 1:length(ind_y) + + s = ind_y(is); + + x = x_sorted(s); + + wIndex = sequence(i).IES(e).wIndex(s); + + % in case there were holes inside the plan "multi" + % multiplies the steertime to take it into account: + if(k == 1 && is == 1) + x_prev = x; + y_prev = y; + end + % x direction + multi = abs(x_prev - x)/stf(i).bixelWidth; + % y direction + multi = multi + abs(y_prev - y)/stf(i).bixelWidth; + % + x_prev = x; + y_prev = y; + + % calculating the time: + + % required spot fluence + numOfParticles = w(wIndex)*this.weightPencilBeam; + % time spent to spill the required spot fluence + spillTime = numOfParticles * 10^6 / this.spillIntensity; + + % spotTime:time spent to steer scan along IES per bixel + t = t + multi * steerTime(i) + spillTime; + + % taking account of the time to recharge the spill in case + % the required fluence was more than spill size + if(spillUsage+ numOfParticles > this.spillSize) + t = t + this.spillRechargeTime; + spillUsage= 0; + end + + % used amount of fluence from current spill + spillUsage= spillUsage + numOfParticles; + + % storing the time and the order of bixels + + % make the both counter and index 'per beam' - help index + wInd = wIndex - offset; + + % timeline according to the spot scanning order + sequence(i).time(orderCount) = t; + % IES of bixels according to the spot scanning order + sequence(i).e(orderCount) = e; + % according to spot scanning order, sorts w index of all + % bixels, use this order to transfer STF order to Spot + % Scanning order + sequence(i).orderToSS(orderCount) = wInd; + + % according to STF order, gives us order of irradiation of + % each bixel, use this order to transfer Spot Scanning + % order to STF order + % orderToSTF(orderToSS) = orderToSS(orderToSTF) = 1:#bixels + sequence(i).orderToSTF(wInd) = orderCount; + + orderCount = orderCount + 1; + + end + end + + t = t + this.esTime; + + end + + % storing the fluence per beam + sequence(i).w = w(offset + 1: offset + stf(i).totalNumOfBixels); + + offset = offset + stf(i).totalNumOfBixels; + end + end + + end + + methods (Static) function [available,msg] = isAvailable(pln,machine) % see superclass for information @@ -37,5 +221,45 @@ available = available && checkModality; end + + function sequence = makePhaseMatrix(sequence, numOfPhases, motionPeriod) + + phaseTime = motionPeriod * 10 ^ 6/numOfPhases; % time of each phase [/mu s] + + for i = 1:length(sequence) + + realTime = phaseTime; + sequence(i).phaseMatrix = zeros(length(sequence(i).time),numOfPhases); + + iPhase = 1; + iTime = 1; + + while (iTime <= length(sequence(i).time)) + if(sequence(i).time(iTime) < realTime) + while(iTime <= length(sequence(i).time) && sequence(i).time(iTime) < realTime) + sequence(i).phaseMatrix(iTime, iPhase) = 1; + iTime = iTime + 1; + end + else + + iPhase = iPhase + 1; + % back to 1 after going over all phases + if(iPhase > numOfPhases) + iPhase = 1; + end + realTime = realTime + phaseTime; + end + end + + % permuatation of phaseMatrix from SS order to STF order + sequence(i).phaseMatrix = sequence(i).phaseMatrix(sequence(i).orderToSTF,:); + [sequence(i).phaseNum,~] = find(sequence(i).phaseMatrix'); + % inserting the fluence in phaseMatrix + sequence(i).phaseMatrix = sequence(i).phaseMatrix .* sequence(i).w; + end + end + end + + end \ No newline at end of file diff --git a/matRad/sequencing/matRad_SequencingPhotonsAbstract.m b/matRad/sequencing/matRad_SequencingPhotonsAbstract.m index 99fe496d2..cb0d63bea 100644 --- a/matRad/sequencing/matRad_SequencingPhotonsAbstract.m +++ b/matRad/sequencing/matRad_SequencingPhotonsAbstract.m @@ -9,6 +9,10 @@ methods + function sequence = sequence(this,w,stf) + + throw(MException('MATLAB:class:AbstractMember','Abstract function sequence needs to be implemented!')) + end function [D_0,D_k, shapes,calFac, indInMx] = initBeam(this,stf, wCurr) @@ -56,25 +60,7 @@ end - function resultGUI = updateResultGUI(this,resultGUI,sequence, stf,dij) - resultGUI.w = sequence.w; - resultGUI.wSequenced = sequence.w; - resultGUI.sequencing = sequence; - resultGUI.apertureInfo = this.sequencing2ApertureInfo(sequence,stf); - - doseSequencedDoseGrid = reshape(dij.physicalDose{1} * sequence.w,dij.doseGrid.dimensions); - % interpolate to ct grid for visualiation & analysis - resultGUI.physicalDose = matRad_interp3(dij.doseGrid.x,dij.doseGrid.y',dij.doseGrid.z, ... - doseSequencedDoseGrid, ... - dij.ctGrid.x,dij.ctGrid.y',dij.ctGrid.z); - - % if weights exists from an former DAO remove it - if isfield(resultGUI,'wDao') - resultGUI = rmfield(resultGUI,'wDao'); - end - end - - function apertureInfo = sequencing2ApertureInfo(this,sequence,stf) + function sequence = sequencing2ApertureInfo(this,sequence,stf) % MLC parameters: bixelWidth = stf(1).bixelWidth; % [mm] % define central leaf pair (here we want the 0mm position to be in the @@ -166,11 +152,11 @@ 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 = sequence.beam(i).shapesWeight(m); - apertureInfo.beam(i).shape(m).shapeMap = shapeMap; - apertureInfo.beam(i).shape(m).vectorOffset = vectorOffset; + sequence.apertureInfo.beam(i).shape(m).leftLeafPos = leftLeafPos; + sequence.apertureInfo.beam(i).shape(m).rightLeafPos = rightLeafPos; + sequence.apertureInfo.beam(i).shape(m).weight = sequence.beam(i).shapesWeight(m); + sequence.apertureInfo.beam(i).shape(m).shapeMap = shapeMap; + sequence.apertureInfo.beam(i).shape(m).vectorOffset = vectorOffset; % update index for bookkeeping vectorOffset = vectorOffset + dimZ; @@ -199,124 +185,30 @@ minZ-bixelWidth/2 maxZ+bixelWidth/2]; % save data for each beam - apertureInfo.beam(i).numOfShapes = sequence.beam(i).numOfShapes; - apertureInfo.beam(i).numOfActiveLeafPairs = dimZ; - apertureInfo.beam(i).leafPairPos = leafPairPos; - apertureInfo.beam(i).isActiveLeafPair = isActiveLeafPair; - apertureInfo.beam(i).centralLeafPair = centralLeafPair; - apertureInfo.beam(i).lim_l = lim_l; - apertureInfo.beam(i).lim_r = lim_r; - apertureInfo.beam(i).bixelIndMap = bixelIndMap; - apertureInfo.beam(i).posOfCornerBixel = posOfCornerBixel; - apertureInfo.beam(i).MLCWindow = MLCWindow; + sequence.apertureInfo.beam(i).numOfShapes = sequence.beam(i).numOfShapes; + sequence.apertureInfo.beam(i).numOfActiveLeafPairs = dimZ; + sequence.apertureInfo.beam(i).leafPairPos = leafPairPos; + sequence.apertureInfo.beam(i).isActiveLeafPair = isActiveLeafPair; + sequence.apertureInfo.beam(i).centralLeafPair = centralLeafPair; + sequence.apertureInfo.beam(i).lim_l = lim_l; + sequence.apertureInfo.beam(i).lim_r = lim_r; + sequence.apertureInfo.beam(i).bixelIndMap = bixelIndMap; + sequence.apertureInfo.beam(i).posOfCornerBixel = posOfCornerBixel; + sequence.apertureInfo.beam(i).MLCWindow = MLCWindow; end % save global data - apertureInfo.bixelWidth = bixelWidth; - apertureInfo.numOfMLCLeafPairs = this.numOfMLCLeafPairs; - apertureInfo.totalNumOfBixels = totalNumOfBixels; - apertureInfo.totalNumOfShapes = sum([apertureInfo.beam.numOfShapes]); - apertureInfo.totalNumOfLeafPairs = sum([apertureInfo.beam.numOfShapes]*[apertureInfo.beam.numOfActiveLeafPairs]'); + sequence.apertureInfo.bixelWidth = bixelWidth; + sequence.apertureInfo.numOfMLCLeafPairs = this.numOfMLCLeafPairs; + sequence.apertureInfo.totalNumOfBixels = totalNumOfBixels; + sequence.apertureInfo.totalNumOfShapes = sum([sequence.apertureInfo.beam.numOfShapes]); + sequence.apertureInfo.totalNumOfLeafPairs = sum([sequence.apertureInfo.beam.numOfShapes]*[sequence.apertureInfo.beam.numOfActiveLeafPairs]'); % create vectors for optimization - [apertureInfo.apertureVector, apertureInfo.mappingMx, apertureInfo.limMx] = matRad_OptimizationProblemDAO.matRad_daoApertureInfo2Vec(apertureInfo); + [sequence.apertureInfo.apertureVector, sequence.apertureInfo.mappingMx, sequence.apertureInfo.limMx] = matRad_OptimizationProblemDAO.matRad_daoApertureInfo2Vec(sequence.apertureInfo); end - function [pln,stf] = aperture2collimation(this,pln,stf,sequence,apertureInfo) - - bixelWidth = apertureInfo.bixelWidth; - leafWidth = bixelWidth; - convResolution = 0.5; %[mm] - - %The collimator limits are infered here from the apertureInfo. This could - %be handled differently by explicitly storing collimator info in the base - %data? - symmetricMLClimits = vertcat(apertureInfo.beam.MLCWindow); - symmetricMLClimits = max(abs(symmetricMLClimits)); - fieldWidth = 2*max(symmetricMLClimits); - - %modify basic pln variables - pln.propStf.bixelWidth = 'field'; - pln.propStf.collimation.convResolution = 0.5; %[mm] - pln.propStf.collimation.fieldWidth = fieldWidth; - pln.propStf.collimation.leafWidth = leafWidth; - - % - %[bixelFieldX,bixelFieldY] = ndgrid(-fieldWidth/2:bixelWidth:fieldWidth/2,-fieldWidth/2:leafWidth:fieldWidth/2); - [convFieldX,convFieldY] = meshgrid(-fieldWidth/2:convResolution:fieldWidth/2); - - %TODO: Not used in calcPhotonDose but imported from DICOM - %pln.propStf.collimation.Devices ... - %pln.propStf.collimation.numOfFields - %pln.propStf.collimation.beamMeterset - - for iBeam = 1:numel(stf) - stfTmp = stf(iBeam); - beamSequencing = sequence.beam(iBeam); - beamAperture = apertureInfo.beam(iBeam); - - stfTmp.bixelWidth = 'field'; - - nShapes = beamSequencing.numOfShapes; - - stfTmp.numOfRays = 1;% - stfTmp.numOfBixelsPerRay = nShapes; - stfTmp.totalNumOfBixels = nShapes; - - ray = struct(); - ray.rayPos_bev = [0 0 0]; - ray.targetPoint_bev = [0 stfTmp.SAD 0]; - ray.weight = 1; - ray.energy = stfTmp.ray(1).energy; - ray.beamletCornersAtIso = stfTmp.ray(1).beamletCornersAtIso; - ray.rayCorners_SCD = stfTmp.ray(1).rayCorners_SCD; - - %ray.shape = beamSequencing.sum; - shapeTotalF = zeros(size(convFieldX)); - - ray.shapes = struct(); - for iShape = 1:nShapes - currShape = beamAperture.shape(iShape); - activeLeafPairPosY = beamAperture.leafPairPos; - F = zeros(size(convFieldX)); - if this.visMode - hF = figure; imagesc(F); title(sprintf('Beam %d, Shape %d',iBeam,iShape)); hold on; - end - for iLeafPair = 1:numel(activeLeafPairPosY) - posY = activeLeafPairPosY(iLeafPair); - ixY = convFieldY >= posY-leafWidth/2 & convFieldY < posY + leafWidth/2; - ixX = convFieldX >= currShape.leftLeafPos(iLeafPair) & convFieldX < currShape.rightLeafPos(iLeafPair); - ix = ixX & ixY; - F(ix) = 1; - if visBool - figure(hF); imagesc(F); drawnow; pause(0.1); - end - end - - if visBool - pause(1); close(hF); - end - - F = F*currShape.weight; - shapeTotalF = shapeTotalF + F; - - ray.shapes(iShape).convFluence = F; - ray.shapes(iShape).shapeMap = currShape.shapeMap; - ray.shapes(iShape).weight = currShape.weight; - ray.shapes(iShape).leftLeafPos = currShape.leftLeafPos; - ray.shapes(iShape).rightLeafPos = currShape.rightLeafPos; - ray.shapes(iShape).leafPairCenterPos = activeLeafPairPosY; - end - - ray.shape = shapeTotalF; - ray.weight = ones(1,nShapes); - ray.collimation = pln.propStf.collimation; - stfTmp.ray = ray; - - stf(iBeam) = stfTmp; - end - end function plotSegments(this,sequencing) % create the sequencing figure @@ -409,5 +301,106 @@ function plotSegments(this,sequencing) msg = []; end end + + function [pln,stf] = aperture2collimation(pln,stf,sequence, visBool) + + if nargin < 4 + visBool = false; + end + + bixelWidth = sequence.apertureInfo.bixelWidth; + leafWidth = bixelWidth; + convResolution = 0.5; %[mm] + + %The collimator limits are infered here from the apertureInfo. This could + %be handled differently by explicitly storing collimator info in the base + %data? + symmetricMLClimits = vertcat(apertureInfo.beam.MLCWindow); + symmetricMLClimits = max(abs(symmetricMLClimits)); + fieldWidth = 2*max(symmetricMLClimits); + + %modify basic pln variables + pln.propStf.bixelWidth = 'field'; + pln.propStf.collimation.convResolution = 0.5; %[mm] + pln.propStf.collimation.fieldWidth = fieldWidth; + pln.propStf.collimation.leafWidth = leafWidth; + + % + %[bixelFieldX,bixelFieldY] = ndgrid(-fieldWidth/2:bixelWidth:fieldWidth/2,-fieldWidth/2:leafWidth:fieldWidth/2); + [convFieldX,convFieldY] = meshgrid(-fieldWidth/2:convResolution:fieldWidth/2); + + %TODO: Not used in calcPhotonDose but imported from DICOM + %pln.propStf.collimation.Devices ... + %pln.propStf.collimation.numOfFields + %pln.propStf.collimation.beamMeterset + + for iBeam = 1:numel(stf) + stfTmp = stf(iBeam); + beamSequencing = sequence.beam(iBeam); + beamAperture = sequence.apertureInfo.beam(iBeam); + + stfTmp.bixelWidth = 'field'; + + nShapes = beamSequencing.numOfShapes; + + stfTmp.numOfRays = 1;% + stfTmp.numOfBixelsPerRay = nShapes; + stfTmp.totalNumOfBixels = nShapes; + + ray = struct(); + ray.rayPos_bev = [0 0 0]; + ray.targetPoint_bev = [0 stfTmp.SAD 0]; + ray.weight = 1; + ray.energy = stfTmp.ray(1).energy; + ray.beamletCornersAtIso = stfTmp.ray(1).beamletCornersAtIso; + ray.rayCorners_SCD = stfTmp.ray(1).rayCorners_SCD; + + %ray.shape = beamSequencing.sum; + shapeTotalF = zeros(size(convFieldX)); + + ray.shapes = struct(); + for iShape = 1:nShapes + currShape = beamAperture.shape(iShape); + activeLeafPairPosY = beamAperture.leafPairPos; + F = zeros(size(convFieldX)); + if visBool + hF = figure; imagesc(F); title(sprintf('Beam %d, Shape %d',iBeam,iShape)); hold on; + end + for iLeafPair = 1:numel(activeLeafPairPosY) + posY = activeLeafPairPosY(iLeafPair); + ixY = convFieldY >= posY-leafWidth/2 & convFieldY < posY + leafWidth/2; + ixX = convFieldX >= currShape.leftLeafPos(iLeafPair) & convFieldX < currShape.rightLeafPos(iLeafPair); + ix = ixX & ixY; + F(ix) = 1; + if visBool + figure(hF); imagesc(F); drawnow; pause(0.1); + end + end + + if visBool + pause(1); close(hF); + end + + F = F*currShape.weight; + shapeTotalF = shapeTotalF + F; + + ray.shapes(iShape).convFluence = F; + ray.shapes(iShape).shapeMap = currShape.shapeMap; + ray.shapes(iShape).weight = currShape.weight; + ray.shapes(iShape).leftLeafPos = currShape.leftLeafPos; + ray.shapes(iShape).rightLeafPos = currShape.rightLeafPos; + ray.shapes(iShape).leafPairCenterPos = activeLeafPairPosY; + end + + ray.shape = shapeTotalF; + ray.weight = ones(1,nShapes); + ray.collimation = pln.propStf.collimation; + stfTmp.ray = ray; + + stf(iBeam) = stfTmp; + end + end end -end \ No newline at end of file +end + + diff --git a/matRad/sequencing/matRad_SequencingPhotonsEngelLeaf.m b/matRad/sequencing/matRad_SequencingPhotonsEngelLeaf.m index acd2af5cd..fb366521c 100644 --- a/matRad/sequencing/matRad_SequencingPhotonsEngelLeaf.m +++ b/matRad/sequencing/matRad_SequencingPhotonsEngelLeaf.m @@ -245,6 +245,7 @@ if this.visMode this.plotSegments(sequence) end + sequence = this.sequencing2ApertureInfo(sequence,stf); end end methods (Static) diff --git a/matRad/sequencing/matRad_SequencingPhotonsSiochiLeaf.m b/matRad/sequencing/matRad_SequencingPhotonsSiochiLeaf.m index 04fb60596..002806b80 100644 --- a/matRad/sequencing/matRad_SequencingPhotonsSiochiLeaf.m +++ b/matRad/sequencing/matRad_SequencingPhotonsSiochiLeaf.m @@ -49,6 +49,7 @@ if this.visMode this.plotSegments(sequence) end + sequence = this.sequencing2ApertureInfo(sequence,stf); end diff --git a/matRad/sequencing/matRad_SequencingPhotonsXiaLeaf.m b/matRad/sequencing/matRad_SequencingPhotonsXiaLeaf.m index c0a47111e..a8aa9a525 100644 --- a/matRad/sequencing/matRad_SequencingPhotonsXiaLeaf.m +++ b/matRad/sequencing/matRad_SequencingPhotonsXiaLeaf.m @@ -126,6 +126,7 @@ if this.visMode this.plotSegments(sequence) end + sequence = this.sequencing2ApertureInfo(sequence,stf); end end methods (Static) From bb931d3463dd093de0244e0305acfd631077fd93 Mon Sep 17 00:00:00 2001 From: JenHardt <84377305+JenHardt@users.noreply.github.com> Date: Fri, 27 Feb 2026 08:58:23 +0100 Subject: [PATCH 04/20] Update ompMC --- submodules/ompMC | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/ompMC b/submodules/ompMC index a8f4a673a..c612c7843 160000 --- a/submodules/ompMC +++ b/submodules/ompMC @@ -1 +1 @@ -Subproject commit a8f4a673a84bca5392f2a55d52ad1cd0c4dd74db +Subproject commit c612c7843732aa1bb41fbb27438dcf2e3ec166a7 From 4bbfe0fe9e23e581a8f14507d4b519aecf640f85 Mon Sep 17 00:00:00 2001 From: JenHardt <84377305+JenHardt@users.noreply.github.com> Date: Fri, 27 Feb 2026 10:40:57 +0100 Subject: [PATCH 05/20] delete old files --- matRad/matRad_sequencingOld.m | 71 ---- .../sequencing/matRad_engelLeafSequencing.m | 395 ------------------ .../matRad_sequencing2ApertureInfo.m | 179 -------- .../sequencing/matRad_siochiLeafSequencing.m | 379 ----------------- matRad/sequencing/matRad_xiaLeafSequencing.m | 284 ------------- 5 files changed, 1308 deletions(-) delete mode 100644 matRad/matRad_sequencingOld.m delete mode 100644 matRad/sequencing/matRad_engelLeafSequencing.m delete mode 100644 matRad/sequencing/matRad_sequencing2ApertureInfo.m delete mode 100644 matRad/sequencing/matRad_siochiLeafSequencing.m delete mode 100644 matRad/sequencing/matRad_xiaLeafSequencing.m diff --git a/matRad/matRad_sequencingOld.m b/matRad/matRad_sequencingOld.m deleted file mode 100644 index 51207ada2..000000000 --- a/matRad/matRad_sequencingOld.m +++ /dev/null @@ -1,71 +0,0 @@ -function resultGUI = matRad_sequencing(resultGUI,stf,dij,pln,visBool) -% matRad inverse planning wrapper function -% -% call -% resultGUI = matRad_sequencing(resultGUI,stf,dij,pln) -% -% input -% dij: matRad dij struct -% stf: matRad stf struct -% pln: matRad pln struct -% resultGUI: struct containing optimized fluence vector, dose, and (for -% biological optimization) RBE-weighted dose etc. -% -% output -% resultGUI: struct containing optimized fluence vector, dose, and (for -% biological optimization) RBE-weighted dose etc. -% -% References -% - -% -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% -% 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/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_cfg = MatRad_Config.instance(); - -if nargin < 5 - visBool = 0; -end - -if ~isfield(pln,'propSeq') - pln.propSeq = struct('runSequencing',false); -end - -if strcmp(pln.radiationMode,'photons') && (pln.propSeq.runSequencing || pln.propOpt.runDAO) - - if ~isfield(pln.propSeq, 'sequencer') - pln.propSeq.sequencer = 'siochi'; % default: siochi sequencing algorithm - 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).') - end - - switch pln.propSeq.sequencer - case 'xia' - resultGUI = matRad_xiaLeafSequencing(resultGUI,stf,dij,pln.propSeq.sequencingLevel,visBool); - case 'engel' - resultGUI = matRad_engelLeafSequencing(resultGUI,stf,dij,pln.propSeq.sequencingLevel,visBool); - case 'siochi' - resultGUI = matRad_siochiLeafSequencing(resultGUI,stf,dij,pln.propSeq.sequencingLevel,visBool); - otherwise - matRad_cfg.dispError('Could not find specified sequencing algorithm ''%s''',pln.propSeq.sequencer); - end -elseif (pln.propSeq.runSequencing || pln.propOpt.runDAO) && ~strcmp(pln.radiationMode,'photons') - matRad_cfg.dispWarning('Sequencing is only specified for pln.radiationMode = "photons". Continuing with out sequencing ... ') -end -end - - diff --git a/matRad/sequencing/matRad_engelLeafSequencing.m b/matRad/sequencing/matRad_engelLeafSequencing.m deleted file mode 100644 index bc1436574..000000000 --- a/matRad/sequencing/matRad_engelLeafSequencing.m +++ /dev/null @@ -1,395 +0,0 @@ -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 -% -% call -% resultGUI = matRad_engelLeafSequencing(resultGUI,stf,dij,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) -% -% output -% resultGUI: matRad result struct containing the new dose cube -% as well as the corresponding weights -% -% References -% [1] http://www.sciencedirect.com/science/article/pii/S0166218X05001411 -% -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% -% 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/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. -% -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -% if visBool not set toogle off visualization -if nargin < 5 - visBool = 0; -end - -numOfBeams = numel(stf); - -if visBool - % create the sequencing figure - sz = [800 1000]; % figure size - 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)]); -end - -offset = 0; - -for i = 1:numOfBeams - - numOfRaysPerBeam = stf(i).numOfRays; - - % get relevant weights for current beam - wOfCurrBeams = resultGUI.w(1+offset:numOfRaysPerBeam+offset).* ones(size(stf(i).ray,2),1); - - X = ones(size(stf(i).ray,2),1)*NaN; %this way it also works with3dconformal - Z = ones(size(stf(i).ray,2),1)*NaN; - - for j=1:size(stf(i).ray,2) - X(j) = stf(i).ray(j).rayPos_bev(:,1); - Z(j) = stf(i).ray(j).rayPos_bev(:,3); - end - - % sort bixels into matrix - minX = min(X); - maxX = max(X); - minZ = min(Z); - maxZ = max(Z); - - dimOfFluenceMxX = (maxX-minX)/stf(i).bixelWidth + 1; - dimOfFluenceMxZ = (maxZ-minZ)/stf(i).bixelWidth + 1; - - %Create the fluence matrix. - fluenceMx = zeros(dimOfFluenceMxZ,dimOfFluenceMxX); - - % Calculate X and Z position of every fluence's matrix spot - % z axis = axis of leaf movement! - xPos = (X-minX)/stf(i).bixelWidth+1; - zPos = (Z-minZ)/stf(i).bixelWidth+1; - - % Make subscripts for fluence matrix - indInFluenceMx = zPos + (xPos-1)*dimOfFluenceMxZ; - - %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); - - 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 - - % start sequencer - while max(D_k(:) > 0) - - %calculate the difference matrix diffMat - diffMat = diff([zeros(size(D_k,1),1) D_k zeros(size(D_k,1),1)],[],2); - - %calculate complexities - c = sum(max(0,diffMat),2); %TNMU-row-complexity - com = max(c); %TNMU complexity - g = com - c; %row complexity gap - - %initialize segment - segment = zeros(size(D_k)); - - k = k + 1; - - %Plot residual intensity matrix. - if visBool - seqSubPlots(2) = subplot(2,2,2,'parent',seqFig); - imagesc(D_k,'parent',seqSubPlots(2)); - set(seqSubPlots(2),'CLim',[0 numOfLevels],'YDir','normal'); - title(seqSubPlots(2),['k = ' num2str(k)]); - colorbar - drawnow - end - - - %loop over all rows - for j=1:size(D_0,1) - - %determine essential intervals - data(j).left(1) = 0; %left interval limit, actual for an empty interval - data(j).right(1) = 0; %right interal limit, actual for an empty interval - data(j).v(1) = g(j); %greatest number such that the inequalities (6) resp. (7) is satisfied with u=v - data(j).w(1) = inf; %smallest number in the interval - data(j).u(1) = data(j).v(1); %min(v,w) - - [~, pos, ~] = find(diffMat(j,:) > 0); % indices of all positive elements in the j. row of diffmat - [~, neg, ~] = find(diffMat(j,:) < 0); % indices of all negative elements in the j. row of diffMat - - n=2; - - %loop over the positive elements in the j. row of diffmat -> - %possible left interval limits - for m=1:size(pos,2) - - %loop over the negative elements in the j. row of diffMat -> - %possible right interval limit - for l=1:size(neg,2) - - %take only intervals I=[l,r] with l<=r - if pos(m) <= neg(l)-1 - - %set interval limits - data(j).left(n) = pos(m); - data(j).right(n) = neg(l)-1; - - %calculate v according to Lemma 8 - if g(j) <= abs( diffMat(j,pos(m)) + diffMat(j,neg(l)) ) - data(j).v(n) = min( diffMat(j,pos(m)), -diffMat(j,neg(l)) ) + g(j); - else - data(j).v(n) = ( diffMat(j, pos(m)) - diffMat(j, neg(l)) + g(j)) / 2; - end - - %calculate w and u according to equality (11) and - %(12) - data(j).w(n) = min(D_k(j,pos(m):(neg(l)-1))); - data(j).u(n) = min(data(j).v(n), data(j).w(n)); - - n = n+1; - end - end - end - - u(j) = max(data(j).u); - - end - - %calculate u_max from theorem 9 - d_k = min(u); - - %loop over all rows - for j=1:size(D_0,1) - - %find all possible (and essential) intervals - candidate = find(data(j).u >= d_k); - - %calculate the potential of the possible intervals - - %initialize p as -Inf - data(j).p(1:length(data(j).left)) = -Inf; - - %loop over all possible intervals - for s=1:size(candidate,2) - - if (s==1 && data(j).left(candidate(s)) == 0) - data(j).p(candidate(1)) = 0; - - - else - %calculate p1 according to equality (17) - if (d_k == diffMat(j, data(j).left(candidate(s))) && d_k ~= D_k(j, data(j).left(candidate(s)))) - p1 = 1; - - else - p1 = 0; - - end - - %calculate p2 according to equalitiy (18) - % if data(j).right(candidate(s)) < size(D_0, 2) - - if (d_k == -diffMat(j, data(j).right(candidate(s))+1) && d_k ~= D_k(j, data(j).right(candidate(s)))) - p2 = 1; - else - p2 = 0; - end - -% else -% -% if d_k == -diffMat(j, data(j).right(candidate(s))+1) -% p2 = 1; -% else -% p2 = 0; -% end -% -% end - - %calculate p3 according to equality (19) - p3 = size(find(D_k(j, data(j).left(candidate(s)):data(j).right(candidate(s))) == d_k),2); - - data(j).p(candidate(s)) = p1 + p2+ p3; - - end - - end - - %determinate intervals with maximum potential - maxPot = find(data(j).p == max(data(j).p)); - - %if several intervals have maximum potential, select - %the interval which has maximum length - if size(maxPot,2) > 1 - - for t=1:size(maxPot,2) - if t==1 && data(j).left(maxPot(t)) == 0 - data(j).l(1) = 0; - else - data(j).l(maxPot(t)) = data(j).right(maxPot(t)) - data(j).left(maxPot(t)) + 1; - end - end - - %data(j).l(maxPot) = data(j).right(maxPot) - data(j).left(maxPot) + 1; - - maxLength = find(data(j).l == max(data(j).l)); - - %left and right interval limits of the selected - %interval - leftIntLimit(j) = data(j).left(maxLength(1)); - rightIntLimit(j) = data(j).right(maxLength(1)); - - - else - - %left and right interval limits of the selected - %interval - leftIntLimit(j) = data(j).left(maxPot); - rightIntLimit(j) = data(j).right(maxPot); - - - end - - %create segment associated by the selected interval - if leftIntLimit(j) ~= 0 - - segment(j,leftIntLimit(j):rightIntLimit(j)) = 1; - - end - - end - - %write the segment in shape_k - shape_k = segment; - - %show the leaf positions - if visBool - seqSubPlots(4) = subplot(2,2,3.5,'parent',seqFig); - imagesc(shape_k,'parent',seqSubPlots(4)); - hold(seqSubPlots(4),'on'); - set(seqSubPlots(4),'YDir','normal') - xlabel(seqSubPlots(4),'x - direction parallel to leaf motion ') - ylabel(seqSubPlots(4),'z - direction perpendicular to leaf motion ') - title(seqSubPlots(4),['beam # ' num2str(i) ' shape # ' num2str(k) ' d_k = ' num2str(d_k)]); - for j = 1:dimOfFluenceMxZ - leftLeafIx = find(shape_k(j,:)>0,1,'first'); - rightLeafIx = find(shape_k(j,:)>0,1,'last'); - if leftLeafIx > 1 - plot(seqSubPlots(4),[.5 leftLeafIx-.5],j-[.5 .5] ,'w','LineWidth',2) - plot(seqSubPlots(4),[.5 leftLeafIx-.5],j+[.5 .5] ,'w','LineWidth',2) - plot(seqSubPlots(4),[ leftLeafIx-.5 leftLeafIx-.5],j+[.5 -.5] ,'w','LineWidth',2) - end - if rightLeafIx0 - - 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; - - 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); - end - - sequencing.w(1+offset:numOfRaysPerBeam+offset,1) = D_0(indInFluenceMx)/numOfLevels*calFac; - - offset = offset + numOfRaysPerBeam; - -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 -resultGUI.physicalDose = matRad_interp3(dij.doseGrid.x,dij.doseGrid.y',dij.doseGrid.z, ... - doseSequencedDoseGrid, ... - dij.ctGrid.x,dij.ctGrid.y',dij.ctGrid.z); - -% if weights exists from an former DAO remove it -if isfield(resultGUI,'wDao') - resultGUI = rmfield(resultGUI,'wDao'); -end - -end - diff --git a/matRad/sequencing/matRad_sequencing2ApertureInfo.m b/matRad/sequencing/matRad_sequencing2ApertureInfo.m deleted file mode 100644 index 867cabd43..000000000 --- a/matRad/sequencing/matRad_sequencing2ApertureInfo.m +++ /dev/null @@ -1,179 +0,0 @@ -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) -% -% input -% Sequencing: matRad sequencing result struct -% stf: matRad steering information 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/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. -% -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -% 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 -bixelIndOffset = 0; % used for creation of bixel index maps -totalNumOfBixels = sum([stf(:).totalNumOfBixels]); -totalNumOfShapes = sum([Sequencing.beam.numOfShapes]); -vectorOffset = totalNumOfShapes + 1; % used for bookkeeping in the vector for optimization - -% loop over all beams -for i=1:size(stf,2) - - %% 1. read stf and create maps (Ray & Bixelindex) - - % get x- and z-coordinates of bixels - rayPos_bev = reshape([stf(i).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(i).bixelWidth + 1; - dimZ = (maxZ-minZ)/stf(i).bixelWidth + 1; - - rayMap = zeros(dimZ,dimX); - - % get indices for x and z positions - xPos = (X-minX)/stf(i).bixelWidth + 1; - zPos = (Z-minZ)/stf(i).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(i).numOfRays] + bixelIndOffset; - bixelIndOffset = bixelIndOffset + stf(i).numOfRays; - - % store physical position of first entry in bixelIndMap - posOfCornerBixel = [minX 0 minZ]; - - % get leaf limits from the leaf map - lim_l = NaN * ones(dimZ,1); - lim_r = NaN * ones(dimZ,1); - % looping oder leaf pairs - for l = 1:dimZ - lim_lInd = find(rayMap(l,:),1,'first'); - lim_rInd = find(rayMap(l,:),1,'last'); - % the physical position [mm] can be calculated from the indices - lim_l(l) = (lim_lInd-1)*bixelWidth + minX - 1/2*bixelWidth; - lim_r(l) = (lim_rInd-1)*bixelWidth + minX + 1/2*bixelWidth; - end - - % get leaf positions for all shapes - % leaf positions can be extracted from the shapes created in Sequencing - 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; - - 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; - apertureInfo.beam(i).shape(m).vectorOffset = vectorOffset; - - % update index for bookkeeping - vectorOffset = vectorOffset + dimZ; - - 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]; - - % save data for each beam - apertureInfo.beam(i).numOfShapes = Sequencing.beam(i).numOfShapes; - apertureInfo.beam(i).numOfActiveLeafPairs = dimZ; - apertureInfo.beam(i).leafPairPos = leafPairPos; - apertureInfo.beam(i).isActiveLeafPair = isActiveLeafPair; - apertureInfo.beam(i).centralLeafPair = centralLeafPair; - apertureInfo.beam(i).lim_l = lim_l; - apertureInfo.beam(i).lim_r = lim_r; - apertureInfo.beam(i).bixelIndMap = bixelIndMap; - apertureInfo.beam(i).posOfCornerBixel = posOfCornerBixel; - apertureInfo.beam(i).MLCWindow = MLCWindow; - -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]'); - -% create vectors for optimization -[apertureInfo.apertureVector, apertureInfo.mappingMx, apertureInfo.limMx] = matRad_OptimizationProblemDAO.matRad_daoApertureInfo2Vec(apertureInfo); - -end diff --git a/matRad/sequencing/matRad_siochiLeafSequencing.m b/matRad/sequencing/matRad_siochiLeafSequencing.m deleted file mode 100644 index 02841d5b0..000000000 --- a/matRad/sequencing/matRad_siochiLeafSequencing.m +++ /dev/null @@ -1,379 +0,0 @@ -function resultGUI = matRad_siochiLeafSequencing(resultGUI,stf,dij,numOfLevels,visBool) -% multileaf collimator leaf sequencing algorithm -% for intensity modulated beams with multiple static segments according to -% Siochi (1999)International Journal of Radiation Oncology * Biology * Physics, -% originally implemented in PLUNC (https://sites.google.com/site/planunc/) -% -% Implemented in matRad by Eric Christiansen, Emily Heath, and Tong Xu -% -% call -% resultGUI = -% matRad_siochiLeafSequencing(resultGUI,stf,dij,numOfLevels) -% resultGUI = -% matRad_siochiLeafSequencing(resultGUI,stf,dij,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) -% -% output -% resultGUI: matRad result struct containing the new dose cube -% as well as the corresponding weights -% -% References -% [1] https://www.ncbi.nlm.nih.gov/pubmed/10078655 -% -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% -% 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/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. -% -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -% if visBool not set toogle off visualization -if nargin < 5 - visBool = 0; -end - -numOfBeams = numel(stf); - -if visBool - % create the sequencing figure - sz = [800 1000]; % figure size - 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)]); -end - -offset = 0; - -if ~isfield(resultGUI,'wUnsequenced') - wUnsequenced = resultGUI.w; -else - wUnsequenced = resultGUI.wUnsequenced; -end - -for i = 1:numOfBeams - - numOfRaysPerBeam = stf(i).numOfRays; - - % get relevant weights for current beam - wOfCurrBeams = wUnsequenced(1+offset:numOfRaysPerBeam+offset).* ones(size(stf(i).ray,2),1);%REVIEW OFFSET - - X = ones(size(stf(i).ray,2),1)*NaN; %this way it also works with3dconformal - Z = ones(size(stf(i).ray,2),1)*NaN; - - for j = 1:size(stf(i).ray,2) - X(j) = stf(i).ray(j).rayPos_bev(:,1); - Z(j) = stf(i).ray(j).rayPos_bev(:,3); - end - - % sort bixels into matrix - minX = min(X); - maxX = max(X); - minZ = min(Z); - maxZ = max(Z); - - dimOfFluenceMxX = (maxX-minX)/stf(i).bixelWidth + 1; - dimOfFluenceMxZ = (maxZ-minZ)/stf(i).bixelWidth + 1; - - %Create the fluence matrix. - fluenceMx = zeros(dimOfFluenceMxZ,dimOfFluenceMxX); - - % Calculate X and Z position of every fluence's matrix spot z axis = - % axis of leaf movement! - xPos = (X-minX)/stf(i).bixelWidth+1; - zPos = (Z-minZ)/stf(i).bixelWidth+1; - - % Make subscripts for fluence matrix - indInFluenceMx = zPos + (xPos-1)*dimOfFluenceMxZ; - - %Save weights in fluence matrix. - fluenceMx(indInFluenceMx) = wOfCurrBeams; - - % Stratification - calFac = max(fluenceMx(:)); - D_k = round(fluenceMx/calFac*numOfLevels); - - % Save the stratification in the initial intensity matrix D_0. - D_0 = D_k; - - % container to remember generated shapes; allocate space for 10000 - % shapes - shapes = NaN*ones(dimOfFluenceMxZ,dimOfFluenceMxX,10000); - shapesWeight = zeros(10000,1); - k = 0; - - if visBool - clf(seqFig); - colormap(seqFig,'jet'); - - seqSubPlots(1) = subplot(2,2,1,'parent',seqFig); - imagesc(D_k,'parent',seqSubPlots(1)); - set(seqSubPlots(1),'CLim',[0 numOfLevels],'YDir','normal'); - title(seqSubPlots(1),['Beam # ' num2str(i) ': max(D_0) = ' num2str(max(D_0(:))) ' - ' num2str(numel(unique(D_0))) ' intensity levels']); - xlabel(seqSubPlots(1),'x - direction parallel to leaf motion ') - ylabel(seqSubPlots(1),'z - direction perpendicular to leaf motion ') - colorbar; - drawnow - 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); - - 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 - - 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 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 - offset = offset + numOfRaysPerBeam; - -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 -resultGUI.physicalDose = matRad_interp3(dij.doseGrid.x,dij.doseGrid.y',dij.doseGrid.z, ... - doseSequencedDoseGrid, ... - dij.ctGrid.x,dij.ctGrid.y',dij.ctGrid.z); - -% if weights exists from an former DAO remove it -if isfield(resultGUI,'wDao') - resultGUI = rmfield(resultGUI,'wDao'); -end - -end - -function [tops, bases] = matRad_siochiDecomposePort(map,dimZ,dimX,minZ,maxZ,minX,maxX) -%Returns tops and bases of a fluence matrix "map" for Siochi leaf -%sequencing algorithm (rod pushing part). Accounts for collisions and -%tongue and groove (Tng) effects. - -tops = zeros(dimZ, dimX); -bases = zeros(dimZ, dimX); - -for i = minX:maxX - maxTop = -1; - TnG = 1; - for j = minZ:maxZ - if i == minX - bases(j,i) = 1; - tops(j,i) = bases(j,i)+map(j,i)-1; - else %assign trial base positions - if map(j,i) >= map(j,i-1) %current rod >= previous, match the bases - bases(j,i) = bases(j,i-1); - tops(j,i) = bases(j,i)+map(j,i)-1; - else %current rod maxTop - maxTop = tops(j,i); - maxRow = j; - end - end - - %Correct for collision and tongue and groove error - while(TnG) - %go from maxRow down checking for TnG. This occurs when a shorter - %rod is "peeking over" a longer one in the direction transverse to - %the leaf motion. To fix this, match either the tops or bases of - %the rods. - for j = (maxRow-1):-1:minZ - if map(j,i) < map(j+1,i) - if tops(j,i) > tops(j+1,i) - tops(j+1,i) = tops(j,i); - bases(j+1,i) = tops(j+1,i)-map(j+1,i)+1; - elseif bases(j,i) < bases(j+1,i) - bases(j,i) = bases(j+1,i); - tops(j,i) = bases(j,i)+map(j,i)-1; - end - else - if tops(j,i) < tops(j+1,i) - tops(j,i) = tops(j+1,i); - bases(j,i) = tops(j,i)-map(j,i)+1; - elseif bases(j,i) > bases(j+1,i) - bases(j+1,i) = bases(j,i); - tops(j+1,i) = bases(j+1,i)+map(j+1,i)-1; - end - end - end - %go from maxRow up checking for TnG - for j = (maxRow+1):maxZ - if map(j,i) < map(j-1,i) - if tops(j,i) > tops(j-1,i) - tops(j-1,i) = tops(j,i); - bases(j-1,i) = tops(j-1,i)-map(j-1,i)+1; - elseif bases(j,i) < bases(j-1,i) - bases(j,i) = bases(j-1,i); - tops(j,i) = bases(j,i)+map(j,i)-1; - end - else - if tops(j,i) < tops(j-1,i) - tops(j,i) = tops(j-1,i); - bases(j,i) = tops(j,i)-map(j,i)+1; - elseif bases(j,i) > bases(j-1,i) - bases(j-1,i) = bases(j,i); - tops(j-1,i) = bases(j-1,i)+map(j-1,i)-1; - end - end - end - %now check if all TnG conditions have been removed - TnG = 0; - for j = (minZ+1):maxZ - if map(j,i) < map(j-1,i); - if tops(j,i) > tops(j-1,i) - TnG = 1; - elseif bases(j,i) < bases(j-1,i) - TnG = 1; - end - else - if tops(j,i) < tops(j-1,i) - TnG = 1; - elseif bases(j,i) > bases(j-1,i) - TnG = 1; - end - end - end - end -end - -end - -function [shapes,shapesWeight,k,D_k] = matRad_siochiConvertToSegments(shapes,shapesWeight,k,tops,bases,visBool,i,D_k,numOfLevels,seqFig,seqSubPlots) -%Convert tops and bases to shape matrices. These are taken as to be the -%shapes of uniform level/elevation after the rods are pushed. -if nargin < 6 - visBool = 0; -end - - -levels = max(tops(:)); - -for level = 1:levels - %check if slab is new - if matRad_siochiDifferentSlab(tops,bases,level) - k = k+1; %increment number of unique slabs - shape_k = (bases <= level).*(level <= tops); %shape of current slab - shapes(:,:,k) = shape_k; - end - shapesWeight(k) = shapesWeight(k)+1; %if slab is not unique, this increments weight again - - if visBool - %show the leaf positions - [dimZ,dimX] = size(tops); - seqSubPlots(4) = subplot(2,2,3.5,'parent',seqFig); - imagesc(shape_k,'parent',seqSubPlots(4)); - hold(seqSubPlots(4),'on'); - set(seqSubPlots(4),'YDir','normal') - xlabel(seqSubPlots(4),'x - direction parallel to leaf motion ') - ylabel(seqSubPlots(4),'z - direction perpendicular to leaf motion ') - title(seqSubPlots(4),['beam # ' num2str(i) ' shape # ' num2str(k) ' d_k = ' num2str(shapesWeight(k))]); - for j = 1:dimZ - leftLeafIx = find(shape_k(j,:)>0,1,'first'); - rightLeafIx = find(shape_k(j,:)>0,1,'last'); - if leftLeafIx > 1 - plot(seqSubPlots(4),[.5 leftLeafIx-.5],j-[.5 .5] ,'w','LineWidth',2) - plot(seqSubPlots(4),[.5 leftLeafIx-.5],j+[.5 .5] ,'w','LineWidth',2) - plot(seqSubPlots(4),[ leftLeafIx-.5 leftLeafIx-.5],j+[.5 -.5] ,'w','LineWidth',2) - end - if rightLeafIx 0 - - k = k + 1; - - %Plot residual intensity matrix. - if visBool - seqSubPlots(2) = subplot(2,2,2,'parent',seqFig); - imagesc(D_k,'parent',seqSubPlots(2)); - set(seqSubPlots(2),'CLim',[0 L_0],'YDir','normal'); - title(seqSubPlots(2),['k = ' num2str(k) ' - ' num2str(numel(unique(D_k))) ' intensity levels remaining...']); - xlabel(seqSubPlots(2),'x - direction parallel to leaf motion '); - ylabel(seqSubPlots(2),'z - direction perpendicular to leaf motion '); - colorbar - drawnow - end - - %Rounded off integer. Equation 7. - m = floor(log2(L_k)); - - % Convert m=1 if is less than 1. This happens when L_k belong to ]0,2[ - if m < 1 - m = 1; - end - - %Calculate the delivery intensity unit. Equation 6. - d_k = floor(2^(m-1)); - - % Opening matrix. - openingMx = D_k >= d_k; - - % Plot opening matrix. - if visBool - seqSubPlots(3) = subplot(2,2,3,'parent',seqFig); - imagesc(openingMx,'parent',seqSubPlots(3)); - set(seqSubPlots(3),'YDir','normal') - xlabel(seqSubPlots(3),'x - direction parallel to leaf motion ') - ylabel(seqSubPlots(3),'z - direction perpendicular to leaf motion ') - title(seqSubPlots(3),'Opening matrix'); - drawnow - end - - if strcmp(mode,'sw') % sliding window technique! - for j = 1:dimOfFluenceMxZ - openIx = find(openingMx(j,:) == 1,1,'first'); - if ~isempty(openIx) - closeIx = find(openingMx(j,openIx+1:end) == 0,1,'first'); - if ~isempty(closeIx) - openingMx(j,openIx+closeIx:end) = 0; - end - end - - end - elseif strcmp(mode,'rl') % reducing levels technique! - for j = 1:dimOfFluenceMxZ - [maxVal,maxIx] = max(openingMx(j,:) .* D_k(j,:)); - if maxVal > 0 - closeIx = maxIx + find(openingMx(j,maxIx+1:end) == 0,1,'first'); - if ~isempty(closeIx) - openingMx(j,closeIx:end) = 0; - end - openIx = find(openingMx(j,1:maxIx-1) == 0,1,'last'); - if ~isempty(openIx) - openingMx(j,1:openIx) = 0; - end - end - - end - - end - - shape_k = openingMx * d_k; - - if visBool - seqSubPlots(4) = subplot(2,2,4,'parent',seqFig); - imagesc(shape_k,'parent',seqSubPlots(4)); - set(seqSubPlots(4),'YDir','normal') - hold(seqSubPlots(4),'on'); - xlabel(seqSubPlots(4),'x - direction parallel to leaf motion ') - ylabel(seqSubPlots(4),'z - direction perpendicular to leaf motion ') - title(seqSubPlots(4),['beam # ' num2str(i) ' shape # ' num2str(k) ' d_k = ' num2str(d_k)]); - for j = 1:dimOfFluenceMxZ - leftLeafIx = find(shape_k(j,:)>0,1,'first'); - rightLeafIx = find(shape_k(j,:)>0,1,'last'); - if leftLeafIx > 1 - plot(seqSubPlots(4),[.5 leftLeafIx-.5],j-[.5 .5] ,'w','LineWidth',2) - plot(seqSubPlots(4),[.5 leftLeafIx-.5],j+[.5 .5] ,'w','LineWidth',2) - plot(seqSubPlots(4),[ leftLeafIx-.5 leftLeafIx-.5],j+[.5 -.5] ,'w','LineWidth',2) - end - if rightLeafIx0 - - 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; - - 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); - end - - - sequencing.w(1+offset:numOfRaysPerBeam+offset,1) = D_0(indInFluenceMx)/numOfLevels*calFac; - - offset = offset + numOfRaysPerBeam; - -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 -resultGUI.physicalDose = matRad_interp3(dij.doseGrid.x,dij.doseGrid.y',dij.doseGrid.z, ... - doseSequencedDoseGrid, ... - dij.ctGrid.x,dij.ctGrid.y',dij.ctGrid.z); - -% if weights exists from an former DAO remove it -if isfield(resultGUI,'wDao') - resultGUI = rmfield(resultGUI,'wDao'); -end - -end - From b777135b2a18f810fae00ac36408497a78bbe61d Mon Sep 17 00:00:00 2001 From: JenHardt <84377305+JenHardt@users.noreply.github.com> Date: Fri, 27 Feb 2026 10:50:36 +0100 Subject: [PATCH 06/20] updated 3d conformal --- .../matRad_SequencingPhotonsAbstract.m | 490 +++++++++--------- .../matRad_SequencingPhotonsEngelLeaf.m | 440 ++++++++-------- .../matRad_SequencingPhotonsSiochiLeaf.m | 288 +++++----- .../matRad_SequencingPhotonsXiaLeaf.m | 188 +++---- 4 files changed, 722 insertions(+), 684 deletions(-) diff --git a/matRad/sequencing/matRad_SequencingPhotonsAbstract.m b/matRad/sequencing/matRad_SequencingPhotonsAbstract.m index cb0d63bea..d58c876a9 100644 --- a/matRad/sequencing/matRad_SequencingPhotonsAbstract.m +++ b/matRad/sequencing/matRad_SequencingPhotonsAbstract.m @@ -1,189 +1,191 @@ classdef (Abstract) matRad_SequencingPhotonsAbstract < matRad_SequencingBase - %UNTITLED Summary of this class goes here + % UNTITLED Summary of this class goes here % Detailed explanation goes her properties - numOfMLCLeafPairs = 80; - sequencingLevel = 5; + numOfMLCLeafPairs = 80 + sequencingLevel = 5 end methods - function sequence = sequence(this,w,stf) + function sequence = sequence(this, w, stf) - throw(MException('MATLAB:class:AbstractMember','Abstract function sequence needs to be implemented!')) + throw(MException('MATLAB:class:AbstractMember', 'Abstract function sequence needs to be implemented!')); end - function [D_0,D_k, shapes,calFac, indInMx] = initBeam(this,stf, wCurr) + function [D_0, D_k, shapes, calFac, indInMx] = initBeam(this, stf, wCurr) + + numOfRaysPerBeam = size(stf.ray, 2); + X = ones(numOfRaysPerBeam, 1) * NaN; + Z = ones(numOfRaysPerBeam, 1) * NaN; + + for j = 1:numOfRaysPerBeam + X(j) = stf.ray(j).rayPos_bev(:, 1); + Z(j) = stf.ray(j).rayPos_bev(:, 3); + end + + % sort bixels into matrix + minX = min(X); + maxX = max(X); + minZ = min(Z); + maxZ = max(Z); + + dimOfFluenceMxX = (maxX - minX) / stf.bixelWidth + 1; + dimOfFluenceMxZ = (maxZ - minZ) / stf.bixelWidth + 1; + + % Create the fluence matrix. + fluenceMx = zeros(dimOfFluenceMxZ, dimOfFluenceMxX); + + % Calculate X and Z position of every fluence's matrix spot z axis = + % axis of leaf movement! + xPos = (X - minX) / stf.bixelWidth + 1; + zPos = (Z - minZ) / stf.bixelWidth + 1; + + % Make subscripts for fluence matrix + indInMx = zPos + (xPos - 1) * dimOfFluenceMxZ; + + % Save weights in fluence matrix. + fluenceMx(indInMx) = wCurr .* ones(numOfRaysPerBeam, 1); + + % Stratification + calFac = max(fluenceMx(:)); + D_k = round(fluenceMx / calFac * this.sequencingLevel); + + % 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); - numOfRaysPerBeam = stf.numOfRays; - X = ones(numOfRaysPerBeam,1)*NaN; - Z = ones(numOfRaysPerBeam,1)*NaN; - - for j = 1:stf.numOfRays - X(j) = stf.ray(j).rayPos_bev(:,1); - Z(j) = stf.ray(j).rayPos_bev(:,3); - end - - % sort bixels into matrix - minX = min(X); - maxX = max(X); - minZ = min(Z); - maxZ = max(Z); - - dimOfFluenceMxX = (maxX-minX)/stf.bixelWidth + 1; - dimOfFluenceMxZ = (maxZ-minZ)/stf.bixelWidth + 1; - - %Create the fluence matrix. - fluenceMx = zeros(dimOfFluenceMxZ,dimOfFluenceMxX); - - % Calculate X and Z position of every fluence's matrix spot z axis = - % axis of leaf movement! - xPos = (X-minX)/stf.bixelWidth+1; - zPos = (Z-minZ)/stf.bixelWidth+1; - - % Make subscripts for fluence matrix - indInMx = zPos + (xPos-1)*dimOfFluenceMxZ; - - %Save weights in fluence matrix. - fluenceMx(indInMx) = wCurr; - - % Stratification - calFac = max(fluenceMx(:)); - D_k = round(fluenceMx/calFac*this.sequencingLevel); - - % 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); - end - function sequence = sequencing2ApertureInfo(this,sequence,stf) + function sequence = sequencing2ApertureInfo(this, sequence, stf) % MLC parameters: bixelWidth = stf(1).bixelWidth; % [mm] % 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(this.numOfMLCLeafPairs/2); - + centralLeafPair = ceil(this.numOfMLCLeafPairs / 2); + % initializing variables bixelIndOffset = 0; % used for creation of bixel index maps totalNumOfBixels = sum([stf(:).totalNumOfBixels]); totalNumOfShapes = sum([sequence.beam.numOfShapes]); vectorOffset = totalNumOfShapes + 1; % used for bookkeeping in the vector for optimization - + % loop over all beams - for i=1:size(stf,2) - + for i = 1:size(stf, 2) + %% 1. read stf and create maps (Ray & Bixelindex) - + % get x- and z-coordinates of bixels - rayPos_bev = reshape([stf(i).ray.rayPos_bev],3,[]); - X = rayPos_bev(1,:)'; - Z = rayPos_bev(3,:)'; - + rayPos_bev = reshape([stf(i).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(i).bixelWidth + 1; - dimZ = (maxZ-minZ)/stf(i).bixelWidth + 1; - - rayMap = zeros(dimZ,dimX); - + 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 - xPos = (X-minX)/stf(i).bixelWidth + 1; - zPos = (Z-minZ)/stf(i).bixelWidth + 1; - + xPos = (X - minX) / stf(i).bixelWidth + 1; + zPos = (Z - minZ) / stf(i).bixelWidth + 1; + % get indices in the ray-map - indInRay = zPos + (xPos-1)*dimZ; - + indInRay = zPos + (xPos - 1) * dimZ; + % fill ray-map rayMap(indInRay) = 1; - + % create map of bixel indices - bixelIndMap = NaN * ones(dimZ,dimX); + bixelIndMap = NaN * ones(dimZ, dimX); bixelIndMap(indInRay) = [1:stf(i).numOfRays] + bixelIndOffset; bixelIndOffset = bixelIndOffset + stf(i).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); + lim_l = NaN * ones(dimZ, 1); + lim_r = NaN * ones(dimZ, 1); % looping oder leaf pairs for l = 1:dimZ - lim_lInd = find(rayMap(l,:),1,'first'); - lim_rInd = find(rayMap(l,:),1,'last'); + 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; + 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:sequence.beam(i).numOfShapes - + % loading shape from Sequencing result - shapeMap = sequence.beam(i).shapes(:,:,m); + shapeMap = sequence.beam(i).shapes(:, :, m); % get left and right leaf indices from shapemap % initializing limits - leftLeafPos = NaN * ones(dimZ,1); - rightLeafPos = NaN * ones(dimZ,1); + 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'); - + 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; + 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; - + % 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; + end end - + % save data for each shape of this beam sequence.apertureInfo.beam(i).shape(m).leftLeafPos = leftLeafPos; sequence.apertureInfo.beam(i).shape(m).rightLeafPos = rightLeafPos; sequence.apertureInfo.beam(i).shape(m).weight = sequence.beam(i).shapesWeight(m); sequence.apertureInfo.beam(i).shape(m).shapeMap = shapeMap; sequence.apertureInfo.beam(i).shape(m).vectorOffset = vectorOffset; - + % update index for bookkeeping vectorOffset = vectorOffset + dimZ; - + end - - % z-coordinates of active leaf pairs + + % 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; - + + topLeafPair = centralLeafPair - topLeafPairPos / bixelWidth; + bottomLeafPair = centralLeafPair - bottomLeafPairPos / bixelWidth; + % create bool map of active leaf pairs - isActiveLeafPair = zeros(this.numOfMLCLeafPairs,1); + isActiveLeafPair = zeros(this.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]; - + MLCWindow = [minX - bixelWidth / 2 maxX + bixelWidth / 2 ... + minZ - bixelWidth / 2 maxZ + bixelWidth / 2]; + % save data for each beam sequence.apertureInfo.beam(i).numOfShapes = sequence.beam(i).numOfShapes; sequence.apertureInfo.beam(i).numOfActiveLeafPairs = dimZ; @@ -195,114 +197,114 @@ sequence.apertureInfo.beam(i).bixelIndMap = bixelIndMap; sequence.apertureInfo.beam(i).posOfCornerBixel = posOfCornerBixel; sequence.apertureInfo.beam(i).MLCWindow = MLCWindow; - + end - + % save global data sequence.apertureInfo.bixelWidth = bixelWidth; sequence.apertureInfo.numOfMLCLeafPairs = this.numOfMLCLeafPairs; sequence.apertureInfo.totalNumOfBixels = totalNumOfBixels; sequence.apertureInfo.totalNumOfShapes = sum([sequence.apertureInfo.beam.numOfShapes]); - sequence.apertureInfo.totalNumOfLeafPairs = sum([sequence.apertureInfo.beam.numOfShapes]*[sequence.apertureInfo.beam.numOfActiveLeafPairs]'); - + sequence.apertureInfo.totalNumOfLeafPairs = sum([sequence.apertureInfo.beam.numOfShapes] * [sequence.apertureInfo.beam.numOfActiveLeafPairs]'); + % create vectors for optimization [sequence.apertureInfo.apertureVector, sequence.apertureInfo.mappingMx, sequence.apertureInfo.limMx] = matRad_OptimizationProblemDAO.matRad_daoApertureInfo2Vec(sequence.apertureInfo); end - - function plotSegments(this,sequencing) - % create the sequencing figure - sz = [800 1000]; % figure size - 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)]); - - for i = 1:numel(sequencing) - - D_0 = sequencing.beam(i).fluence; - - clf(seqFig); - colormap(seqFig,'jet'); - - seqSubPlots(1) = subplot(2,2,1,'parent',seqFig); - imagesc(sequencing.beam(i).fluence,'parent',seqSubPlots(1)); - set(seqSubPlots(1),'CLim',[0 this.sequencingLevel],'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 - - %show the leaf positions - D_k = sequencing.beam(i).fluence; - for k = 1:sequencing.beam(i).numOfShapes - shape_k = sequencing.beam(i).shapes(:,:,k); - [dimZ,dimX] = size(sequencing.beam(i).fluence); - seqSubPlots(4) = subplot(2,2,3.5,'parent',seqFig); - imagesc(shape_k,'parent',seqSubPlots(4)); - hold(seqSubPlots(4),'on'); - set(seqSubPlots(4),'YDir','normal') - xlabel(seqSubPlots(4),'x - direction parallel to leaf motion ') - ylabel(seqSubPlots(4),'z - direction perpendicular to leaf motion ') - title(seqSubPlots(4),['beam # ' num2str(i) ' shape # ' num2str(k) ' d_k = ' num2str(sequencing.beam(i).shapesWeight(k))]); - for j = 1:dimZ - leftLeafIx = find(shape_k(j,:)>0,1,'first'); - rightLeafIx = find(shape_k(j,:)>0,1,'last'); - if leftLeafIx > 1 - plot(seqSubPlots(4),[.5 leftLeafIx-.5],j-[.5 .5] ,'w','LineWidth',2) - plot(seqSubPlots(4),[.5 leftLeafIx-.5],j+[.5 .5] ,'w','LineWidth',2) - plot(seqSubPlots(4),[ leftLeafIx-.5 leftLeafIx-.5],j+[.5 -.5] ,'w','LineWidth',2) - end - if rightLeafIx 0, 1, 'last'); + if leftLeafIx > 1 + plot(seqSubPlots(4), [.5 leftLeafIx - .5], j - [.5 .5], 'w', 'LineWidth', 2); + plot(seqSubPlots(4), [.5 leftLeafIx - .5], j + [.5 .5], 'w', 'LineWidth', 2); + plot(seqSubPlots(4), [leftLeafIx - .5 leftLeafIx - .5], j + [.5 -.5], 'w', 'LineWidth', 2); + end + if rightLeafIx < dimX + plot(seqSubPlots(4), [dimX + .5 rightLeafIx + .5], j - [.5 .5], 'w', 'LineWidth', 2); + plot(seqSubPlots(4), [dimX + .5 rightLeafIx + .5], j + [.5 .5], 'w', 'LineWidth', 2); + plot(seqSubPlots(4), [rightLeafIx + .5 rightLeafIx + .5], j + [.5 -.5], 'w', 'LineWidth', 2); + end + if isempty(rightLeafIx) && isempty (leftLeafIx) + plot(seqSubPlots(4), [dimX + .5 .5], j - [.5 .5], 'w', 'LineWidth', 2); + plot(seqSubPlots(4), [dimX + .5 .5], j + [.5 .5], 'w', 'LineWidth', 2); + plot(seqSubPlots(4), .5 * dimX * [1 1] + [0.5], j + [.5 -.5], 'w', 'LineWidth', 2); end - pause(1); - - %Plot residual intensity matrix. - D_k = D_k-shape_k; %residual intensity matrix for visualization - seqSubPlots(2) = subplot(2,2,2,'parent',seqFig); - imagesc(D_k,'parent',seqSubPlots(2)); - set(seqSubPlots(2),'CLim',[0 this.sequencingLevel],'YDir','normal'); - title(seqSubPlots(2),['k = ' num2str(k)]); - colorbar - drawnow - - axis tight - drawnow end + pause(1); + % Plot residual intensity matrix. + D_k = D_k - shape_k; % residual intensity matrix for visualization + seqSubPlots(2) = subplot(2, 2, 2, 'parent', seqFig); + imagesc(D_k, 'parent', seqSubPlots(2)); + set(seqSubPlots(2), 'CLim', [0 this.sequencingLevel], 'YDir', 'normal'); + title(seqSubPlots(2), ['k = ' num2str(k)]); + colorbar; + drawnow; + axis tight; + drawnow; end - end + + end + end + end methods (Static) - function [available,msg] = isAvailable(pln,machine) - - if nargin < 2 - machine = matRad_loadMachine(pln); - end - %checkBasic - available = isfield(machine,'meta') && isfield(machine,'data'); - - available = available && any(isfield(machine.meta,{'machine','name'})); - - if ~available - msg = 'Your machine file is invalid and does not contain the basic field (meta/data/radiationMode)!'; - else - msg = []; - end + + function [available, msg] = isAvailable(pln, machine) + + if nargin < 2 + machine = matRad_loadMachine(pln); + end + % checkBasic + available = isfield(machine, 'meta') && isfield(machine, 'data'); + + available = available && any(isfield(machine.meta, {'machine', 'name'})); + + if ~available + msg = 'Your machine file is invalid and does not contain the basic field (meta/data/radiationMode)!'; + else + msg = []; + end end - function [pln,stf] = aperture2collimation(pln,stf,sequence, visBool) + function [pln, stf] = aperture2collimation(pln, stf, sequence, visBool) if nargin < 4 visBool = false; @@ -310,43 +312,43 @@ function plotSegments(this,sequencing) bixelWidth = sequence.apertureInfo.bixelWidth; leafWidth = bixelWidth; - convResolution = 0.5; %[mm] - - %The collimator limits are infered here from the apertureInfo. This could - %be handled differently by explicitly storing collimator info in the base - %data? + convResolution = 0.5; % [mm] + + % The collimator limits are infered here from the apertureInfo. This could + % be handled differently by explicitly storing collimator info in the base + % data? symmetricMLClimits = vertcat(apertureInfo.beam.MLCWindow); symmetricMLClimits = max(abs(symmetricMLClimits)); - fieldWidth = 2*max(symmetricMLClimits); - - %modify basic pln variables + fieldWidth = 2 * max(symmetricMLClimits); + + % modify basic pln variables pln.propStf.bixelWidth = 'field'; - pln.propStf.collimation.convResolution = 0.5; %[mm] + pln.propStf.collimation.convResolution = 0.5; % [mm] pln.propStf.collimation.fieldWidth = fieldWidth; pln.propStf.collimation.leafWidth = leafWidth; - + % - %[bixelFieldX,bixelFieldY] = ndgrid(-fieldWidth/2:bixelWidth:fieldWidth/2,-fieldWidth/2:leafWidth:fieldWidth/2); - [convFieldX,convFieldY] = meshgrid(-fieldWidth/2:convResolution:fieldWidth/2); - - %TODO: Not used in calcPhotonDose but imported from DICOM - %pln.propStf.collimation.Devices ... - %pln.propStf.collimation.numOfFields - %pln.propStf.collimation.beamMeterset - + % [bixelFieldX,bixelFieldY] = ndgrid(-fieldWidth/2:bixelWidth:fieldWidth/2,-fieldWidth/2:leafWidth:fieldWidth/2); + [convFieldX, convFieldY] = meshgrid(-fieldWidth / 2:convResolution:fieldWidth / 2); + + % TODO: Not used in calcPhotonDose but imported from DICOM + % pln.propStf.collimation.Devices ... + % pln.propStf.collimation.numOfFields + % pln.propStf.collimation.beamMeterset + for iBeam = 1:numel(stf) stfTmp = stf(iBeam); beamSequencing = sequence.beam(iBeam); beamAperture = sequence.apertureInfo.beam(iBeam); - + stfTmp.bixelWidth = 'field'; - + nShapes = beamSequencing.numOfShapes; - - stfTmp.numOfRays = 1;% + + stfTmp.numOfRays = 1; % stfTmp.numOfBixelsPerRay = nShapes; stfTmp.totalNumOfBixels = nShapes; - + ray = struct(); ray.rayPos_bev = [0 0 0]; ray.targetPoint_bev = [0 stfTmp.SAD 0]; @@ -354,36 +356,43 @@ function plotSegments(this,sequencing) ray.energy = stfTmp.ray(1).energy; ray.beamletCornersAtIso = stfTmp.ray(1).beamletCornersAtIso; ray.rayCorners_SCD = stfTmp.ray(1).rayCorners_SCD; - - %ray.shape = beamSequencing.sum; + + % ray.shape = beamSequencing.sum; shapeTotalF = zeros(size(convFieldX)); - + ray.shapes = struct(); for iShape = 1:nShapes currShape = beamAperture.shape(iShape); activeLeafPairPosY = beamAperture.leafPairPos; F = zeros(size(convFieldX)); if visBool - hF = figure; imagesc(F); title(sprintf('Beam %d, Shape %d',iBeam,iShape)); hold on; + hF = figure; + imagesc(F); + title(sprintf('Beam %d, Shape %d', iBeam, iShape)); + hold on; end for iLeafPair = 1:numel(activeLeafPairPosY) posY = activeLeafPairPosY(iLeafPair); - ixY = convFieldY >= posY-leafWidth/2 & convFieldY < posY + leafWidth/2; + ixY = convFieldY >= posY - leafWidth / 2 & convFieldY < posY + leafWidth / 2; ixX = convFieldX >= currShape.leftLeafPos(iLeafPair) & convFieldX < currShape.rightLeafPos(iLeafPair); - ix = ixX & ixY; + ix = ixX & ixY; F(ix) = 1; if visBool - figure(hF); imagesc(F); drawnow; pause(0.1); + figure(hF); + imagesc(F); + drawnow; + pause(0.1); end end - + if visBool - pause(1); close(hF); + pause(1); + close(hF); end - - F = F*currShape.weight; + + F = F * currShape.weight; shapeTotalF = shapeTotalF + F; - + ray.shapes(iShape).convFluence = F; ray.shapes(iShape).shapeMap = currShape.shapeMap; ray.shapes(iShape).weight = currShape.weight; @@ -391,16 +400,15 @@ function plotSegments(this,sequencing) ray.shapes(iShape).rightLeafPos = currShape.rightLeafPos; ray.shapes(iShape).leafPairCenterPos = activeLeafPairPosY; end - + ray.shape = shapeTotalF; - ray.weight = ones(1,nShapes); + ray.weight = ones(1, nShapes); ray.collimation = pln.propStf.collimation; stfTmp.ray = ray; - + stf(iBeam) = stfTmp; end end + end end - - diff --git a/matRad/sequencing/matRad_SequencingPhotonsEngelLeaf.m b/matRad/sequencing/matRad_SequencingPhotonsEngelLeaf.m index fb366521c..c1eac8d2c 100644 --- a/matRad/sequencing/matRad_SequencingPhotonsEngelLeaf.m +++ b/matRad/sequencing/matRad_SequencingPhotonsEngelLeaf.m @@ -1,294 +1,302 @@ classdef matRad_SequencingPhotonsEngelLeaf < matRad_SequencingPhotonsAbstract -% multileaf collimator leaf sequencing algorithm -% for intensity modulated beams with multiple static segments accroding -% to Engel et al. 2005 Discrete Applied Mathematics -% -% References -% [1] http://www.sciencedirect.com/science/article/pii/S0166218X05001411 -% -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% -% 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/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. -% -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + % multileaf collimator leaf sequencing algorithm + % for intensity modulated beams with multiple static segments accroding + % to Engel et al. 2005 Discrete Applied Mathematics + % + % References + % [1] http://www.sciencedirect.com/science/article/pii/S0166218X05001411 + % + % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + % + % 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/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 = 'Photons Engel Leaf Sequenceer'; - shortName = 'engel'; - possibleRadiationModes = {'photons'}; - end + name = 'Photons Engel Leaf Sequenceer' + shortName = 'engel' + possibleRadiationModes = {'photons'} + end methods - function sequence = sequence(this,w,stf) + function sequence = sequence(this, w, stf) matRad_cfg = MatRad_Config.instance(); numOfBeams = numel(stf); offset = 0; for i = 1:numOfBeams - [D_0,D_k, shapes,calFac,indInMx] = this.initBeam(stf(i),w(1+offset:stf(i).numOfRays+offset)); + [D_0, D_k, shapes, calFac, indInMx] = this.initBeam(stf(i), w(1 + offset:stf(i).numOfRays + offset)); + + k = 0; - k = 0; - % start sequencer while max(D_k(:) > 0) - - %calculate the difference matrix diffMat - diffMat = diff([zeros(size(D_k,1),1) D_k zeros(size(D_k,1),1)],[],2); - - %calculate complexities - c = sum(max(0,diffMat),2); %TNMU-row-complexity - com = max(c); %TNMU complexity - g = com - c; %row complexity gap - - %initialize segment + + % calculate the difference matrix diffMat + diffMat = diff([zeros(size(D_k, 1), 1) D_k zeros(size(D_k, 1), 1)], [], 2); + + % calculate complexities + c = sum(max(0, diffMat), 2); % TNMU-row-complexity + com = max(c); % TNMU complexity + g = com - c; % row complexity gap + + % initialize segment segment = zeros(size(D_k)); - + k = k + 1; - - - %loop over all rows - for j=1:size(D_0,1) - - %determine essential intervals - data(j).left(1) = 0; %left interval limit, actual for an empty interval - data(j).right(1) = 0; %right interal limit, actual for an empty interval - data(j).v(1) = g(j); %greatest number such that the inequalities (6) resp. (7) is satisfied with u=v - data(j).w(1) = inf; %smallest number in the interval - data(j).u(1) = data(j).v(1); %min(v,w) - - [~, pos, ~] = find(diffMat(j,:) > 0); % indices of all positive elements in the j. row of diffmat - [~, neg, ~] = find(diffMat(j,:) < 0); % indices of all negative elements in the j. row of diffMat - - n=2; - - %loop over the positive elements in the j. row of diffmat -> - %possible left interval limits - for m=1:size(pos,2) - - %loop over the negative elements in the j. row of diffMat -> - %possible right interval limit - for l=1:size(neg,2) - - %take only intervals I=[l,r] with l<=r - if pos(m) <= neg(l)-1 - - %set interval limits + + % loop over all rows + for j = 1:size(D_0, 1) + + % determine essential intervals + data(j).left(1) = 0; % left interval limit, actual for an empty interval + data(j).right(1) = 0; % right interal limit, actual for an empty interval + data(j).v(1) = g(j); % greatest number such that the inequalities (6) resp. (7) is satisfied with u=v + data(j).w(1) = inf; % smallest number in the interval + data(j).u(1) = data(j).v(1); % min(v,w) + + [~, pos, ~] = find(diffMat(j, :) > 0); % indices of all positive elements in the j. row of diffmat + [~, neg, ~] = find(diffMat(j, :) < 0); % indices of all negative elements in the j. row of diffMat + + n = 2; + + % loop over the positive elements in the j. row of diffmat -> + % possible left interval limits + for m = 1:size(pos, 2) + + % loop over the negative elements in the j. row of diffMat -> + % possible right interval limit + for l = 1:size(neg, 2) + + % take only intervals I=[l,r] with l<=r + if pos(m) <= neg(l) - 1 + + % set interval limits data(j).left(n) = pos(m); - data(j).right(n) = neg(l)-1; - - %calculate v according to Lemma 8 - if g(j) <= abs( diffMat(j,pos(m)) + diffMat(j,neg(l)) ) - data(j).v(n) = min( diffMat(j,pos(m)), -diffMat(j,neg(l)) ) + g(j); + data(j).right(n) = neg(l) - 1; + + % calculate v according to Lemma 8 + if g(j) <= abs(diffMat(j, pos(m)) + diffMat(j, neg(l))) + data(j).v(n) = min(diffMat(j, pos(m)), -diffMat(j, neg(l))) + g(j); else - data(j).v(n) = ( diffMat(j, pos(m)) - diffMat(j, neg(l)) + g(j)) / 2; + data(j).v(n) = (diffMat(j, pos(m)) - diffMat(j, neg(l)) + g(j)) / 2; end - - %calculate w and u according to equality (11) and - %(12) - data(j).w(n) = min(D_k(j,pos(m):(neg(l)-1))); + + % calculate w and u according to equality (11) and + % (12) + data(j).w(n) = min(D_k(j, pos(m):(neg(l) - 1))); data(j).u(n) = min(data(j).v(n), data(j).w(n)); - - n = n+1; + + n = n + 1; end end end - + u(j) = max(data(j).u); - + end - - %calculate u_max from theorem 9 + + % calculate u_max from theorem 9 d_k = min(u); - - %loop over all rows - for j=1:size(D_0,1) - - %find all possible (and essential) intervals - candidate = find(data(j).u >= d_k); - - %calculate the potential of the possible intervals - - %initialize p as -Inf + + % loop over all rows + for j = 1:size(D_0, 1) + + % find all possible (and essential) intervals + candidate = find(data(j).u >= d_k); + + % calculate the potential of the possible intervals + + % initialize p as -Inf data(j).p(1:length(data(j).left)) = -Inf; - - %loop over all possible intervals - for s=1:size(candidate,2) - - if (s==1 && data(j).left(candidate(s)) == 0) + + % loop over all possible intervals + for s = 1:size(candidate, 2) + + if s == 1 && data(j).left(candidate(s)) == 0 data(j).p(candidate(1)) = 0; - - + else - %calculate p1 according to equality (17) - if (d_k == diffMat(j, data(j).left(candidate(s))) && d_k ~= D_k(j, data(j).left(candidate(s)))) + % calculate p1 according to equality (17) + if d_k == diffMat(j, data(j).left(candidate(s))) && d_k ~= D_k(j, data(j).left(candidate(s))) p1 = 1; - + else p1 = 0; - + end - - %calculate p2 according to equalitiy (18) - % if data(j).right(candidate(s)) < size(D_0, 2) - - if (d_k == -diffMat(j, data(j).right(candidate(s))+1) && d_k ~= D_k(j, data(j).right(candidate(s)))) - p2 = 1; - else - p2 = 0; - end - - % else - % - % if d_k == -diffMat(j, data(j).right(candidate(s))+1) - % p2 = 1; - % else - % p2 = 0; - % end - % - % end - - %calculate p3 according to equality (19) - p3 = size(find(D_k(j, data(j).left(candidate(s)):data(j).right(candidate(s))) == d_k),2); - - data(j).p(candidate(s)) = p1 + p2+ p3; - + + % calculate p2 according to equalitiy (18) + % if data(j).right(candidate(s)) < size(D_0, 2) + + if d_k == -diffMat(j, data(j).right(candidate(s)) + 1) && d_k ~= D_k(j, data(j).right(candidate(s))) + p2 = 1; + else + p2 = 0; + end + + % else + % + % if d_k == -diffMat(j, data(j).right(candidate(s))+1) + % p2 = 1; + % else + % p2 = 0; + % end + % + % end + + % calculate p3 according to equality (19) + p3 = size(find(D_k(j, data(j).left(candidate(s)):data(j).right(candidate(s))) == d_k), 2); + + data(j).p(candidate(s)) = p1 + p2 + p3; + end - + end - - %determinate intervals with maximum potential - maxPot = find(data(j).p == max(data(j).p)); - - %if several intervals have maximum potential, select - %the interval which has maximum length - if size(maxPot,2) > 1 - - for t=1:size(maxPot,2) - if t==1 && data(j).left(maxPot(t)) == 0 - data(j).l(1) = 0; - else - data(j).l(maxPot(t)) = data(j).right(maxPot(t)) - data(j).left(maxPot(t)) + 1; - end + + % determinate intervals with maximum potential + maxPot = find(data(j).p == max(data(j).p)); + + % if several intervals have maximum potential, select + % the interval which has maximum length + if size(maxPot, 2) > 1 + + for t = 1:size(maxPot, 2) + if t == 1 && data(j).left(maxPot(t)) == 0 + data(j).l(1) = 0; + else + data(j).l(maxPot(t)) = data(j).right(maxPot(t)) - data(j).left(maxPot(t)) + 1; end - - %data(j).l(maxPot) = data(j).right(maxPot) - data(j).left(maxPot) + 1; - - maxLength = find(data(j).l == max(data(j).l)); - - %left and right interval limits of the selected - %interval - leftIntLimit(j) = data(j).left(maxLength(1)); - rightIntLimit(j) = data(j).right(maxLength(1)); - - - else - - %left and right interval limits of the selected - %interval - leftIntLimit(j) = data(j).left(maxPot); - rightIntLimit(j) = data(j).right(maxPot); - - end - - %create segment associated by the selected interval - if leftIntLimit(j) ~= 0 - - segment(j,leftIntLimit(j):rightIntLimit(j)) = 1; - - end - + + % data(j).l(maxPot) = data(j).right(maxPot) - data(j).left(maxPot) + 1; + + maxLength = find(data(j).l == max(data(j).l)); + + % left and right interval limits of the selected + % interval + leftIntLimit(j) = data(j).left(maxLength(1)); + rightIntLimit(j) = data(j).right(maxLength(1)); + + else + + % left and right interval limits of the selected + % interval + leftIntLimit(j) = data(j).left(maxPot); + rightIntLimit(j) = data(j).right(maxPot); + + end + + % create segment associated by the selected interval + if leftIntLimit(j) ~= 0 + + segment(j, leftIntLimit(j):rightIntLimit(j)) = 1; + + end + end - - %write the segment in shape_k - shape_k = segment; - - %save shape_k in container - shapes(:,:,k) = shape_k; - - %save the calculated MU - shapesWeight(k) = d_k; - - %calculate new matrix, the diference matrix and complexities - D_k = D_k - d_k*shape_k; - - %delete variables + + % write the segment in shape_k + shape_k = segment; + + % save shape_k in container + shapes(:, :, k) = shape_k; + + % save the calculated MU + shapesWeight(k) = d_k; + + % calculate new matrix, the diference matrix and complexities + D_k = D_k - d_k * shape_k; + + % delete variables clear data; clear segment; clear u; clear leftIntLimit; clear rightIntLimit; - + + end + if sum(w(1 + offset:stf(i).numOfRays + offset)) > 0 + sequence.beam(i).numOfShapes = k; + sequence.beam(i).shapes = shapes(:, :, 1:k); + sequence.beam(i).shapesWeight = shapesWeight(1:k) / this.sequencingLevel * calFac; + sequence.beam(i).bixelIx = 1 + offset:size(stf(i).ray, 2) + offset; + sequence.beam(i).fluence = D_0; + else + sequence.beam(i).numOfShapes = 1; + sequence.beam(i).shapes = zeros(size(D_0)); + sequence.beam(i).shapesWeight = zeros(size(D_0)); + sequence.beam(i).bixelIx = 1 + offset:size(stf(i).ray, 2) + offset; + sequence.beam(i).fluence = zeros(size(D_0)); + end + if stf(i).numOfRays > 1 + sequence.w(1 + offset:stf(i).numOfRays + offset, 1) = D_0(indInMx) / this.sequencingLevel * calFac; + else + sequence.w(1 + offset:stf(i).numOfRays + offset, 1) = D_0(indInMx(1)) / this.sequencingLevel * calFac; end - - sequence.beam(i).numOfShapes = k; - sequence.beam(i).shapes = shapes(:,:,1:k); - sequence.beam(i).shapesWeight = shapesWeight(1:k)/this.sequencingLevel*calFac; - sequence.beam(i).bixelIx = 1+offset:stf(i).numOfRays+offset; - sequence.beam(i).fluence = D_0; - - sequence.w(1+offset:stf(i).numOfRays+offset,1) = D_0(indInMx)/this.sequencingLevel*calFac; - offset = offset + stf(i).numOfRays; - + end if this.visMode - this.plotSegments(sequence) + this.plotSegments(sequence); end - sequence = this.sequencing2ApertureInfo(sequence,stf); + sequence = this.sequencing2ApertureInfo(sequence, stf); end + end methods (Static) - function [available,msg] = isAvailable(pln,machine) - % see superclass for information - + + function [available, msg] = isAvailable(pln, machine) + % see superclass for information + if nargin < 2 machine = matRad_loadMachine(pln); end % Check superclass availability - [available,msg] = matRad_SequencingPhotonsAbstract.isAvailable(pln,machine); + [available, msg] = matRad_SequencingPhotonsAbstract.isAvailable(pln, machine); if ~available - return; + return else available = false; msg = []; end - - %checkBasic + + % checkBasic try - checkBasic = isfield(machine,'meta') && isfield(machine,'data'); - - %check modality + checkBasic = isfield(machine, 'meta') && isfield(machine, 'data'); + + % check modality checkModality = any(strcmp(matRad_SequencingPhotonsEngelLeaf.possibleRadiationModes, machine.meta.radiationMode)) && any(strcmp(matRad_SequencingPhotonsEngelLeaf.possibleRadiationModes, pln.radiationMode)); - - %Sanity check compatibility + + % Sanity check compatibility if checkModality - checkModality = strcmp(machine.meta.radiationMode,pln.radiationMode); + checkModality = strcmp(machine.meta.radiationMode, pln.radiationMode); end - + preCheck = checkBasic && checkModality; - + if ~preCheck - return; + return end catch msg = 'Your machine file is invalid and does not contain the basic field (meta/data/radiationMode)!'; - return; + return end available = preCheck; end + end -end \ No newline at end of file +end diff --git a/matRad/sequencing/matRad_SequencingPhotonsSiochiLeaf.m b/matRad/sequencing/matRad_SequencingPhotonsSiochiLeaf.m index 002806b80..0d5b9e2b0 100644 --- a/matRad/sequencing/matRad_SequencingPhotonsSiochiLeaf.m +++ b/matRad/sequencing/matRad_SequencingPhotonsSiochiLeaf.m @@ -1,161 +1,170 @@ classdef matRad_SequencingPhotonsSiochiLeaf < matRad_SequencingPhotonsAbstract - %UNTITLED Summary of this class goes here + % UNTITLED Summary of this class goes here % Detailed explanation goes here properties (Constant) - name = 'Photons Siochi Leaf Sequenceer'; - shortName = 'siochi'; - possibleRadiationModes = {'photons'}; + name = 'Photons Siochi Leaf Sequenceer' + shortName = 'siochi' + possibleRadiationModes = {'photons'} - end + end methods + function sequence = sequence(this, w, stf) - function sequence = sequence(this,w,stf) - - offset = 0; - + for i = 1:numel(stf) - - [D_0,D_k, shapes,calFac,indInMx] = this.initBeam(stf(i),w(1+offset:stf(i).numOfRays+offset)); - - shapesWeight = zeros(10000,1); + + [D_0, D_k, shapes, calFac, indInMx] = this.initBeam(stf(i), w(1 + offset:stf(i).numOfRays + offset)); + + shapesWeight = zeros(10000, 1); k = 0; - - %Decompose the port, do rod pushing - [tops, bases] = this.decomposePort(D_k); - %Form segments - [shapes,shapesWeight,k]=this.convertToSegments(shapes,shapesWeight,k,tops,bases); - - sequence.beam(i).numOfShapes = k; - sequence.beam(i).shapes = shapes(:,:,1:k); - sequence.beam(i).shapesWeight = shapesWeight(1:k)/this.sequencingLevel*calFac; - sequence.beam(i).bixelIx = 1+offset:stf(i).numOfRays+offset; - sequence.beam(i).fluence = D_0; - sequence.beam(i).sum = zeros(size(D_0)); - - for j = 1:k - sequence.beam(i).sum = sequence.beam(i).sum+sequence.beam(i).shapes(:,:,j)*sequence.beam(i).shapesWeight(j); + if sum(w(1 + offset:stf(i).numOfRays + offset)) > 0 + + % Decompose the port, do rod pushing + [tops, bases] = this.decomposePort(D_k); + % Form segments + [shapes, shapesWeight, k] = this.convertToSegments(shapes, shapesWeight, k, tops, bases); + + sequence.beam(i).numOfShapes = k; + sequence.beam(i).shapes = shapes(:, :, 1:k); + sequence.beam(i).shapesWeight = shapesWeight(1:k) / this.sequencingLevel * calFac; + sequence.beam(i).bixelIx = 1 + offset:size(stf(i).ray, 2) + offset; + sequence.beam(i).fluence = D_0; + sequence.beam(i).sum = zeros(size(D_0)); + + for j = 1:k + sequence.beam(i).sum = sequence.beam(i).sum + sequence.beam(i).shapes(:, :, j) * sequence.beam(i).shapesWeight(j); + end + else + sequence.beam(i).numOfShapes = 1; + sequence.beam(i).shapes = zeros(size(D_0)); + sequence.beam(i).shapesWeight = zeros(size(D_0)); + sequence.beam(i).bixelIx = 1 + offset:size(stf(i).ray, 2) + offset; + sequence.beam(i).fluence = zeros(size(D_0)); + sequence.beam(i).sum = zeros(size(D_0)); + end + if stf(i).numOfRays > 1 + sequence.w(1 + offset:stf(i).numOfRays + offset, 1) = sequence.beam(i).sum(indInMx); + else + sequence.w(1 + offset:stf(i).numOfRays + offset, 1) = w(1 + offset:stf(i).numOfRays + offset); end - sequence.w(1+offset:stf(i).numOfRays+offset,1) = sequence.beam(i).sum(indInMx); - offset = offset + stf(i).numOfRays; - + end if this.visMode - this.plotSegments(sequence) + this.plotSegments(sequence); end - sequence = this.sequencing2ApertureInfo(sequence,stf); + sequence = this.sequencing2ApertureInfo(sequence, stf); end + function [tops, bases] = decomposePort(~, map) + % Returns tops and bases of a fluence matrix "map" for Siochi leaf + % sequencing algorithm (rod pushing part). Accounts for collisions and + % tongue and groove (Tng) effects. - function [tops, bases] = decomposePort(~,map) - %Returns tops and bases of a fluence matrix "map" for Siochi leaf - %sequencing algorithm (rod pushing part). Accounts for collisions and - %tongue and groove (Tng) effects. - - [dimZ,dimX] = size(map); - map_nonZero = (map~=0); + [dimZ, dimX] = size(map); + map_nonZero = (map ~= 0); - [D_k_Z, D_k_X] = ind2sub([dimZ,dimX], find(map_nonZero)); + [D_k_Z, D_k_X] = ind2sub([dimZ, dimX], find(map_nonZero)); minZ = min(D_k_Z); maxZ = max(D_k_Z); minX = min(D_k_X); maxX = max(D_k_X); - + tops = zeros(dimZ, dimX); bases = zeros(dimZ, dimX); - + for i = minX:maxX maxTop = -1; TnG = 1; for j = minZ:maxZ if i == minX - bases(j,i) = 1; - tops(j,i) = bases(j,i)+map(j,i)-1; - else %assign trial base positions - if map(j,i) >= map(j,i-1) %current rod >= previous, match the bases - bases(j,i) = bases(j,i-1); - tops(j,i) = bases(j,i)+map(j,i)-1; - else %current rod = map(j, i - 1) % current rod >= previous, match the bases + bases(j, i) = bases(j, i - 1); + tops(j, i) = bases(j, i) + map(j, i) - 1; + else % current rod maxTop - maxTop = tops(j,i); + % determine which rod has the highest top in column + if tops(j, i) > maxTop + maxTop = tops(j, i); maxRow = j; end end - - %Correct for collision and tongue and groove error - while(TnG) - %go from maxRow down checking for TnG. This occurs when a shorter - %rod is "peeking over" a longer one in the direction transverse to - %the leaf motion. To fix this, match either the tops or bases of - %the rods. - for j = (maxRow-1):-1:minZ - if map(j,i) < map(j+1,i) - if tops(j,i) > tops(j+1,i) - tops(j+1,i) = tops(j,i); - bases(j+1,i) = tops(j+1,i)-map(j+1,i)+1; - elseif bases(j,i) < bases(j+1,i) - bases(j,i) = bases(j+1,i); - tops(j,i) = bases(j,i)+map(j,i)-1; + + % Correct for collision and tongue and groove error + while TnG + % go from maxRow down checking for TnG. This occurs when a shorter + % rod is "peeking over" a longer one in the direction transverse to + % the leaf motion. To fix this, match either the tops or bases of + % the rods. + for j = (maxRow - 1):-1:minZ + if map(j, i) < map(j + 1, i) + if tops(j, i) > tops(j + 1, i) + tops(j + 1, i) = tops(j, i); + bases(j + 1, i) = tops(j + 1, i) - map(j + 1, i) + 1; + elseif bases(j, i) < bases(j + 1, i) + bases(j, i) = bases(j + 1, i); + tops(j, i) = bases(j, i) + map(j, i) - 1; end else - if tops(j,i) < tops(j+1,i) - tops(j,i) = tops(j+1,i); - bases(j,i) = tops(j,i)-map(j,i)+1; - elseif bases(j,i) > bases(j+1,i) - bases(j+1,i) = bases(j,i); - tops(j+1,i) = bases(j+1,i)+map(j+1,i)-1; + if tops(j, i) < tops(j + 1, i) + tops(j, i) = tops(j + 1, i); + bases(j, i) = tops(j, i) - map(j, i) + 1; + elseif bases(j, i) > bases(j + 1, i) + bases(j + 1, i) = bases(j, i); + tops(j + 1, i) = bases(j + 1, i) + map(j + 1, i) - 1; end end end - %go from maxRow up checking for TnG - for j = (maxRow+1):maxZ - if map(j,i) < map(j-1,i) - if tops(j,i) > tops(j-1,i) - tops(j-1,i) = tops(j,i); - bases(j-1,i) = tops(j-1,i)-map(j-1,i)+1; - elseif bases(j,i) < bases(j-1,i) - bases(j,i) = bases(j-1,i); - tops(j,i) = bases(j,i)+map(j,i)-1; + % go from maxRow up checking for TnG + for j = (maxRow + 1):maxZ + if map(j, i) < map(j - 1, i) + if tops(j, i) > tops(j - 1, i) + tops(j - 1, i) = tops(j, i); + bases(j - 1, i) = tops(j - 1, i) - map(j - 1, i) + 1; + elseif bases(j, i) < bases(j - 1, i) + bases(j, i) = bases(j - 1, i); + tops(j, i) = bases(j, i) + map(j, i) - 1; end else - if tops(j,i) < tops(j-1,i) - tops(j,i) = tops(j-1,i); - bases(j,i) = tops(j,i)-map(j,i)+1; - elseif bases(j,i) > bases(j-1,i) - bases(j-1,i) = bases(j,i); - tops(j-1,i) = bases(j-1,i)+map(j-1,i)-1; + if tops(j, i) < tops(j - 1, i) + tops(j, i) = tops(j - 1, i); + bases(j, i) = tops(j, i) - map(j, i) + 1; + elseif bases(j, i) > bases(j - 1, i) + bases(j - 1, i) = bases(j, i); + tops(j - 1, i) = bases(j - 1, i) + map(j - 1, i) - 1; end end end - %now check if all TnG conditions have been removed + % now check if all TnG conditions have been removed TnG = 0; - for j = (minZ+1):maxZ - if map(j,i) < map(j-1,i); - if tops(j,i) > tops(j-1,i) + for j = (minZ + 1):maxZ + if map(j, i) < map(j - 1, i) + if tops(j, i) > tops(j - 1, i) TnG = 1; - elseif bases(j,i) < bases(j-1,i) + elseif bases(j, i) < bases(j - 1, i) TnG = 1; end else - if tops(j,i) < tops(j-1,i) + if tops(j, i) < tops(j - 1, i) TnG = 1; - elseif bases(j,i) > bases(j-1,i) + elseif bases(j, i) > bases(j - 1, i) TnG = 1; end end @@ -164,79 +173,80 @@ end end - function [shapes,shapesWeight,k] = convertToSegments(this, shapes,shapesWeight,k,tops,bases) - %Convert tops and bases to shape matrices. These are taken as to be the - %shapes of uniform level/elevation after the rods are pushed. - - + function [shapes, shapesWeight, k] = convertToSegments(this, shapes, shapesWeight, k, tops, bases) + % Convert tops and bases to shape matrices. These are taken as to be the + % shapes of uniform level/elevation after the rods are pushed. + levels = max(tops(:)); - + for level = 1:levels - %check if slab is new - if this.differentSlab(tops,bases,level) - k = k+1; %increment number of unique slabs - shape_k = (bases <= level).*(level <= tops); %shape of current slab - shapes(:,:,k) = shape_k; + % check if slab is new + if this.differentSlab(tops, bases, level) + k = k + 1; % increment number of unique slabs + shape_k = (bases <= level) .* (level <= tops); % shape of current slab + shapes(:, :, k) = shape_k; end - shapesWeight(k) = shapesWeight(k)+1; %if slab is not unique, this increments weight again + shapesWeight(k) = shapesWeight(k) + 1; % if slab is not unique, this increments weight again end end - function diffSlab = differentSlab(~,tops,bases,level) + function diffSlab = differentSlab(~, tops, bases, level) + + % Returns 1 if slab level is different than slab level-1 0 otherwise - %Returns 1 if slab level is different than slab level-1 0 otherwise - - if level == 1 %first slab is automatically different + if level == 1 % first slab is automatically different diffSlab = 1; else - shapeLevel = (bases <= level).*(level <= tops); %shape of slab with current level - shapeLevel_1 = (bases <= level-1).*(level-1 <= tops); %shape of slab with previous level - diffSlab = ~isequal(shapeLevel,shapeLevel_1); %tests if slabs are equal; isequaln was not giving correct results + shapeLevel = (bases <= level) .* (level <= tops); % shape of slab with current level + shapeLevel_1 = (bases <= level - 1) .* (level - 1 <= tops); % shape of slab with previous level + diffSlab = ~isequal(shapeLevel, shapeLevel_1); % tests if slabs are equal; isequaln was not giving correct results end end end methods (Static) - function [available,msg] = isAvailable(pln,machine) - % see superclass for information - + + function [available, msg] = isAvailable(pln, machine) + % see superclass for information + if nargin < 2 machine = matRad_loadMachine(pln); end % Check superclass availability - [available,msg] = matRad_SequencingPhotonsAbstract.isAvailable(pln,machine); + [available, msg] = matRad_SequencingPhotonsAbstract.isAvailable(pln, machine); if ~available - return; + return else available = false; msg = []; end - - %checkBasic + + % checkBasic try - checkBasic = isfield(machine,'meta') && isfield(machine,'data'); - - %check modality + checkBasic = isfield(machine, 'meta') && isfield(machine, 'data'); + + % check modality checkModality = any(strcmp(matRad_SequencingPhotonsSiochiLeaf.possibleRadiationModes, machine.meta.radiationMode)) && any(strcmp(matRad_SequencingPhotonsSiochiLeaf.possibleRadiationModes, pln.radiationMode)); - - %Sanity check compatibility + + % Sanity check compatibility if checkModality - checkModality = strcmp(machine.meta.radiationMode,pln.radiationMode); + checkModality = strcmp(machine.meta.radiationMode, pln.radiationMode); end - + preCheck = checkBasic && checkModality; - + if ~preCheck - return; + return end catch msg = 'Your machine file is invalid and does not contain the basic field (meta/data/radiationMode)!'; - return; + return end available = preCheck; end + end -end \ No newline at end of file +end diff --git a/matRad/sequencing/matRad_SequencingPhotonsXiaLeaf.m b/matRad/sequencing/matRad_SequencingPhotonsXiaLeaf.m index a8aa9a525..6092e7a80 100644 --- a/matRad/sequencing/matRad_SequencingPhotonsXiaLeaf.m +++ b/matRad/sequencing/matRad_SequencingPhotonsXiaLeaf.m @@ -1,175 +1,187 @@ classdef matRad_SequencingPhotonsXiaLeaf < matRad_SequencingPhotonsAbstract -% multileaf collimator leaf sequence algorithm -% for intensity modulated beams with multiple static segments according to -% Xia et al. (1998) Medical Physics -% -% [1] http://online.medphys.org/resource/1/mphya6/v25/i8/p1424_s1 -% -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% -% 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/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. -% -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + % multileaf collimator leaf sequence algorithm + % for intensity modulated beams with multiple static segments according to + % Xia et al. (1998) Medical Physics + % + % [1] http://online.medphys.org/resource/1/mphya6/v25/i8/p1424_s1 + % + % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + % + % 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/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 = 'Photons Xia Leaf Sequenceer'; - shortName = 'xia'; - possibleRadiationModes = {'photons'}; - end + name = 'Photons Xia Leaf Sequenceer' + shortName = 'xia' + possibleRadiationModes = {'photons'} + end properties mode = 'rl' % sliding window (sw) or reducing level (rl) end methods - function sequence = sequence(this,w,stf) + function sequence = sequence(this, w, stf) matRad_cfg = MatRad_Config.instance(); numOfBeams = numel(stf); offset = 0; for i = 1:numOfBeams - [D_0,D_k, shapes,calFac,indInMx] = this.initBeam(stf(i),w(1+offset:stf(i).numOfRays+offset)); + [D_0, D_k, shapes, calFac, indInMx] = this.initBeam(stf(i), w(1 + offset:stf(i).numOfRays + offset)); - % Save the maximun intensity (Equation 5) L_k = max(D_k(:)); - + % Save the maximun initial intensity matrix value in L_0. L_0 = L_k; - + % Set k=0, this variable is used for residuals intensity matrices D_k. k = 0; - + % start sequencer while L_k > 0 - + k = k + 1; - - - %Rounded off integer. Equation 7. + + % Rounded off integer. Equation 7. m = floor(log2(L_k)); - + % Convert m=1 if is less than 1. This happens when L_k belong to ]0,2[ if m < 1 m = 1; end - - %Calculate the delivery intensity unit. Equation 6. - d_k = floor(2^(m-1)); - + + % Calculate the delivery intensity unit. Equation 6. + d_k = floor(2^(m - 1)); + % Opening matrix. openingMx = D_k >= d_k; - - dimOfFluenceMxZ = size(shapes,1); + + dimOfFluenceMxZ = size(shapes, 1); switch this.mode case 'sw' % sliding window technique! for j = 1:dimOfFluenceMxZ - openIx = find(openingMx(j,:) == 1,1,'first'); + openIx = find(openingMx(j, :) == 1, 1, 'first'); if ~isempty(openIx) - closeIx = find(openingMx(j,openIx+1:end) == 0,1,'first'); + closeIx = find(openingMx(j, openIx + 1:end) == 0, 1, 'first'); if ~isempty(closeIx) - openingMx(j,openIx+closeIx:end) = 0; + openingMx(j, openIx + closeIx:end) = 0; end end - + end - case 'rl' % reducing levels technique! + case 'rl' % reducing levels technique! for j = 1:dimOfFluenceMxZ - [maxVal,maxIx] = max(openingMx(j,:) .* D_k(j,:)); + [maxVal, maxIx] = max(openingMx(j, :) .* D_k(j, :)); if maxVal > 0 - closeIx = maxIx + find(openingMx(j,maxIx+1:end) == 0,1,'first'); + closeIx = maxIx + find(openingMx(j, maxIx + 1:end) == 0, 1, 'first'); if ~isempty(closeIx) - openingMx(j,closeIx:end) = 0; + openingMx(j, closeIx:end) = 0; end - openIx = find(openingMx(j,1:maxIx-1) == 0,1,'last'); + openIx = find(openingMx(j, 1:maxIx - 1) == 0, 1, 'last'); if ~isempty(openIx) - openingMx(j,1:openIx) = 0; + openingMx(j, 1:openIx) = 0; end end - + end - otherwise - matRad_cfg.dispError('unknown sequencing mode') - end - + otherwise + matRad_cfg.dispError('unknown sequencing mode'); + end + shape_k = openingMx * d_k; - - shapes(:,:,k) = shape_k; + + shapes(:, :, k) = shape_k; shapesWeight(k) = d_k; D_k = D_k - shape_k; - + L_k = max(D_k(:)); % eq 5 - + + end + + if sum(w(1 + offset:stf(i).numOfRays + offset)) > 0 + + sequence.beam(i).numOfShapes = k; + sequence.beam(i).shapes = shapes(:, :, 1:k); + sequence.beam(i).shapesWeight = shapesWeight(1:k) / this.sequencingLevel * calFac; + sequence.beam(i).bixelIx = 1 + offset:size(stf(i).ray, 2) + offset; + sequence.beam(i).fluence = D_0; + + else + sequence.beam(i).numOfShapes = 1; + sequence.beam(i).shapes = zeros(size(D_0)); + sequence.beam(i).shapesWeight = zeros(size(D_0)); + sequence.beam(i).bixelIx = 1 + offset:size(stf(i).ray, 2); + sequence.beam(i).fluence = zeros(size(D_0)); + end + if stf(i).numOfRays > 1 + sequence.w(1 + offset:stf(i).numOfRays + offset, 1) = D_0(indInMx) / this.sequencingLevel * calFac; + else + sequence.w(1 + offset:stf(i).numOfRays + offset, 1) = D_0(indInMx(1)) / this.sequencingLevel * calFac; end - - sequence.beam(i).numOfShapes = k; - sequence.beam(i).shapes = shapes(:,:,1:k); - sequence.beam(i).shapesWeight = shapesWeight(1:k)/this.sequencingLevel*calFac; - sequence.beam(i).bixelIx = 1+offset:stf(i).numOfRays+offset; - sequence.beam(i).fluence = D_0; - - sequence.w(1+offset:stf(i).numOfRays+offset,1) = D_0(indInMx)/this.sequencingLevel*calFac; - offset = offset + stf(i).numOfRays; - end if this.visMode - this.plotSegments(sequence) + this.plotSegments(sequence); end - sequence = this.sequencing2ApertureInfo(sequence,stf); + sequence = this.sequencing2ApertureInfo(sequence, stf); end + end methods (Static) - function [available,msg] = isAvailable(pln,machine) - % see superclass for information - + + function [available, msg] = isAvailable(pln, machine) + % see superclass for information + if nargin < 2 machine = matRad_loadMachine(pln); end % Check superclass availability - [available,msg] = matRad_SequencingPhotonsAbstract.isAvailable(pln,machine); + [available, msg] = matRad_SequencingPhotonsAbstract.isAvailable(pln, machine); if ~available - return; + return else available = false; msg = []; end - - %checkBasic + + % checkBasic try - checkBasic = isfield(machine,'meta') && isfield(machine,'data'); - - %check modality + checkBasic = isfield(machine, 'meta') && isfield(machine, 'data'); + + % check modality checkModality = any(strcmp(matRad_SequencingPhotonsXiaLeaf.possibleRadiationModes, machine.meta.radiationMode)) && any(strcmp(matRad_SequencingPhotonsXiaLeaf.possibleRadiationModes, pln.radiationMode)); - - %Sanity check compatibility + + % Sanity check compatibility if checkModality - checkModality = strcmp(machine.meta.radiationMode,pln.radiationMode); + checkModality = strcmp(machine.meta.radiationMode, pln.radiationMode); end - + preCheck = checkBasic && checkModality; - + if ~preCheck - return; + return end catch msg = 'Your machine file is invalid and does not contain the basic field (meta/data/radiationMode)!'; - return; + return end available = preCheck; end + end -end \ No newline at end of file +end From 68ea49a43979e33ce4ad9532b122a0a9ab02a09a Mon Sep 17 00:00:00 2001 From: JenHardt <84377305+JenHardt@users.noreply.github.com> Date: Fri, 27 Feb 2026 11:13:58 +0100 Subject: [PATCH 07/20] part of class now --- .../sequencing/matRad_aperture2collimation.m | 131 ------------------ 1 file changed, 131 deletions(-) delete mode 100644 matRad/sequencing/matRad_aperture2collimation.m diff --git a/matRad/sequencing/matRad_aperture2collimation.m b/matRad/sequencing/matRad_aperture2collimation.m deleted file mode 100644 index 934daf8e4..000000000 --- a/matRad/sequencing/matRad_aperture2collimation.m +++ /dev/null @@ -1,131 +0,0 @@ -function [pln,stf] = matRad_aperture2collimation(pln,stf,sequencing,apertureInfo) -% matRad function to convert sequencing information / aperture information -% into collimation information in pln and stf for field-based dose -% calculation -% -% call -% [pln,stf] = matRad_aperture2collimation(pln,stf,sequencing,apertureInfo) -% -% input -% pln: pln file used to generate the sequenced plan -% stf: stf file used to generate the sequenced plan -% sequencing: sequencing information (from resultGUI) -% apertureInfo: apertureInfo (from resultGUI) -% -% output -% pln: matRad pln struct with collimation information -% stf: matRad stf struct with shapes instead of beamlets -% -% References -% - -% -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% -% Copyright 2022 the matRad development team. -% Author: wahln -% -% 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. -% -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -%Debug visualization -visBool = false; - -bixelWidth = apertureInfo.bixelWidth; -leafWidth = bixelWidth; -convResolution = 0.5; %[mm] - -%The collimator limits are infered here from the apertureInfo. This could -%be handled differently by explicitly storing collimator info in the base -%data? -symmetricMLClimits = vertcat(apertureInfo.beam.MLCWindow); -symmetricMLClimits = max(abs(symmetricMLClimits)); -fieldWidth = 2*max(symmetricMLClimits); - -%modify basic pln variables -pln.propStf.bixelWidth = 'field'; -pln.propStf.collimation.convResolution = 0.5; %[mm] -pln.propStf.collimation.fieldWidth = fieldWidth; -pln.propStf.collimation.leafWidth = leafWidth; - -% -%[bixelFieldX,bixelFieldY] = ndgrid(-fieldWidth/2:bixelWidth:fieldWidth/2,-fieldWidth/2:leafWidth:fieldWidth/2); -[convFieldX,convFieldY] = meshgrid(-fieldWidth/2:convResolution:fieldWidth/2); - -%TODO: Not used in calcPhotonDose but imported from DICOM -%pln.propStf.collimation.Devices ... -%pln.propStf.collimation.numOfFields -%pln.propStf.collimation.beamMeterset - -for iBeam = 1:numel(stf) - stfTmp = stf(iBeam); - beamSequencing = sequencing.beam(iBeam); - beamAperture = apertureInfo.beam(iBeam); - - stfTmp.bixelWidth = 'field'; - - nShapes = beamSequencing.numOfShapes; - - stfTmp.numOfRays = 1;% - stfTmp.numOfBixelsPerRay = nShapes; - stfTmp.totalNumOfBixels = nShapes; - - ray = struct(); - ray.rayPos_bev = [0 0 0]; - ray.targetPoint_bev = [0 stfTmp.SAD 0]; - ray.weight = 1; - ray.energy = stfTmp.ray(1).energy; - ray.beamletCornersAtIso = stfTmp.ray(1).beamletCornersAtIso; - ray.rayCorners_SCD = stfTmp.ray(1).rayCorners_SCD; - - %ray.shape = beamSequencing.sum; - shapeTotalF = zeros(size(convFieldX)); - - ray.shapes = struct(); - for iShape = 1:nShapes - currShape = beamAperture.shape(iShape); - activeLeafPairPosY = beamAperture.leafPairPos; - F = zeros(size(convFieldX)); - if visBool - hF = figure; imagesc(F); title(sprintf('Beam %d, Shape %d',iBeam,iShape)); hold on; - end - for iLeafPair = 1:numel(activeLeafPairPosY) - posY = activeLeafPairPosY(iLeafPair); - ixY = convFieldY >= posY-leafWidth/2 & convFieldY < posY + leafWidth/2; - ixX = convFieldX >= currShape.leftLeafPos(iLeafPair) & convFieldX < currShape.rightLeafPos(iLeafPair); - ix = ixX & ixY; - F(ix) = 1; - if visBool - figure(hF); imagesc(F); drawnow; pause(0.1); - end - end - - if visBool - pause(1); close(hF); - end - - F = F*currShape.weight; - shapeTotalF = shapeTotalF + F; - - ray.shapes(iShape).convFluence = F; - ray.shapes(iShape).shapeMap = currShape.shapeMap; - ray.shapes(iShape).weight = currShape.weight; - ray.shapes(iShape).leftLeafPos = currShape.leftLeafPos; - ray.shapes(iShape).rightLeafPos = currShape.rightLeafPos; - ray.shapes(iShape).leafPairCenterPos = activeLeafPairPosY; - end - - ray.shape = shapeTotalF; - ray.weight = ones(1,nShapes); - ray.collimation = pln.propStf.collimation; - stfTmp.ray = ray; - - stf(iBeam) = stfTmp; -end - - From cb4095314c9dc42d0121b312b71850bd1c309432 Mon Sep 17 00:00:00 2001 From: JenHardt <84377305+JenHardt@users.noreply.github.com> Date: Fri, 27 Feb 2026 11:15:32 +0100 Subject: [PATCH 08/20] part of ion sequencer class --- matRad/4D/matRad_makeBixelTimeSeq.m | 208 ---------------------------- matRad/4D/matRad_makePhaseMatrix.m | 78 ----------- 2 files changed, 286 deletions(-) delete mode 100644 matRad/4D/matRad_makeBixelTimeSeq.m delete mode 100644 matRad/4D/matRad_makePhaseMatrix.m diff --git a/matRad/4D/matRad_makeBixelTimeSeq.m b/matRad/4D/matRad_makeBixelTimeSeq.m deleted file mode 100644 index 70036cf20..000000000 --- a/matRad/4D/matRad_makeBixelTimeSeq.m +++ /dev/null @@ -1,208 +0,0 @@ -function timeSequence = matRad_makeBixelTimeSeq(stf, resultGUI) -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% using the steering information of matRad, makes a time sequenced order -% according to the irradiation scheme in spot scanning -% -% call -% timeSequence = matRad_makeBixelTimeSeq(stf, resultGUI) -% -% input -% stf: matRad steering information struct -% resultGUI: struct containing optimized fluence vector -% -% output -% timeSequence: struct containing bixel ordering information and the -% time sequence of the spot scanning -% -% References -% spill structure and timing informations: -% http://cdsweb.cern.ch/record/1182954 -% http://iopscience.iop.org/article/10.1088/0031-9155/56/20/003/meta -% -% -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% -% Copyright 2018 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. -% -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -% defining the constant parameters -% -% time required for synchrotron to change energy - -es_time = 3 * 10^6; % [\mu s] -% time required for synchrotron to recharge it' spill -spill_recharge_time = 2 * 10^6; % [\mu s] -% number of particles generated in each spill -spill_size = 4 * 10 ^ 10; -% speed of synchrotron's lateral scanning in an IES -scan_speed = 10; % m/s -% number of particles per second -spill_intensity = 4 * 10 ^ 8; - - -steerTime = [stf.bixelWidth] * (10 ^ 3)/ scan_speed; % [\mu s] - -timeSequence = struct; - -% first loop loops over all bixels to store their position and ray number -% in each IES -wOffset = 0; -for i = 1:length(stf) % looping over all beams - - usedEnergies = unique([stf(i).ray(:).energy]); - usedEnergiesSorted = sort(usedEnergies, 'descend'); - - timeSequence(i).orderToSTF = zeros(stf(i).totalNumOfBixels, 1); - timeSequence(i).orderToSS = zeros(stf(i).totalNumOfBixels, 1); - timeSequence(i).time = zeros(stf(i).totalNumOfBixels, 1); - timeSequence(i).e = zeros(stf(i).totalNumOfBixels, 1); - - - for e = 1:length(usedEnergies) % looping over IES's - - s = 1; - - for j = 1:stf(i).numOfRays % looping over all rays - - % find the rays which are active in current IES - if(any(stf(i).ray(j).energy == usedEnergiesSorted(e))) - - x = stf(i).ray(j).rayPos_bev(1); - y = stf(i).ray(j).rayPos_bev(3); - % - timeSequence(i).IES(e).x(s) = x; % store x position - timeSequence(i).IES(e).y(s) = y; % store y position - timeSequence(i).IES(e).w_index(s) = wOffset + ... - sum(stf(i).numOfBixelsPerRay(1:(j-1))) + ... - find(stf(i).ray(j).energy == usedEnergiesSorted(e)); % store index - - s = s + 1; - - end - end - end - - wOffset = wOffset + sum(stf(i).numOfBixelsPerRay); - -end - -% after storing all the required information, -% same loop over all bixels will put each bixel in it's order - -spill_usage = 0; -offset = 0; - -for i = 1:length(stf) - - usedEnergies = unique([stf(i).ray(:).energy]); - - t = 0; - order_count = 1; - - for e = 1: length(usedEnergies) - - % sort the y positions from high to low (backforth is up do down) - y_sorted = sort(unique(timeSequence(i).IES(e).y), 'descend'); - x_sorted = sort(timeSequence(i).IES(e).x, 'ascend'); - - for k = 1:length(y_sorted) - - y = y_sorted(k); - % find indexes corresponding to current y position - % in other words, number of bixels in the current row - ind_y = find(timeSequence(i).IES(e).y == y); - - % since backforth fasion is zig zag like, flip the order every - % second row - if ~rem(k,2) - ind_y = fliplr(ind_y); - end - - % loop over all the bixels in the row - for is = 1:length(ind_y) - - s = ind_y(is); - - x = x_sorted(s); - - w_index = timeSequence(i).IES(e).w_index(s); - - % in case there were holes inside the plan "multi" - % multiplies the steertime to take it into account: - if(k == 1 && is == 1) - x_prev = x; - y_prev = y; - end - % x direction - multi = abs(x_prev - x)/stf(i).bixelWidth; - % y direction - multi = multi + abs(y_prev - y)/stf(i).bixelWidth; - % - x_prev = x; - y_prev = y; - - % calculating the time: - - % required spot fluence - numOfParticles = resultGUI.w(w_index)* 10^6; - % time spent to spill the required spot fluence - spillTime = numOfParticles * 10^6 / spill_intensity; - - % spotTime:time spent to steer scan along IES per bixel - t = t + multi * steerTime(i) + spillTime; - - % taking account of the time to recharge the spill in case - % the required fluence was more than spill size - if(spill_usage + numOfParticles > spill_size) - t = t + spill_recharge_time; - spill_usage = 0; - end - - % used amount of fluence from current spill - spill_usage = spill_usage + numOfParticles; - - % storing the time and the order of bixels - - % make the both counter and index 'per beam' - help index - w_ind = w_index - offset; - - % timeline according to the spot scanning order - timeSequence(i).time(order_count) = t; - % IES of bixels according to the spot scanning order - timeSequence(i).e(order_count) = e; - % according to spot scanning order, sorts w index of all - % bixels, use this order to transfer STF order to Spot - % Scanning order - timeSequence(i).orderToSS(order_count) = w_ind; - - % according to STF order, gives us order of irradiation of - % each bixel, use this order to transfer Spot Scanning - % order to STF order - % orderToSTF(orderToSS) = orderToSS(orderToSTF) = 1:#bixels - timeSequence(i).orderToSTF(w_ind) = order_count; - - order_count = order_count + 1; - - end - end - - t = t + es_time; - - end - - % storing the fluence per beam - timeSequence(i).w = resultGUI.w(offset + 1: offset + stf(i).totalNumOfBixels); - - offset = offset + stf(i).totalNumOfBixels; - -end - -end \ No newline at end of file diff --git a/matRad/4D/matRad_makePhaseMatrix.m b/matRad/4D/matRad_makePhaseMatrix.m deleted file mode 100644 index 861717ff9..000000000 --- a/matRad/4D/matRad_makePhaseMatrix.m +++ /dev/null @@ -1,78 +0,0 @@ -function timeSequence = matRad_makePhaseMatrix(timeSequence, numOfPhases, motionPeriod, motion) -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% using the time sequence and the ordering of the bixel iradiation, and -% number of scenarios, makes a phase matrix of size number of bixels * -% number of scenarios -% -% -% call -% timeSequence = matRad_makePhaseMatrix(timeSequence, numOfPhases, motionPeriod, motion) -% -% input -% timeSequence: struct containing bixel ordering information and the -% time sequence of the spot scanning -% numOfCtScen: number of the desired phases -% motionPeriod: the extent of a whole breathing cycle (in seconds) -% motion: motion scenario: 'linear'(default), 'sampled_period' -% -% output -% timeSequence: phase matrix field added -% -% References -% - -% -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% -% Copyright 2018 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. -% -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -% time of each phase [/mu s] -phaseTime = motionPeriod * 10 ^ 6/numOfPhases; - -for i = 1:length(timeSequence) - - realTime = phaseTime; - timeSequence(i).phaseMatrix = zeros(length(timeSequence(i).time),numOfPhases); - - iPhase = 1; - iTime = 1; - - while (iTime <= length(timeSequence(i).time)) - if(timeSequence(i).time(iTime) < realTime) - - while(iTime <= length(timeSequence(i).time) && timeSequence(i).time(iTime) < realTime) - timeSequence(i).phaseMatrix(iTime, iPhase) = 1; - iTime = iTime + 1; - end - - else - - - iPhase = iPhase + 1; - - % back to 1 after going over all phases - if(iPhase > numOfPhases) - iPhase = 1; - end - - realTime = realTime + phaseTime; - - end - end - - % permuatation of phaseMatrix from SS order to STF order - timeSequence(i).phaseMatrix = timeSequence(i).phaseMatrix(timeSequence(i).orderToSTF,:); - [timeSequence(i).phaseNum,~] = find(timeSequence(i).phaseMatrix'); - % inserting the fluence in phaseMatrix - timeSequence(i).phaseMatrix = timeSequence(i).phaseMatrix .* timeSequence(i).w; - -end - From f4e74e4c92be9de59457cacd11a5bf8ed0881673 Mon Sep 17 00:00:00 2001 From: JenHardt <84377305+JenHardt@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:16:36 +0100 Subject: [PATCH 09/20] removed unused opt result --- matRad/matRad_directApertureOptimization.m | 79 +++++++++++----------- 1 file changed, 38 insertions(+), 41 deletions(-) diff --git a/matRad/matRad_directApertureOptimization.m b/matRad/matRad_directApertureOptimization.m index 1e909924f..91e3fb52f 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, pln) % matRad function to run direct aperture optimization % % call @@ -10,7 +10,7 @@ % apertureInfo: aperture shape info struct % optResult: resultGUI struct to which the output data will be added, if % this field is empty optResult struct will be created -% +% % pln: matRad pln struct % % output @@ -23,56 +23,54 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% 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/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 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/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_cfg = MatRad_Config.instance(); - % adjust overlap priorities cst = matRad_setOverlapPriorities(cst); -% check & adjust objectives and constraints internally for fractionation -for i = 1:size(cst,1) - for j = 1:numel(cst{i,6}) - obj = cst{i,6}{j}; - - %In case it is a default saved struct, convert to object - %Also intrinsically checks that we have a valid optimization - %objective or constraint function in the end - if ~isa(obj,'matRad_DoseOptimizationFunction') +% check & adjust objectives and constraints internally for fractionation +for i = 1:size(cst, 1) + for j = 1:numel(cst{i, 6}) + obj = cst{i, 6}{j}; + + % In case it is a default saved struct, convert to object + % Also intrinsically checks that we have a valid optimization + % objective or constraint function in the end + if ~isa(obj, 'matRad_DoseOptimizationFunction') try obj = matRad_DoseOptimizationFunction.createInstanceFromStruct(obj); catch - matRad_cfg.dispError('cst{%d,6}{%d} is not a valid Objective/constraint! Remove or Replace and try again!',i,j); + matRad_cfg.dispError('cst{%d,6}{%d} is not a valid Objective/constraint! Remove or Replace and try again!', i, j); end end - - obj = obj.setDoseParameters(obj.getDoseParameters()/pln.numOfFractions); - - cst{i,6}{j} = obj; + + obj = obj.setDoseParameters(obj.getDoseParameters() / pln.numOfFractions); + + cst{i, 6}{j} = obj; end end -% resizing cst to dose cube resolution -cst = matRad_resizeCstToGrid(cst,dij.ctGrid.x,dij.ctGrid.y,dij.ctGrid.z,... - dij.doseGrid.x,dij.doseGrid.y,dij.doseGrid.z); +% resizing cst to dose cube resolution +cst = matRad_resizeCstToGrid(cst, dij.ctGrid.x, dij.ctGrid.y, dij.ctGrid.z, ... + dij.doseGrid.x, dij.doseGrid.y, dij.doseGrid.z); - -if ~isfield(pln,'bioModel') +if ~isfield(pln, 'bioModel') pln.bioModel = 'none'; end -if ~isa(pln.bioModel,'matRad_BiologicalModel') - pln.bioModel = matRad_BiologicalModel.validate(pln.bioModel,pln.radiationMode); +if ~isa(pln.bioModel, 'matRad_BiologicalModel') + pln.bioModel = matRad_BiologicalModel.validate(pln.bioModel, pln.radiationMode); end % set optimization options @@ -83,14 +81,14 @@ options.model = pln.bioModel.model; % update aperture info vector -apertureInfo = matRad_OptimizationProblemDAO.matRad_daoVec2ApertureInfo(apertureInfo,apertureInfo.apertureVector); +apertureInfo = matRad_OptimizationProblemDAO.matRad_daoVec2ApertureInfo(apertureInfo, apertureInfo.apertureVector); -%Use Dose Projection only +% Use Dose Projection only backProjection = matRad_DoseProjection(); -optiProb = matRad_OptimizationProblemDAO(backProjection,apertureInfo); +optiProb = matRad_OptimizationProblemDAO(backProjection, apertureInfo); -if ~isfield(pln.propOpt,'optimizer') +if ~isfield(pln.propOpt, 'optimizer') pln.propOpt.optimizer = 'IPOPT'; end @@ -100,21 +98,20 @@ case 'fmincon' optimizer = matRad_OptimizerFmincon; otherwise - matRad_cfg.dispWarning('Optimizer ''%s'' not known! Fallback to IPOPT!',pln.propOpt.optimizer); + matRad_cfg.dispWarning('Optimizer ''%s'' not known! Fallback to IPOPT!', pln.propOpt.optimizer); optimizer = matRad_OptimizerIPOPT; end % Run IPOPT. -optimizer = optimizer.optimize(apertureInfo.apertureVector,optiProb,dij,cst); +optimizer = optimizer.optimize(apertureInfo.apertureVector, optiProb, dij, cst); wOpt = optimizer.wResult; % update the apertureInfoStruct and calculate bixel weights -apertureInfo = matRad_OptimizationProblemDAO.matRad_daoVec2ApertureInfo(apertureInfo,wOpt); +apertureInfo = matRad_OptimizationProblemDAO.matRad_daoVec2ApertureInfo(apertureInfo, wOpt); % logging final results matRad_cfg.dispInfo('Calculating final cubes...\n'); -resultGUI = matRad_calcCubes(apertureInfo.bixelWeights,dij); +resultGUI = matRad_calcCubes(apertureInfo.bixelWeights, dij); resultGUI.w = apertureInfo.bixelWeights; resultGUI.wDAO = apertureInfo.bixelWeights; -resultGUI.apertureInfo = apertureInfo; - +resultGUI.sequencing.apertureInfo = apertureInfo; From 460f8ba4943dca299f97576dc1dc6d3a7a571adf Mon Sep 17 00:00:00 2001 From: JenHardt <84377305+JenHardt@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:20:50 +0100 Subject: [PATCH 10/20] updated DAO example --- examples/matRad_example3_photonsDAO.m | 78 +++++++++++++-------------- 1 file changed, 37 insertions(+), 41 deletions(-) diff --git a/examples/matRad_example3_photonsDAO.m b/examples/matRad_example3_photonsDAO.m index 7809b304d..6f66c5a5c 100644 --- a/examples/matRad_example3_photonsDAO.m +++ b/examples/matRad_example3_photonsDAO.m @@ -2,48 +2,48 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% 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/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 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/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. % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% In this example we will show +%% In this example we will show % (i) how to load patient data into matRad -% (ii) how to setup a photon dose calculation and +% (ii) how to setup a photon dose calculation and % (iii) how to inversely optimize directly from command window in MatLab. % (iv) how to apply a sequencing algorithm % (v) how to run a direct aperture optimization % (iv) how to visually and quantitatively evaluate the result %% set matRad runtime configuration -matRad_rc; %If this throws an error, run it from the parent directory first to set the paths +matRad_rc; % If this throws an error, run it from the parent directory first to set the paths %% Patient Data Import % import the head & neck patient into your workspace. load('HEAD_AND_NECK.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 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. pln.radiationMode = 'photons'; % either photons / protons / carbon pln.machine = 'Generic'; pln.numOfFractions = 30; - + pln.propStf.gantryAngles = [0:90:359]; -pln.propStf.couchAngles = [0 0 0 0 ]; +pln.propStf.couchAngles = [0 0 0 0]; pln.propStf.bixelWidth = 5; pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); -pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0); +pln.propStf.isoCenter = ones(pln.propStf.numOfBeams, 1) * matRad_getIsoCenter(cst, ct, 0); -pln.bioModel = 'none'; +pln.bioModel = 'none'; pln.multScen = 'nomScen'; % dose calculation settings @@ -51,65 +51,61 @@ pln.propDoseCalc.doseGrid.resolution.y = 5; % [mm] pln.propDoseCalc.doseGrid.resolution.z = 5; % [mm] -% We can also use other solver for optimization than IPOPT. matRad +% We can also use other solver for optimization than IPOPT. matRad % currently supports fmincon from the MATLAB Optimization Toolbox. First we % check if the fmincon-Solver is available, and if it es, we set in in the % pln.propOpt.optimizer vairable. Otherwise wie set to the default % optimizer 'IPOPT' if matRad_OptimizerFmincon.IsAvailable() - pln.propOpt.optimizer = 'fmincon'; + pln.propOpt.optimizer = 'fmincon'; else pln.propOpt.optimizer = 'IPOPT'; end -pln.propOpt.quantityOpt = 'physicalDose'; +pln.propOpt.quantityOpt = 'physicalDose'; %% Generate Beam Geometry STF -stf = matRad_generateStf(ct,cst,pln); +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 +% 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); +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 +% 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. Once the optimization has finished, trigger once the GUI to % visualize the optimized dose cubes. -resultGUI = matRad_fluenceOptimization(dij,cst,pln); -%matRadGUI; +resultGUI = matRad_fluenceOptimization(dij, cst, pln); +% 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 +% 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. - %% some testing of sequencing pln.propSeq.sequencer = 'siochi'; pln.propSeq.sequencingLevel = 10; -resultGUI_SIOCHI = matRad_sequencing(resultGUI,stf,pln, dij); -resultGUI_SIOCHI_OLD = matRad_siochiLeafSequencing(resultGUI,stf,dij,pln.propSeq.sequencingLevel,0); +resultGUI_SIOCHI = matRad_sequencing(resultGUI, stf, pln, dij); pln.propSeq.sequencer = 'xia'; -resultGUI_XIA = matRad_sequencing(resultGUI,stf,pln, dij); -resultGUI_XIA_OLD = matRad_xiaLeafSequencing(resultGUI,stf,dij,pln.propSeq.sequencingLevel,0); +resultGUI_XIA = matRad_sequencing(resultGUI, stf, pln, dij); pln.propSeq.sequencer = 'engel'; -resultGUI_ENGEL = matRad_sequencing(resultGUI,stf,pln, dij); -resultGUI_ENGEL_OLD = matRad_engelLeafSequencing(resultGUI,stf,dij,pln.propSeq.sequencingLevel,0); +resultGUI_ENGEL = matRad_sequencing(resultGUI, stf, pln, dij); %% DAO - Direct Aperture Optimization -% The Direct Aperture Optimization is an optimization approach where we +% The Direct Aperture Optimization is an optimization approach where we % directly optimize aperture shapes and weights. -resultGUI_SIOCHI_DAO = matRad_directApertureOptimization(dij,cst,resultGUI_SIOCHI.sequencing.apertureInfo,resultGUI,pln); +resultGUI_SIOCHI_DAO = matRad_directApertureOptimization(dij, cst, resultGUI_SIOCHI.sequencing.apertureInfo, pln); %% Aperture visualization % Use a matrad function to visualize the resulting aperture shapes matRad_visApertureInfo(resultGUI_SIOCHI_DAO.sequencing.apertureInfo); %% Indicator Calculation and display of DVH and QI -#resultGUI = matRad_planAnalysis(resultGUI,ct,cst,stf,pln); +resultGUI = matRad_planAnalysis(resultGUI, ct, cst, stf, pln); From 5bdd747fb5040d767c78b9e45709cbdf2363d0dd Mon Sep 17 00:00:00 2001 From: JenHardt <84377305+JenHardt@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:21:45 +0100 Subject: [PATCH 11/20] remove unnecessary update ResultGUi --- matRad/sequencing/matRad_SequencingBase.m | 5 ----- matRad/sequencing/matRad_SequencingIons.m | 4 ---- 2 files changed, 9 deletions(-) diff --git a/matRad/sequencing/matRad_SequencingBase.m b/matRad/sequencing/matRad_SequencingBase.m index 6de20f5fb..54f352e90 100644 --- a/matRad/sequencing/matRad_SequencingBase.m +++ b/matRad/sequencing/matRad_SequencingBase.m @@ -144,11 +144,6 @@ function assignPropertiesFromPln(this,pln,warnWhenPropertyChanged) matRad_cfg.dispError('This is an Abstract Base class! Function needs to be called for instantiable subclasses!'); end - function resultGUI = updateResultGUI(this,sequence,varargin) - - matRad_cfg = MatRad_Config.instance(); - matRad_cfg.dispError('This is an Abstract Base class! Function needs to be called for instantiable subclasses!'); - end end methods (Static) function sequencer = getSequencerFromPln(pln, warnDefault) diff --git a/matRad/sequencing/matRad_SequencingIons.m b/matRad/sequencing/matRad_SequencingIons.m index f9f225797..9a575fd35 100644 --- a/matRad/sequencing/matRad_SequencingIons.m +++ b/matRad/sequencing/matRad_SequencingIons.m @@ -25,10 +25,6 @@ sequence = this.calcSpotTime(sequence,w,stf); end - function resultGUI = updateResultGUI(~,sequence,~,~) - resultGUI.sequencing = sequence; - end - function sequence = calcSpotOrder(~,stf) sequence = struct; From dd363103402f14ad04f053d223ee9ed258da6575 Mon Sep 17 00:00:00 2001 From: JenHardt <84377305+JenHardt@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:23:25 +0100 Subject: [PATCH 12/20] set fill empty Bixels to true to fix DAO issue with empty bixels --- matRad/steering/matRad_StfGeneratorExternalRayBixelAbstract.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matRad/steering/matRad_StfGeneratorExternalRayBixelAbstract.m b/matRad/steering/matRad_StfGeneratorExternalRayBixelAbstract.m index 524cf6490..cb792cb6f 100644 --- a/matRad/steering/matRad_StfGeneratorExternalRayBixelAbstract.m +++ b/matRad/steering/matRad_StfGeneratorExternalRayBixelAbstract.m @@ -20,7 +20,7 @@ couchAngles = 0; bixelWidth = 0; isoCenter - fillEmptyBixels + fillEmptyBixels = true; centered end From 6e7ca5be370adead9c4a03c5c06cb74759068db6 Mon Sep 17 00:00:00 2001 From: JenHardt <84377305+JenHardt@users.noreply.github.com> Date: Wed, 4 Mar 2026 13:36:09 +0100 Subject: [PATCH 13/20] restore Files --- matRad/4D/matRad_makeBixelTimeSeq.m | 206 ++++++++++++++++++++++++++++ matRad/4D/matRad_makePhaseMatrix.m | 76 ++++++++++ 2 files changed, 282 insertions(+) create mode 100644 matRad/4D/matRad_makeBixelTimeSeq.m create mode 100644 matRad/4D/matRad_makePhaseMatrix.m diff --git a/matRad/4D/matRad_makeBixelTimeSeq.m b/matRad/4D/matRad_makeBixelTimeSeq.m new file mode 100644 index 000000000..1266a6997 --- /dev/null +++ b/matRad/4D/matRad_makeBixelTimeSeq.m @@ -0,0 +1,206 @@ +function timeSequence = matRad_makeBixelTimeSeq(stf, resultGUI) +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% using the steering information of matRad, makes a time sequenced order +% according to the irradiation scheme in spot scanning +% +% call +% timeSequence = matRad_makeBixelTimeSeq(stf, resultGUI) +% +% input +% stf: matRad steering information struct +% resultGUI: struct containing optimized fluence vector +% +% output +% timeSequence: struct containing bixel ordering information and the +% time sequence of the spot scanning +% +% References +% spill structure and timing informations: +% http://cdsweb.cern.ch/record/1182954 +% http://iopscience.iop.org/article/10.1088/0031-9155/56/20/003/meta +% +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2018 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. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% defining the constant parameters +% +% time required for synchrotron to change energy + +es_time = 3 * 10^6; % [\mu s] +% time required for synchrotron to recharge it' spill +spill_recharge_time = 2 * 10^6; % [\mu s] +% number of particles generated in each spill +spill_size = 4 * 10^10; +% speed of synchrotron's lateral scanning in an IES +scan_speed = 10; % m/s +% number of particles per second +spill_intensity = 4 * 10^8; + +steerTime = [stf.bixelWidth] * (10^3) / scan_speed; % [\mu s] + +timeSequence = struct; + +% first loop loops over all bixels to store their position and ray number +% in each IES +wOffset = 0; +for i = 1:length(stf) % looping over all beams + + usedEnergies = unique([stf(i).ray(:).energy]); + usedEnergiesSorted = sort(usedEnergies, 'descend'); + + timeSequence(i).orderToSTF = zeros(stf(i).totalNumOfBixels, 1); + timeSequence(i).orderToSS = zeros(stf(i).totalNumOfBixels, 1); + timeSequence(i).time = zeros(stf(i).totalNumOfBixels, 1); + timeSequence(i).e = zeros(stf(i).totalNumOfBixels, 1); + + for e = 1:length(usedEnergies) % looping over IES's + + s = 1; + + for j = 1:stf(i).numOfRays % looping over all rays + + % find the rays which are active in current IES + if any(stf(i).ray(j).energy == usedEnergiesSorted(e)) + + x = stf(i).ray(j).rayPos_bev(1); + y = stf(i).ray(j).rayPos_bev(3); + % + timeSequence(i).IES(e).x(s) = x; % store x position + timeSequence(i).IES(e).y(s) = y; % store y position + timeSequence(i).IES(e).w_index(s) = wOffset + ... + sum(stf(i).numOfBixelsPerRay(1:(j - 1))) + ... + find(stf(i).ray(j).energy == usedEnergiesSorted(e)); % store index + + s = s + 1; + + end + end + end + + wOffset = wOffset + sum(stf(i).numOfBixelsPerRay); + +end + +% after storing all the required information, +% same loop over all bixels will put each bixel in it's order + +spill_usage = 0; +offset = 0; + +for i = 1:length(stf) + + usedEnergies = unique([stf(i).ray(:).energy]); + + t = 0; + order_count = 1; + + for e = 1:length(usedEnergies) + + % sort the y positions from high to low (backforth is up do down) + y_sorted = sort(unique(timeSequence(i).IES(e).y), 'descend'); + x_sorted = sort(timeSequence(i).IES(e).x, 'ascend'); + + for k = 1:length(y_sorted) + + y = y_sorted(k); + % find indexes corresponding to current y position + % in other words, number of bixels in the current row + ind_y = find(timeSequence(i).IES(e).y == y); + + % since backforth fasion is zig zag like, flip the order every + % second row + if ~rem(k, 2) + ind_y = fliplr(ind_y); + end + + % loop over all the bixels in the row + for is = 1:length(ind_y) + + s = ind_y(is); + + x = x_sorted(s); + + w_index = timeSequence(i).IES(e).w_index(s); + + % in case there were holes inside the plan "multi" + % multiplies the steertime to take it into account: + if k == 1 && is == 1 + x_prev = x; + y_prev = y; + end + % x direction + multi = abs(x_prev - x) / stf(i).bixelWidth; + % y direction + multi = multi + abs(y_prev - y) / stf(i).bixelWidth; + % + x_prev = x; + y_prev = y; + + % calculating the time: + + % required spot fluence + numOfParticles = resultGUI.w(w_index) * 10^6; + % time spent to spill the required spot fluence + spillTime = numOfParticles * 10^6 / spill_intensity; + + % spotTime:time spent to steer scan along IES per bixel + t = t + multi * steerTime(i) + spillTime; + + % taking account of the time to recharge the spill in case + % the required fluence was more than spill size + if spill_usage + numOfParticles > spill_size + t = t + spill_recharge_time; + spill_usage = 0; + end + + % used amount of fluence from current spill + spill_usage = spill_usage + numOfParticles; + + % storing the time and the order of bixels + + % make the both counter and index 'per beam' - help index + w_ind = w_index - offset; + + % timeline according to the spot scanning order + timeSequence(i).time(order_count) = t; + % IES of bixels according to the spot scanning order + timeSequence(i).e(order_count) = e; + % according to spot scanning order, sorts w index of all + % bixels, use this order to transfer STF order to Spot + % Scanning order + timeSequence(i).orderToSS(order_count) = w_ind; + + % according to STF order, gives us order of irradiation of + % each bixel, use this order to transfer Spot Scanning + % order to STF order + % orderToSTF(orderToSS) = orderToSS(orderToSTF) = 1:#bixels + timeSequence(i).orderToSTF(w_ind) = order_count; + + order_count = order_count + 1; + + end + end + + t = t + es_time; + + end + + % storing the fluence per beam + timeSequence(i).w = resultGUI.w(offset + 1:offset + stf(i).totalNumOfBixels); + + offset = offset + stf(i).totalNumOfBixels; + +end + +end diff --git a/matRad/4D/matRad_makePhaseMatrix.m b/matRad/4D/matRad_makePhaseMatrix.m new file mode 100644 index 000000000..07cc52d25 --- /dev/null +++ b/matRad/4D/matRad_makePhaseMatrix.m @@ -0,0 +1,76 @@ +function timeSequence = matRad_makePhaseMatrix(timeSequence, numOfPhases, motionPeriod, motion) +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% using the time sequence and the ordering of the bixel iradiation, and +% number of scenarios, makes a phase matrix of size number of bixels * +% number of scenarios +% +% +% call +% timeSequence = matRad_makePhaseMatrix(timeSequence, numOfPhases, motionPeriod, motion) +% +% input +% timeSequence: struct containing bixel ordering information and the +% time sequence of the spot scanning +% numOfCtScen: number of the desired phases +% motionPeriod: the extent of a whole breathing cycle (in seconds) +% motion: motion scenario: 'linear'(default), 'sampled_period' +% +% output +% timeSequence: phase matrix field added +% +% References +% - +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2018 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. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% time of each phase [/mu s] +phaseTime = motionPeriod * 10^6 / numOfPhases; + +for i = 1:length(timeSequence) + + realTime = phaseTime; + timeSequence(i).phaseMatrix = zeros(length(timeSequence(i).time), numOfPhases); + + iPhase = 1; + iTime = 1; + + while iTime <= length(timeSequence(i).time) + if timeSequence(i).time(iTime) < realTime + + while iTime <= length(timeSequence(i).time) && timeSequence(i).time(iTime) < realTime + timeSequence(i).phaseMatrix(iTime, iPhase) = 1; + iTime = iTime + 1; + end + + else + + iPhase = iPhase + 1; + + % back to 1 after going over all phases + if iPhase > numOfPhases + iPhase = 1; + end + + realTime = realTime + phaseTime; + + end + end + + % permuatation of phaseMatrix from SS order to STF order + timeSequence(i).phaseMatrix = timeSequence(i).phaseMatrix(timeSequence(i).orderToSTF, :); + [timeSequence(i).phaseNum, ~] = find(timeSequence(i).phaseMatrix'); + % inserting the fluence in phaseMatrix + timeSequence(i).phaseMatrix = timeSequence(i).phaseMatrix .* timeSequence(i).w; + +end From a307a62ec6192269c4c66043357ba0f16a0cd25d Mon Sep 17 00:00:00 2001 From: JenHardt <84377305+JenHardt@users.noreply.github.com> Date: Wed, 4 Mar 2026 13:37:11 +0100 Subject: [PATCH 14/20] revert files --- .../sequencing/matRad_aperture2collimation.m | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 matRad/sequencing/matRad_aperture2collimation.m diff --git a/matRad/sequencing/matRad_aperture2collimation.m b/matRad/sequencing/matRad_aperture2collimation.m new file mode 100644 index 000000000..934daf8e4 --- /dev/null +++ b/matRad/sequencing/matRad_aperture2collimation.m @@ -0,0 +1,131 @@ +function [pln,stf] = matRad_aperture2collimation(pln,stf,sequencing,apertureInfo) +% matRad function to convert sequencing information / aperture information +% into collimation information in pln and stf for field-based dose +% calculation +% +% call +% [pln,stf] = matRad_aperture2collimation(pln,stf,sequencing,apertureInfo) +% +% input +% pln: pln file used to generate the sequenced plan +% stf: stf file used to generate the sequenced plan +% sequencing: sequencing information (from resultGUI) +% apertureInfo: apertureInfo (from resultGUI) +% +% output +% pln: matRad pln struct with collimation information +% stf: matRad stf struct with shapes instead of beamlets +% +% References +% - +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2022 the matRad development team. +% Author: wahln +% +% 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. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%Debug visualization +visBool = false; + +bixelWidth = apertureInfo.bixelWidth; +leafWidth = bixelWidth; +convResolution = 0.5; %[mm] + +%The collimator limits are infered here from the apertureInfo. This could +%be handled differently by explicitly storing collimator info in the base +%data? +symmetricMLClimits = vertcat(apertureInfo.beam.MLCWindow); +symmetricMLClimits = max(abs(symmetricMLClimits)); +fieldWidth = 2*max(symmetricMLClimits); + +%modify basic pln variables +pln.propStf.bixelWidth = 'field'; +pln.propStf.collimation.convResolution = 0.5; %[mm] +pln.propStf.collimation.fieldWidth = fieldWidth; +pln.propStf.collimation.leafWidth = leafWidth; + +% +%[bixelFieldX,bixelFieldY] = ndgrid(-fieldWidth/2:bixelWidth:fieldWidth/2,-fieldWidth/2:leafWidth:fieldWidth/2); +[convFieldX,convFieldY] = meshgrid(-fieldWidth/2:convResolution:fieldWidth/2); + +%TODO: Not used in calcPhotonDose but imported from DICOM +%pln.propStf.collimation.Devices ... +%pln.propStf.collimation.numOfFields +%pln.propStf.collimation.beamMeterset + +for iBeam = 1:numel(stf) + stfTmp = stf(iBeam); + beamSequencing = sequencing.beam(iBeam); + beamAperture = apertureInfo.beam(iBeam); + + stfTmp.bixelWidth = 'field'; + + nShapes = beamSequencing.numOfShapes; + + stfTmp.numOfRays = 1;% + stfTmp.numOfBixelsPerRay = nShapes; + stfTmp.totalNumOfBixels = nShapes; + + ray = struct(); + ray.rayPos_bev = [0 0 0]; + ray.targetPoint_bev = [0 stfTmp.SAD 0]; + ray.weight = 1; + ray.energy = stfTmp.ray(1).energy; + ray.beamletCornersAtIso = stfTmp.ray(1).beamletCornersAtIso; + ray.rayCorners_SCD = stfTmp.ray(1).rayCorners_SCD; + + %ray.shape = beamSequencing.sum; + shapeTotalF = zeros(size(convFieldX)); + + ray.shapes = struct(); + for iShape = 1:nShapes + currShape = beamAperture.shape(iShape); + activeLeafPairPosY = beamAperture.leafPairPos; + F = zeros(size(convFieldX)); + if visBool + hF = figure; imagesc(F); title(sprintf('Beam %d, Shape %d',iBeam,iShape)); hold on; + end + for iLeafPair = 1:numel(activeLeafPairPosY) + posY = activeLeafPairPosY(iLeafPair); + ixY = convFieldY >= posY-leafWidth/2 & convFieldY < posY + leafWidth/2; + ixX = convFieldX >= currShape.leftLeafPos(iLeafPair) & convFieldX < currShape.rightLeafPos(iLeafPair); + ix = ixX & ixY; + F(ix) = 1; + if visBool + figure(hF); imagesc(F); drawnow; pause(0.1); + end + end + + if visBool + pause(1); close(hF); + end + + F = F*currShape.weight; + shapeTotalF = shapeTotalF + F; + + ray.shapes(iShape).convFluence = F; + ray.shapes(iShape).shapeMap = currShape.shapeMap; + ray.shapes(iShape).weight = currShape.weight; + ray.shapes(iShape).leftLeafPos = currShape.leftLeafPos; + ray.shapes(iShape).rightLeafPos = currShape.rightLeafPos; + ray.shapes(iShape).leafPairCenterPos = activeLeafPairPosY; + end + + ray.shape = shapeTotalF; + ray.weight = ones(1,nShapes); + ray.collimation = pln.propStf.collimation; + stfTmp.ray = ray; + + stf(iBeam) = stfTmp; +end + + From dc96b203ce848aec91920724b4df49c769512001 Mon Sep 17 00:00:00 2001 From: JenHardt <84377305+JenHardt@users.noreply.github.com> Date: Wed, 4 Mar 2026 13:39:36 +0100 Subject: [PATCH 15/20] revert files --- matRad/matRad_sequencingOld.m | 71 ++++ .../sequencing/matRad_engelLeafSequencing.m | 395 ++++++++++++++++++ .../matRad_sequencing2ApertureInfo.m | 179 ++++++++ .../sequencing/matRad_siochiLeafSequencing.m | 379 +++++++++++++++++ matRad/sequencing/matRad_xiaLeafSequencing.m | 284 +++++++++++++ 5 files changed, 1308 insertions(+) create mode 100644 matRad/matRad_sequencingOld.m create mode 100644 matRad/sequencing/matRad_engelLeafSequencing.m create mode 100644 matRad/sequencing/matRad_sequencing2ApertureInfo.m create mode 100644 matRad/sequencing/matRad_siochiLeafSequencing.m create mode 100644 matRad/sequencing/matRad_xiaLeafSequencing.m diff --git a/matRad/matRad_sequencingOld.m b/matRad/matRad_sequencingOld.m new file mode 100644 index 000000000..51207ada2 --- /dev/null +++ b/matRad/matRad_sequencingOld.m @@ -0,0 +1,71 @@ +function resultGUI = matRad_sequencing(resultGUI,stf,dij,pln,visBool) +% matRad inverse planning wrapper function +% +% call +% resultGUI = matRad_sequencing(resultGUI,stf,dij,pln) +% +% input +% dij: matRad dij struct +% stf: matRad stf struct +% pln: matRad pln struct +% resultGUI: struct containing optimized fluence vector, dose, and (for +% biological optimization) RBE-weighted dose etc. +% +% output +% resultGUI: struct containing optimized fluence vector, dose, and (for +% biological optimization) RBE-weighted dose etc. +% +% References +% - +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% 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/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_cfg = MatRad_Config.instance(); + +if nargin < 5 + visBool = 0; +end + +if ~isfield(pln,'propSeq') + pln.propSeq = struct('runSequencing',false); +end + +if strcmp(pln.radiationMode,'photons') && (pln.propSeq.runSequencing || pln.propOpt.runDAO) + + if ~isfield(pln.propSeq, 'sequencer') + pln.propSeq.sequencer = 'siochi'; % default: siochi sequencing algorithm + 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).') + end + + switch pln.propSeq.sequencer + case 'xia' + resultGUI = matRad_xiaLeafSequencing(resultGUI,stf,dij,pln.propSeq.sequencingLevel,visBool); + case 'engel' + resultGUI = matRad_engelLeafSequencing(resultGUI,stf,dij,pln.propSeq.sequencingLevel,visBool); + case 'siochi' + resultGUI = matRad_siochiLeafSequencing(resultGUI,stf,dij,pln.propSeq.sequencingLevel,visBool); + otherwise + matRad_cfg.dispError('Could not find specified sequencing algorithm ''%s''',pln.propSeq.sequencer); + end +elseif (pln.propSeq.runSequencing || pln.propOpt.runDAO) && ~strcmp(pln.radiationMode,'photons') + matRad_cfg.dispWarning('Sequencing is only specified for pln.radiationMode = "photons". Continuing with out sequencing ... ') +end +end + + diff --git a/matRad/sequencing/matRad_engelLeafSequencing.m b/matRad/sequencing/matRad_engelLeafSequencing.m new file mode 100644 index 000000000..bc1436574 --- /dev/null +++ b/matRad/sequencing/matRad_engelLeafSequencing.m @@ -0,0 +1,395 @@ +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 +% +% call +% resultGUI = matRad_engelLeafSequencing(resultGUI,stf,dij,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) +% +% output +% resultGUI: matRad result struct containing the new dose cube +% as well as the corresponding weights +% +% References +% [1] http://www.sciencedirect.com/science/article/pii/S0166218X05001411 +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% 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/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. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% if visBool not set toogle off visualization +if nargin < 5 + visBool = 0; +end + +numOfBeams = numel(stf); + +if visBool + % create the sequencing figure + sz = [800 1000]; % figure size + 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)]); +end + +offset = 0; + +for i = 1:numOfBeams + + numOfRaysPerBeam = stf(i).numOfRays; + + % get relevant weights for current beam + wOfCurrBeams = resultGUI.w(1+offset:numOfRaysPerBeam+offset).* ones(size(stf(i).ray,2),1); + + X = ones(size(stf(i).ray,2),1)*NaN; %this way it also works with3dconformal + Z = ones(size(stf(i).ray,2),1)*NaN; + + for j=1:size(stf(i).ray,2) + X(j) = stf(i).ray(j).rayPos_bev(:,1); + Z(j) = stf(i).ray(j).rayPos_bev(:,3); + end + + % sort bixels into matrix + minX = min(X); + maxX = max(X); + minZ = min(Z); + maxZ = max(Z); + + dimOfFluenceMxX = (maxX-minX)/stf(i).bixelWidth + 1; + dimOfFluenceMxZ = (maxZ-minZ)/stf(i).bixelWidth + 1; + + %Create the fluence matrix. + fluenceMx = zeros(dimOfFluenceMxZ,dimOfFluenceMxX); + + % Calculate X and Z position of every fluence's matrix spot + % z axis = axis of leaf movement! + xPos = (X-minX)/stf(i).bixelWidth+1; + zPos = (Z-minZ)/stf(i).bixelWidth+1; + + % Make subscripts for fluence matrix + indInFluenceMx = zPos + (xPos-1)*dimOfFluenceMxZ; + + %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); + + 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 + + % start sequencer + while max(D_k(:) > 0) + + %calculate the difference matrix diffMat + diffMat = diff([zeros(size(D_k,1),1) D_k zeros(size(D_k,1),1)],[],2); + + %calculate complexities + c = sum(max(0,diffMat),2); %TNMU-row-complexity + com = max(c); %TNMU complexity + g = com - c; %row complexity gap + + %initialize segment + segment = zeros(size(D_k)); + + k = k + 1; + + %Plot residual intensity matrix. + if visBool + seqSubPlots(2) = subplot(2,2,2,'parent',seqFig); + imagesc(D_k,'parent',seqSubPlots(2)); + set(seqSubPlots(2),'CLim',[0 numOfLevels],'YDir','normal'); + title(seqSubPlots(2),['k = ' num2str(k)]); + colorbar + drawnow + end + + + %loop over all rows + for j=1:size(D_0,1) + + %determine essential intervals + data(j).left(1) = 0; %left interval limit, actual for an empty interval + data(j).right(1) = 0; %right interal limit, actual for an empty interval + data(j).v(1) = g(j); %greatest number such that the inequalities (6) resp. (7) is satisfied with u=v + data(j).w(1) = inf; %smallest number in the interval + data(j).u(1) = data(j).v(1); %min(v,w) + + [~, pos, ~] = find(diffMat(j,:) > 0); % indices of all positive elements in the j. row of diffmat + [~, neg, ~] = find(diffMat(j,:) < 0); % indices of all negative elements in the j. row of diffMat + + n=2; + + %loop over the positive elements in the j. row of diffmat -> + %possible left interval limits + for m=1:size(pos,2) + + %loop over the negative elements in the j. row of diffMat -> + %possible right interval limit + for l=1:size(neg,2) + + %take only intervals I=[l,r] with l<=r + if pos(m) <= neg(l)-1 + + %set interval limits + data(j).left(n) = pos(m); + data(j).right(n) = neg(l)-1; + + %calculate v according to Lemma 8 + if g(j) <= abs( diffMat(j,pos(m)) + diffMat(j,neg(l)) ) + data(j).v(n) = min( diffMat(j,pos(m)), -diffMat(j,neg(l)) ) + g(j); + else + data(j).v(n) = ( diffMat(j, pos(m)) - diffMat(j, neg(l)) + g(j)) / 2; + end + + %calculate w and u according to equality (11) and + %(12) + data(j).w(n) = min(D_k(j,pos(m):(neg(l)-1))); + data(j).u(n) = min(data(j).v(n), data(j).w(n)); + + n = n+1; + end + end + end + + u(j) = max(data(j).u); + + end + + %calculate u_max from theorem 9 + d_k = min(u); + + %loop over all rows + for j=1:size(D_0,1) + + %find all possible (and essential) intervals + candidate = find(data(j).u >= d_k); + + %calculate the potential of the possible intervals + + %initialize p as -Inf + data(j).p(1:length(data(j).left)) = -Inf; + + %loop over all possible intervals + for s=1:size(candidate,2) + + if (s==1 && data(j).left(candidate(s)) == 0) + data(j).p(candidate(1)) = 0; + + + else + %calculate p1 according to equality (17) + if (d_k == diffMat(j, data(j).left(candidate(s))) && d_k ~= D_k(j, data(j).left(candidate(s)))) + p1 = 1; + + else + p1 = 0; + + end + + %calculate p2 according to equalitiy (18) + % if data(j).right(candidate(s)) < size(D_0, 2) + + if (d_k == -diffMat(j, data(j).right(candidate(s))+1) && d_k ~= D_k(j, data(j).right(candidate(s)))) + p2 = 1; + else + p2 = 0; + end + +% else +% +% if d_k == -diffMat(j, data(j).right(candidate(s))+1) +% p2 = 1; +% else +% p2 = 0; +% end +% +% end + + %calculate p3 according to equality (19) + p3 = size(find(D_k(j, data(j).left(candidate(s)):data(j).right(candidate(s))) == d_k),2); + + data(j).p(candidate(s)) = p1 + p2+ p3; + + end + + end + + %determinate intervals with maximum potential + maxPot = find(data(j).p == max(data(j).p)); + + %if several intervals have maximum potential, select + %the interval which has maximum length + if size(maxPot,2) > 1 + + for t=1:size(maxPot,2) + if t==1 && data(j).left(maxPot(t)) == 0 + data(j).l(1) = 0; + else + data(j).l(maxPot(t)) = data(j).right(maxPot(t)) - data(j).left(maxPot(t)) + 1; + end + end + + %data(j).l(maxPot) = data(j).right(maxPot) - data(j).left(maxPot) + 1; + + maxLength = find(data(j).l == max(data(j).l)); + + %left and right interval limits of the selected + %interval + leftIntLimit(j) = data(j).left(maxLength(1)); + rightIntLimit(j) = data(j).right(maxLength(1)); + + + else + + %left and right interval limits of the selected + %interval + leftIntLimit(j) = data(j).left(maxPot); + rightIntLimit(j) = data(j).right(maxPot); + + + end + + %create segment associated by the selected interval + if leftIntLimit(j) ~= 0 + + segment(j,leftIntLimit(j):rightIntLimit(j)) = 1; + + end + + end + + %write the segment in shape_k + shape_k = segment; + + %show the leaf positions + if visBool + seqSubPlots(4) = subplot(2,2,3.5,'parent',seqFig); + imagesc(shape_k,'parent',seqSubPlots(4)); + hold(seqSubPlots(4),'on'); + set(seqSubPlots(4),'YDir','normal') + xlabel(seqSubPlots(4),'x - direction parallel to leaf motion ') + ylabel(seqSubPlots(4),'z - direction perpendicular to leaf motion ') + title(seqSubPlots(4),['beam # ' num2str(i) ' shape # ' num2str(k) ' d_k = ' num2str(d_k)]); + for j = 1:dimOfFluenceMxZ + leftLeafIx = find(shape_k(j,:)>0,1,'first'); + rightLeafIx = find(shape_k(j,:)>0,1,'last'); + if leftLeafIx > 1 + plot(seqSubPlots(4),[.5 leftLeafIx-.5],j-[.5 .5] ,'w','LineWidth',2) + plot(seqSubPlots(4),[.5 leftLeafIx-.5],j+[.5 .5] ,'w','LineWidth',2) + plot(seqSubPlots(4),[ leftLeafIx-.5 leftLeafIx-.5],j+[.5 -.5] ,'w','LineWidth',2) + end + if rightLeafIx0 + + 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; + + 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); + end + + sequencing.w(1+offset:numOfRaysPerBeam+offset,1) = D_0(indInFluenceMx)/numOfLevels*calFac; + + offset = offset + numOfRaysPerBeam; + +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 +resultGUI.physicalDose = matRad_interp3(dij.doseGrid.x,dij.doseGrid.y',dij.doseGrid.z, ... + doseSequencedDoseGrid, ... + dij.ctGrid.x,dij.ctGrid.y',dij.ctGrid.z); + +% if weights exists from an former DAO remove it +if isfield(resultGUI,'wDao') + resultGUI = rmfield(resultGUI,'wDao'); +end + +end + diff --git a/matRad/sequencing/matRad_sequencing2ApertureInfo.m b/matRad/sequencing/matRad_sequencing2ApertureInfo.m new file mode 100644 index 000000000..867cabd43 --- /dev/null +++ b/matRad/sequencing/matRad_sequencing2ApertureInfo.m @@ -0,0 +1,179 @@ +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) +% +% input +% Sequencing: matRad sequencing result struct +% stf: matRad steering information 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/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. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% 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 +bixelIndOffset = 0; % used for creation of bixel index maps +totalNumOfBixels = sum([stf(:).totalNumOfBixels]); +totalNumOfShapes = sum([Sequencing.beam.numOfShapes]); +vectorOffset = totalNumOfShapes + 1; % used for bookkeeping in the vector for optimization + +% loop over all beams +for i=1:size(stf,2) + + %% 1. read stf and create maps (Ray & Bixelindex) + + % get x- and z-coordinates of bixels + rayPos_bev = reshape([stf(i).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(i).bixelWidth + 1; + dimZ = (maxZ-minZ)/stf(i).bixelWidth + 1; + + rayMap = zeros(dimZ,dimX); + + % get indices for x and z positions + xPos = (X-minX)/stf(i).bixelWidth + 1; + zPos = (Z-minZ)/stf(i).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(i).numOfRays] + bixelIndOffset; + bixelIndOffset = bixelIndOffset + stf(i).numOfRays; + + % store physical position of first entry in bixelIndMap + posOfCornerBixel = [minX 0 minZ]; + + % get leaf limits from the leaf map + lim_l = NaN * ones(dimZ,1); + lim_r = NaN * ones(dimZ,1); + % looping oder leaf pairs + for l = 1:dimZ + lim_lInd = find(rayMap(l,:),1,'first'); + lim_rInd = find(rayMap(l,:),1,'last'); + % the physical position [mm] can be calculated from the indices + lim_l(l) = (lim_lInd-1)*bixelWidth + minX - 1/2*bixelWidth; + lim_r(l) = (lim_rInd-1)*bixelWidth + minX + 1/2*bixelWidth; + end + + % get leaf positions for all shapes + % leaf positions can be extracted from the shapes created in Sequencing + 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; + + 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; + apertureInfo.beam(i).shape(m).vectorOffset = vectorOffset; + + % update index for bookkeeping + vectorOffset = vectorOffset + dimZ; + + 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]; + + % save data for each beam + apertureInfo.beam(i).numOfShapes = Sequencing.beam(i).numOfShapes; + apertureInfo.beam(i).numOfActiveLeafPairs = dimZ; + apertureInfo.beam(i).leafPairPos = leafPairPos; + apertureInfo.beam(i).isActiveLeafPair = isActiveLeafPair; + apertureInfo.beam(i).centralLeafPair = centralLeafPair; + apertureInfo.beam(i).lim_l = lim_l; + apertureInfo.beam(i).lim_r = lim_r; + apertureInfo.beam(i).bixelIndMap = bixelIndMap; + apertureInfo.beam(i).posOfCornerBixel = posOfCornerBixel; + apertureInfo.beam(i).MLCWindow = MLCWindow; + +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]'); + +% create vectors for optimization +[apertureInfo.apertureVector, apertureInfo.mappingMx, apertureInfo.limMx] = matRad_OptimizationProblemDAO.matRad_daoApertureInfo2Vec(apertureInfo); + +end diff --git a/matRad/sequencing/matRad_siochiLeafSequencing.m b/matRad/sequencing/matRad_siochiLeafSequencing.m new file mode 100644 index 000000000..02841d5b0 --- /dev/null +++ b/matRad/sequencing/matRad_siochiLeafSequencing.m @@ -0,0 +1,379 @@ +function resultGUI = matRad_siochiLeafSequencing(resultGUI,stf,dij,numOfLevels,visBool) +% multileaf collimator leaf sequencing algorithm +% for intensity modulated beams with multiple static segments according to +% Siochi (1999)International Journal of Radiation Oncology * Biology * Physics, +% originally implemented in PLUNC (https://sites.google.com/site/planunc/) +% +% Implemented in matRad by Eric Christiansen, Emily Heath, and Tong Xu +% +% call +% resultGUI = +% matRad_siochiLeafSequencing(resultGUI,stf,dij,numOfLevels) +% resultGUI = +% matRad_siochiLeafSequencing(resultGUI,stf,dij,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) +% +% output +% resultGUI: matRad result struct containing the new dose cube +% as well as the corresponding weights +% +% References +% [1] https://www.ncbi.nlm.nih.gov/pubmed/10078655 +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% 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/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. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% if visBool not set toogle off visualization +if nargin < 5 + visBool = 0; +end + +numOfBeams = numel(stf); + +if visBool + % create the sequencing figure + sz = [800 1000]; % figure size + 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)]); +end + +offset = 0; + +if ~isfield(resultGUI,'wUnsequenced') + wUnsequenced = resultGUI.w; +else + wUnsequenced = resultGUI.wUnsequenced; +end + +for i = 1:numOfBeams + + numOfRaysPerBeam = stf(i).numOfRays; + + % get relevant weights for current beam + wOfCurrBeams = wUnsequenced(1+offset:numOfRaysPerBeam+offset).* ones(size(stf(i).ray,2),1);%REVIEW OFFSET + + X = ones(size(stf(i).ray,2),1)*NaN; %this way it also works with3dconformal + Z = ones(size(stf(i).ray,2),1)*NaN; + + for j = 1:size(stf(i).ray,2) + X(j) = stf(i).ray(j).rayPos_bev(:,1); + Z(j) = stf(i).ray(j).rayPos_bev(:,3); + end + + % sort bixels into matrix + minX = min(X); + maxX = max(X); + minZ = min(Z); + maxZ = max(Z); + + dimOfFluenceMxX = (maxX-minX)/stf(i).bixelWidth + 1; + dimOfFluenceMxZ = (maxZ-minZ)/stf(i).bixelWidth + 1; + + %Create the fluence matrix. + fluenceMx = zeros(dimOfFluenceMxZ,dimOfFluenceMxX); + + % Calculate X and Z position of every fluence's matrix spot z axis = + % axis of leaf movement! + xPos = (X-minX)/stf(i).bixelWidth+1; + zPos = (Z-minZ)/stf(i).bixelWidth+1; + + % Make subscripts for fluence matrix + indInFluenceMx = zPos + (xPos-1)*dimOfFluenceMxZ; + + %Save weights in fluence matrix. + fluenceMx(indInFluenceMx) = wOfCurrBeams; + + % Stratification + calFac = max(fluenceMx(:)); + D_k = round(fluenceMx/calFac*numOfLevels); + + % Save the stratification in the initial intensity matrix D_0. + D_0 = D_k; + + % container to remember generated shapes; allocate space for 10000 + % shapes + shapes = NaN*ones(dimOfFluenceMxZ,dimOfFluenceMxX,10000); + shapesWeight = zeros(10000,1); + k = 0; + + if visBool + clf(seqFig); + colormap(seqFig,'jet'); + + seqSubPlots(1) = subplot(2,2,1,'parent',seqFig); + imagesc(D_k,'parent',seqSubPlots(1)); + set(seqSubPlots(1),'CLim',[0 numOfLevels],'YDir','normal'); + title(seqSubPlots(1),['Beam # ' num2str(i) ': max(D_0) = ' num2str(max(D_0(:))) ' - ' num2str(numel(unique(D_0))) ' intensity levels']); + xlabel(seqSubPlots(1),'x - direction parallel to leaf motion ') + ylabel(seqSubPlots(1),'z - direction perpendicular to leaf motion ') + colorbar; + drawnow + 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); + + 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 + + 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 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 + offset = offset + numOfRaysPerBeam; + +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 +resultGUI.physicalDose = matRad_interp3(dij.doseGrid.x,dij.doseGrid.y',dij.doseGrid.z, ... + doseSequencedDoseGrid, ... + dij.ctGrid.x,dij.ctGrid.y',dij.ctGrid.z); + +% if weights exists from an former DAO remove it +if isfield(resultGUI,'wDao') + resultGUI = rmfield(resultGUI,'wDao'); +end + +end + +function [tops, bases] = matRad_siochiDecomposePort(map,dimZ,dimX,minZ,maxZ,minX,maxX) +%Returns tops and bases of a fluence matrix "map" for Siochi leaf +%sequencing algorithm (rod pushing part). Accounts for collisions and +%tongue and groove (Tng) effects. + +tops = zeros(dimZ, dimX); +bases = zeros(dimZ, dimX); + +for i = minX:maxX + maxTop = -1; + TnG = 1; + for j = minZ:maxZ + if i == minX + bases(j,i) = 1; + tops(j,i) = bases(j,i)+map(j,i)-1; + else %assign trial base positions + if map(j,i) >= map(j,i-1) %current rod >= previous, match the bases + bases(j,i) = bases(j,i-1); + tops(j,i) = bases(j,i)+map(j,i)-1; + else %current rod maxTop + maxTop = tops(j,i); + maxRow = j; + end + end + + %Correct for collision and tongue and groove error + while(TnG) + %go from maxRow down checking for TnG. This occurs when a shorter + %rod is "peeking over" a longer one in the direction transverse to + %the leaf motion. To fix this, match either the tops or bases of + %the rods. + for j = (maxRow-1):-1:minZ + if map(j,i) < map(j+1,i) + if tops(j,i) > tops(j+1,i) + tops(j+1,i) = tops(j,i); + bases(j+1,i) = tops(j+1,i)-map(j+1,i)+1; + elseif bases(j,i) < bases(j+1,i) + bases(j,i) = bases(j+1,i); + tops(j,i) = bases(j,i)+map(j,i)-1; + end + else + if tops(j,i) < tops(j+1,i) + tops(j,i) = tops(j+1,i); + bases(j,i) = tops(j,i)-map(j,i)+1; + elseif bases(j,i) > bases(j+1,i) + bases(j+1,i) = bases(j,i); + tops(j+1,i) = bases(j+1,i)+map(j+1,i)-1; + end + end + end + %go from maxRow up checking for TnG + for j = (maxRow+1):maxZ + if map(j,i) < map(j-1,i) + if tops(j,i) > tops(j-1,i) + tops(j-1,i) = tops(j,i); + bases(j-1,i) = tops(j-1,i)-map(j-1,i)+1; + elseif bases(j,i) < bases(j-1,i) + bases(j,i) = bases(j-1,i); + tops(j,i) = bases(j,i)+map(j,i)-1; + end + else + if tops(j,i) < tops(j-1,i) + tops(j,i) = tops(j-1,i); + bases(j,i) = tops(j,i)-map(j,i)+1; + elseif bases(j,i) > bases(j-1,i) + bases(j-1,i) = bases(j,i); + tops(j-1,i) = bases(j-1,i)+map(j-1,i)-1; + end + end + end + %now check if all TnG conditions have been removed + TnG = 0; + for j = (minZ+1):maxZ + if map(j,i) < map(j-1,i); + if tops(j,i) > tops(j-1,i) + TnG = 1; + elseif bases(j,i) < bases(j-1,i) + TnG = 1; + end + else + if tops(j,i) < tops(j-1,i) + TnG = 1; + elseif bases(j,i) > bases(j-1,i) + TnG = 1; + end + end + end + end +end + +end + +function [shapes,shapesWeight,k,D_k] = matRad_siochiConvertToSegments(shapes,shapesWeight,k,tops,bases,visBool,i,D_k,numOfLevels,seqFig,seqSubPlots) +%Convert tops and bases to shape matrices. These are taken as to be the +%shapes of uniform level/elevation after the rods are pushed. +if nargin < 6 + visBool = 0; +end + + +levels = max(tops(:)); + +for level = 1:levels + %check if slab is new + if matRad_siochiDifferentSlab(tops,bases,level) + k = k+1; %increment number of unique slabs + shape_k = (bases <= level).*(level <= tops); %shape of current slab + shapes(:,:,k) = shape_k; + end + shapesWeight(k) = shapesWeight(k)+1; %if slab is not unique, this increments weight again + + if visBool + %show the leaf positions + [dimZ,dimX] = size(tops); + seqSubPlots(4) = subplot(2,2,3.5,'parent',seqFig); + imagesc(shape_k,'parent',seqSubPlots(4)); + hold(seqSubPlots(4),'on'); + set(seqSubPlots(4),'YDir','normal') + xlabel(seqSubPlots(4),'x - direction parallel to leaf motion ') + ylabel(seqSubPlots(4),'z - direction perpendicular to leaf motion ') + title(seqSubPlots(4),['beam # ' num2str(i) ' shape # ' num2str(k) ' d_k = ' num2str(shapesWeight(k))]); + for j = 1:dimZ + leftLeafIx = find(shape_k(j,:)>0,1,'first'); + rightLeafIx = find(shape_k(j,:)>0,1,'last'); + if leftLeafIx > 1 + plot(seqSubPlots(4),[.5 leftLeafIx-.5],j-[.5 .5] ,'w','LineWidth',2) + plot(seqSubPlots(4),[.5 leftLeafIx-.5],j+[.5 .5] ,'w','LineWidth',2) + plot(seqSubPlots(4),[ leftLeafIx-.5 leftLeafIx-.5],j+[.5 -.5] ,'w','LineWidth',2) + end + if rightLeafIx 0 + + k = k + 1; + + %Plot residual intensity matrix. + if visBool + seqSubPlots(2) = subplot(2,2,2,'parent',seqFig); + imagesc(D_k,'parent',seqSubPlots(2)); + set(seqSubPlots(2),'CLim',[0 L_0],'YDir','normal'); + title(seqSubPlots(2),['k = ' num2str(k) ' - ' num2str(numel(unique(D_k))) ' intensity levels remaining...']); + xlabel(seqSubPlots(2),'x - direction parallel to leaf motion '); + ylabel(seqSubPlots(2),'z - direction perpendicular to leaf motion '); + colorbar + drawnow + end + + %Rounded off integer. Equation 7. + m = floor(log2(L_k)); + + % Convert m=1 if is less than 1. This happens when L_k belong to ]0,2[ + if m < 1 + m = 1; + end + + %Calculate the delivery intensity unit. Equation 6. + d_k = floor(2^(m-1)); + + % Opening matrix. + openingMx = D_k >= d_k; + + % Plot opening matrix. + if visBool + seqSubPlots(3) = subplot(2,2,3,'parent',seqFig); + imagesc(openingMx,'parent',seqSubPlots(3)); + set(seqSubPlots(3),'YDir','normal') + xlabel(seqSubPlots(3),'x - direction parallel to leaf motion ') + ylabel(seqSubPlots(3),'z - direction perpendicular to leaf motion ') + title(seqSubPlots(3),'Opening matrix'); + drawnow + end + + if strcmp(mode,'sw') % sliding window technique! + for j = 1:dimOfFluenceMxZ + openIx = find(openingMx(j,:) == 1,1,'first'); + if ~isempty(openIx) + closeIx = find(openingMx(j,openIx+1:end) == 0,1,'first'); + if ~isempty(closeIx) + openingMx(j,openIx+closeIx:end) = 0; + end + end + + end + elseif strcmp(mode,'rl') % reducing levels technique! + for j = 1:dimOfFluenceMxZ + [maxVal,maxIx] = max(openingMx(j,:) .* D_k(j,:)); + if maxVal > 0 + closeIx = maxIx + find(openingMx(j,maxIx+1:end) == 0,1,'first'); + if ~isempty(closeIx) + openingMx(j,closeIx:end) = 0; + end + openIx = find(openingMx(j,1:maxIx-1) == 0,1,'last'); + if ~isempty(openIx) + openingMx(j,1:openIx) = 0; + end + end + + end + + end + + shape_k = openingMx * d_k; + + if visBool + seqSubPlots(4) = subplot(2,2,4,'parent',seqFig); + imagesc(shape_k,'parent',seqSubPlots(4)); + set(seqSubPlots(4),'YDir','normal') + hold(seqSubPlots(4),'on'); + xlabel(seqSubPlots(4),'x - direction parallel to leaf motion ') + ylabel(seqSubPlots(4),'z - direction perpendicular to leaf motion ') + title(seqSubPlots(4),['beam # ' num2str(i) ' shape # ' num2str(k) ' d_k = ' num2str(d_k)]); + for j = 1:dimOfFluenceMxZ + leftLeafIx = find(shape_k(j,:)>0,1,'first'); + rightLeafIx = find(shape_k(j,:)>0,1,'last'); + if leftLeafIx > 1 + plot(seqSubPlots(4),[.5 leftLeafIx-.5],j-[.5 .5] ,'w','LineWidth',2) + plot(seqSubPlots(4),[.5 leftLeafIx-.5],j+[.5 .5] ,'w','LineWidth',2) + plot(seqSubPlots(4),[ leftLeafIx-.5 leftLeafIx-.5],j+[.5 -.5] ,'w','LineWidth',2) + end + if rightLeafIx0 + + 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; + + 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); + end + + + sequencing.w(1+offset:numOfRaysPerBeam+offset,1) = D_0(indInFluenceMx)/numOfLevels*calFac; + + offset = offset + numOfRaysPerBeam; + +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 +resultGUI.physicalDose = matRad_interp3(dij.doseGrid.x,dij.doseGrid.y',dij.doseGrid.z, ... + doseSequencedDoseGrid, ... + dij.ctGrid.x,dij.ctGrid.y',dij.ctGrid.z); + +% if weights exists from an former DAO remove it +if isfield(resultGUI,'wDao') + resultGUI = rmfield(resultGUI,'wDao'); +end + +end + From f28cf6421c221f193121e0d6edcc648123524715 Mon Sep 17 00:00:00 2001 From: JenHardt <84377305+JenHardt@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:13:22 +0100 Subject: [PATCH 16/20] updates test --- test/doseCalc/test_TopasMCEngine.m | 33 ++++++++++++--------- test/sequencing/test_engelLeafSequencing.m | 20 ++++++------- test/sequencing/test_siochiLeafSequencing.m | 20 ++++++------- test/sequencing/test_xiaLeafSequencing.m | 20 ++++++------- 4 files changed, 49 insertions(+), 44 deletions(-) diff --git a/test/doseCalc/test_TopasMCEngine.m b/test/doseCalc/test_TopasMCEngine.m index 361463771..1c9eb8f7e 100644 --- a/test/doseCalc/test_TopasMCEngine.m +++ b/test/doseCalc/test_TopasMCEngine.m @@ -43,13 +43,15 @@ w = ones(1,sum([stf(:).totalNumOfBixels])); if strcmp(radModes{i},'photons') - pln.propOpt.runSequencing = 1; pln.propOpt.runDAO = 1; dij = matRad_calcDoseInfluence(ct,cst,stf,pln); resultGUI = matRad_calcCubes(ones(dij.totalNumOfBixels,1),dij); resultGUI.wUnsequenced = ones(dij.totalNumOfBixels,1); - resultGUI = matRad_siochiLeafSequencing(resultGUI,stf,dij,5); - [pln,stf] = matRad_aperture2collimation(pln,stf,resultGUI.sequencing,resultGUI.apertureInfo); + pln.propSeq.sequencer = 'siochi'; + pln.propSeq.sequencingLevel = 5; + sequencer = matRad_SequencerBase.getSequencerFromPln(pln); + resultGUI = matRad_sequencing(resultGUI, stf, pln); + [pln,stf] = sequencer.aperture2collimation(pln, stf, resultGUI.sequencing); w = resultGUI.w; pln.propDoseCalc.beamProfile = 'phasespace'; end @@ -139,13 +141,14 @@ w = ones(1,sum([stf(:).totalNumOfBixels])); if strcmp(radModes{i},'photons') - pln.propOpt.runSequencing = 1; - pln.propOpt.runDAO = 1; dij = matRad_calcDoseInfluence(ct,cst,stf,pln); resultGUI = matRad_calcCubes(ones(dij.totalNumOfBixels,1),dij); resultGUI.wUnsequenced = ones(dij.totalNumOfBixels,1); - resultGUI = matRad_siochiLeafSequencing(resultGUI,stf,dij,5); - [pln,stf] = matRad_aperture2collimation(pln,stf,resultGUI.sequencing,resultGUI.apertureInfo); + pln.propSeq.sequencer = 'siochi'; + pln.propSeq.sequencingLevel = 5; + sequencer = matRad_SequencerBase.getSequencerFromPln(pln); + resultGUI = matRad_sequencing(resultGUI, stf, pln); + [pln,stf] = sequencer.aperture2collimation(pln, stf, resultGUI.sequencing); pln.propDoseCalc.beamProfile = 'phasespace'; w = resultGUI.w; end @@ -187,17 +190,18 @@ % physical Dose for i = 1:numel(radModes) - if ~strcmp(radModes{i},'photons') + if ~ismember(radModes{i}, {'photons', 'VHEE'}) load([radModes{i} '_testData.mat']); [ct,cst] = matRad_addMovement(ct, cst,5, numOfPhases,[0 3 0],'dvfType','pull'); pln.bioModel = matRad_bioModel(radModes{i},'none'); resultGUI.w = ones(1,sum([stf(:).totalNumOfBixels]))'; - timeSequence = matRad_makeBixelTimeSeq(stf, resultGUI); - timeSequence = matRad_makePhaseMatrix(timeSequence, ct.numOfCtScen, ct.motionPeriod, 'linear'); + resultGUI = matRad_sequencing(resultGUI, stf, pln); + sequencer = matRad_ParticleSequencer(); + resultGUI.sequencing = sequencer.makePhaseMatrix(resultGUI.sequencing, ct.numOfCtScen, ct.motionPeriod); pln.propDoseCalc.engine = 'TOPAS'; pln.propDoseCalc.externalCalculation = 'write'; pln.propDoseCalc.calc4DInterplay = true; - pln.propDoseCalc.calcTimeSequence = timeSequence; + pln.propDoseCalc.calcTimeSequence = resultGUI.sequencing; pln.propDoseCalc.numHistoriesDirect = 1e6; resultGUI = matRad_calcDoseForward(ct,cst,stf,pln, resultGUI.w); @@ -233,12 +237,13 @@ [ct,cst] = matRad_addMovement(ct, cst,5, numOfPhases,[0 3 0],'dvfType','pull'); pln.bioModel = matRad_bioModel(radModes{i},'none'); resultGUI.w = ones(1,sum([stf(:).totalNumOfBixels]))'; - timeSequence = matRad_makeBixelTimeSeq(stf, resultGUI); - timeSequence = matRad_makePhaseMatrix(timeSequence, ct.numOfCtScen, ct.motionPeriod, 'linear'); + resultGUI = matRad_sequencing(resultGUI, stf, pln); + sequencer = matRad_ParticleSequencer(); + resultGUI.sequencing = sequencer.makePhaseMatrix(resultGUI.sequencing, ct.numOfCtScen, ct.motionPeriod); pln.propDoseCalc.engine = 'TOPAS'; pln.propDoseCalc.externalCalculation = 'write'; pln.propDoseCalc.calc4DInterplay = true; - pln.propDoseCalc.calcTimeSequence = timeSequence; + pln.propDoseCalc.calcTimeSequence = resultGUI.sequencing; pln.propDoseCalc.numHistoriesDirect = 1e6; pln.propDoseCalc.scorer.RBE = true; pln.propDoseCalc.scorer.RBE_model = RBEmodel; diff --git a/test/sequencing/test_engelLeafSequencing.m b/test/sequencing/test_engelLeafSequencing.m index a07546c29..0902e10d0 100644 --- a/test/sequencing/test_engelLeafSequencing.m +++ b/test/sequencing/test_engelLeafSequencing.m @@ -12,21 +12,23 @@ % Test Case, and add them to the test-runner initTestSuite; - function [resultGUI,stf,dij] = helper_getTestData() + function [resultGUI,stf,dij,pln] = helper_getTestData() p = load('photons_testData.mat'); + pln = p.pln; + pln.propSeq.sequencer = 'engel'; resultGUI = p.resultGUI; stf = p.stf; dij = p.dij; function test_run_sequencing_basic - [resultGUI,stf,dij] = helper_getTestData(); + [resultGUI,stf,dij,pln] = helper_getTestData(); fn_old = fieldnames(resultGUI); numOfLevels = [1,10]; for levels = numOfLevels - resultGUI_sequenced = matRad_engelLeafSequencing(resultGUI,stf,dij,levels); + resultGUI_sequenced = matRad_sequencing(resultGUI,stf,pln); fn_new = fieldnames(resultGUI_sequenced); for i = 1:numel(fn_old) @@ -35,27 +37,25 @@ end % Basic additions to resultGUI - assertTrue(isvector(resultGUI_sequenced.wSequenced)); - assertTrue(isstruct(resultGUI_sequenced.apertureInfo)); + assertTrue(isstruct(resultGUI_sequenced.sequencing.apertureInfo)); assertTrue(isstruct(resultGUI_sequenced.sequencing)); %Sequencing Struct seq = resultGUI_sequenced.sequencing; assertTrue(isstruct(seq.beam)); assertTrue(numel(seq.beam) == numel(stf)); - for i = 1:numel(seq.beam) + for i = 1:size(seq.beam,2) assertTrue(isscalar(seq.beam(i).numOfShapes)); assertTrue(isnumeric(seq.beam(i).shapes)); - shapeSize = size(seq.beam(i).shapes); - assertEqual(shapeSize(3),seq.beam(i).numOfShapes); + assertEqual(size(seq.beam(i).shapes,3),seq.beam(i).numOfShapes); assertTrue(isvector(seq.beam(i).shapesWeight)); assertTrue(isvector(seq.beam(i).bixelIx)); assertTrue(ismatrix(seq.beam(i).fluence)); - assertEqual(size(seq.beam(i).fluence),shapeSize([1 2])); + assertEqual(size(seq.beam(i).fluence),size(seq.beam(i).shapes,[1,2])); end %ApertureInfo Sturct - apInfo = resultGUI_sequenced.apertureInfo; + apInfo = resultGUI_sequenced.sequencing.apertureInfo; assertTrue(isscalar(apInfo.bixelWidth)); assertTrue(isscalar(apInfo.numOfMLCLeafPairs)); assertTrue(isscalar(apInfo.totalNumOfBixels)); diff --git a/test/sequencing/test_siochiLeafSequencing.m b/test/sequencing/test_siochiLeafSequencing.m index a68b3ee47..f6431ba5a 100644 --- a/test/sequencing/test_siochiLeafSequencing.m +++ b/test/sequencing/test_siochiLeafSequencing.m @@ -12,21 +12,23 @@ % Test Case, and add them to the test-runner initTestSuite; - function [resultGUI,stf,dij] = helper_getTestData() +function [resultGUI,stf,dij,pln] = helper_getTestData() p = load('photons_testData.mat'); + pln = p.pln; + pln.propSeq.sequencer = 'siochi'; resultGUI = p.resultGUI; stf = p.stf; dij = p.dij; function test_run_sequencing_basic - [resultGUI,stf,dij] = helper_getTestData(); + [resultGUI,stf,dij,pln] = helper_getTestData(); fn_old = fieldnames(resultGUI); numOfLevels = [1,10]; for levels = numOfLevels - resultGUI_sequenced = matRad_siochiLeafSequencing(resultGUI,stf,dij,levels); + resultGUI_sequenced = matRad_sequencing(resultGUI,stf,pln); fn_new = fieldnames(resultGUI_sequenced); for i = 1:numel(fn_old) @@ -35,27 +37,25 @@ end % Basic additions to resultGUI - assertTrue(isvector(resultGUI_sequenced.wSequenced)); - assertTrue(isstruct(resultGUI_sequenced.apertureInfo)); + assertTrue(isstruct(resultGUI_sequenced.sequencing.apertureInfo)); assertTrue(isstruct(resultGUI_sequenced.sequencing)); %Sequencing Struct seq = resultGUI_sequenced.sequencing; assertTrue(isstruct(seq.beam)); assertTrue(numel(seq.beam) == numel(stf)); - for i = 1:numel(seq.beam) + for i = 1:size(seq.beam,2) assertTrue(isscalar(seq.beam(i).numOfShapes)); assertTrue(isnumeric(seq.beam(i).shapes)); - shapeSize = size(seq.beam(i).shapes); - assertEqual(shapeSize(3),seq.beam(i).numOfShapes); + assertEqual(size(seq.beam(i).shapes,3),seq.beam(i).numOfShapes); assertTrue(isvector(seq.beam(i).shapesWeight)); assertTrue(isvector(seq.beam(i).bixelIx)); assertTrue(ismatrix(seq.beam(i).fluence)); - assertEqual(size(seq.beam(i).fluence),shapeSize([1 2])); + assertEqual(size(seq.beam(i).fluence),size(seq.beam(i).shapes,[1,2])); end %ApertureInfo Sturct - apInfo = resultGUI_sequenced.apertureInfo; + apInfo = resultGUI_sequenced.sequencing.apertureInfo; assertTrue(isscalar(apInfo.bixelWidth)); assertTrue(isscalar(apInfo.numOfMLCLeafPairs)); assertTrue(isscalar(apInfo.totalNumOfBixels)); diff --git a/test/sequencing/test_xiaLeafSequencing.m b/test/sequencing/test_xiaLeafSequencing.m index a4fabcc2d..e4b146fd8 100644 --- a/test/sequencing/test_xiaLeafSequencing.m +++ b/test/sequencing/test_xiaLeafSequencing.m @@ -12,21 +12,23 @@ % Test Case, and add them to the test-runner initTestSuite; -function [resultGUI,stf,dij] = helper_getTestData() +function [resultGUI,stf,dij,pln] = helper_getTestData() p = load('photons_testData.mat'); + pln = p.pln; + pln.propSeq.sequencer = 'xia'; resultGUI = p.resultGUI; stf = p.stf; dij = p.dij; function test_run_sequencing_basic - [resultGUI,stf,dij] = helper_getTestData(); + [resultGUI,stf,dij,pln] = helper_getTestData(); fn_old = fieldnames(resultGUI); numOfLevels = [1,10]; for levels = numOfLevels - resultGUI_sequenced = matRad_xiaLeafSequencing(resultGUI,stf,dij,levels); + resultGUI_sequenced = matRad_sequencing(resultGUI,stf,pln); fn_new = fieldnames(resultGUI_sequenced); for i = 1:numel(fn_old) @@ -35,27 +37,25 @@ end % Basic additions to resultGUI - assertTrue(isvector(resultGUI_sequenced.wSequenced)); - assertTrue(isstruct(resultGUI_sequenced.apertureInfo)); + assertTrue(isstruct(resultGUI_sequenced.sequencing.apertureInfo)); assertTrue(isstruct(resultGUI_sequenced.sequencing)); %Sequencing Struct seq = resultGUI_sequenced.sequencing; assertTrue(isstruct(seq.beam)); assertTrue(numel(seq.beam) == numel(stf)); - for i = 1:numel(seq.beam) + for i = 1:size(seq.beam,2) assertTrue(isscalar(seq.beam(i).numOfShapes)); assertTrue(isnumeric(seq.beam(i).shapes)); - shapeSize = size(seq.beam(i).shapes); - assertEqual(shapeSize(3),seq.beam(i).numOfShapes); + assertEqual(size(seq.beam(i).shapes,3),seq.beam(i).numOfShapes); assertTrue(isvector(seq.beam(i).shapesWeight)); assertTrue(isvector(seq.beam(i).bixelIx)); assertTrue(ismatrix(seq.beam(i).fluence)); - assertEqual(size(seq.beam(i).fluence),shapeSize([1 2])); + assertEqual(size(seq.beam(i).fluence),size(seq.beam(i).shapes,[1,2])); end %ApertureInfo Sturct - apInfo = resultGUI_sequenced.apertureInfo; + apInfo = resultGUI_sequenced.sequencing.apertureInfo; assertTrue(isscalar(apInfo.bixelWidth)); assertTrue(isscalar(apInfo.numOfMLCLeafPairs)); assertTrue(isscalar(apInfo.totalNumOfBixels)); From 2a138b5cf4b4fa56a6addea20bb86425920303ef Mon Sep 17 00:00:00 2001 From: JenHardt <84377305+JenHardt@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:15:12 +0100 Subject: [PATCH 17/20] fill empty bixels only for photons so not to interfer so much and update of test data --- ...Rad_StfGeneratorExternalRayBixelAbstract.m | 2 +- ...atRad_StfGeneratorPhotonRayBixelAbstract.m | 3 ++- test/testData/helper_testDataCreater.m | 2 +- test/testData/photons_testData.mat | Bin 74152 -> 66222 bytes 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/matRad/steering/matRad_StfGeneratorExternalRayBixelAbstract.m b/matRad/steering/matRad_StfGeneratorExternalRayBixelAbstract.m index cb792cb6f..282d52498 100644 --- a/matRad/steering/matRad_StfGeneratorExternalRayBixelAbstract.m +++ b/matRad/steering/matRad_StfGeneratorExternalRayBixelAbstract.m @@ -20,7 +20,7 @@ couchAngles = 0; bixelWidth = 0; isoCenter - fillEmptyBixels = true; + fillEmptyBixels; centered end diff --git a/matRad/steering/matRad_StfGeneratorPhotonRayBixelAbstract.m b/matRad/steering/matRad_StfGeneratorPhotonRayBixelAbstract.m index 44a4d4b6a..acc4818ba 100644 --- a/matRad/steering/matRad_StfGeneratorPhotonRayBixelAbstract.m +++ b/matRad/steering/matRad_StfGeneratorPhotonRayBixelAbstract.m @@ -14,7 +14,6 @@ % LICENSE file. % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - methods function this = matRad_StfGeneratorPhotonRayBixelAbstract(pln) % Constructs ExternalStfGenerator with or without pln @@ -22,10 +21,12 @@ pln = []; end this@matRad_StfGeneratorExternalRayBixelAbstract(pln); + end function setDefaults(this) % Set default values for ExternalStfGenerator + this.fillEmptyBixels = true; this.setDefaults@matRad_StfGeneratorExternalRayBixelAbstract(); end end diff --git a/test/testData/helper_testDataCreater.m b/test/testData/helper_testDataCreater.m index daec8339e..46637db90 100644 --- a/test/testData/helper_testDataCreater.m +++ b/test/testData/helper_testDataCreater.m @@ -39,7 +39,7 @@ clear VolHelper ixBody ixTarget i %% create pln, stf -radModes = ["protons","helium","carbon","VHEE"]; +radModes = ["photons","protons","helium","carbon","VHEE"]; for radMode = radModes %radMode = 'carbon'; %protons,helium,carbon; diff --git a/test/testData/photons_testData.mat b/test/testData/photons_testData.mat index c595876bc8bd558351ecb87f3b5eeeec98c014a8..ca11ba1149a58543ce9a7f565d7a631f3ee21be9 100644 GIT binary patch literal 66222 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 z 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

nrE z_@BS<{~K^YZbX{_qFYIB)LE}Jy*7_G#LOi=ALO6__Y>?!@k=UmS zS2~b1Jh8Ri0oZa&ciD0qz8>fKPJQpUIm+^=I-eeFoM0TMuK&+*Z*W^A8LpG?Kj*~6Xj9S}poY)xrH#l`DGWnZicam7^3EGA?gd&M043ZOuux}UxgY}`5yzF9 zYJD2S?bgE4M1OgYem)PCGM%kg3D^_=ma7_Ot{DnhSozj~{&9A(3+j(p82H-kn=ut)zHgG!{g>O3Pcq%PFx(0`n2sfN_zcEg*t`IGP2~?2L{!68x7ikd9Yi(Gm_Yl@ zpoX$trN-QO6{Rps_m&R55xc(5WauzqH2O_4?{brf+L|B7_@?9)N(Xa|bwT|A=i93L zvET@b(@Uh+%!}ZEOaH+(cnn40%2Xb&K!+HnkSkuS)R`-vk#1$;&J;sTS)9hy+vCR~ zxNGJv9aD&EbEvD|S7YH<_M8&h;p)8{z#?s{)6wELHzB-;+BvMrU31%?kD{KK7-1;{PaKhhp;_79UZ7tOvWs09qf5+Gk6 zq1^bzR+a{B{u!Kfp3>m9b24;`Xc%ZZYyu11lv>&QK40^wPM-2yINVI%&(1X z(#Ox0O0?Zc1|))AHuWR$OTUM(sM4Arc9^MUYmyP9GKOLK3E|rDZMQ7IU(|Pko;3}925cuLs zw*mg~l2z}@Wf%}Ukz^4c{lCY%Nsw39Fxqw~8*Z;mz;KdR6cqU|2d&_TPqi3QAGi|s z6D#gRDV$-g+8Hecxd!E*Vo3IZPZ z2XUz9f^*C5Sb0>gN`{?4U(BMKHS8&yTYTu`A!=KWiB|-n1*=Ek)*tKHVC5I(3LcSuFTs-0F2U!j(xST)5OUr!1AYTj-9%k z11VUW87XZcgg__;;#k4sNG?TsL2fx3R;2-2K7FHP1SEYVI`jHP371C=qoPv90%v7I z@gJ~KtE3WOHTTr�dO9auML)T2-ooO^yLzg*H5OCxG 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 PcRclByTyCp6OQ;l+So?0 literal 74152 zcma%ib97zpws+bzW|K6wZ5xe^HnyGY*tTuEO=GLEZQHhScly5XIpf@WzA?TvGREFn z|76Yi%;%?PMOr{fNoQth)W{?ze7TSfH%E4xtp}=1=>d?eXzfV#&OD0)%r^~<$565@ ztJN8$H<~K$Xd%tmL32 zgfPfqcCxvZyhG)nD22Y*1pSbM%Bg_In%$!4xDifki$4Xnp>HirJ@d z99zvBujZ2%q&L3_7;A+PVojiq38*E(r$qs`WTa30S@@k)fI4>4&cnIs&>j2v*+9Z~eP)O5TW3%AjB3-#*<0t<@D8(qDib8v@H?^rwGu>M_Fb}pIPK(VKtNb0;P&l1|CHGU;|x{4&eGyj ze-6Le1wjuPEY?q6j1HEUi!9Q$zaP#(Pukl#j}Eb=#~mI8MNdH5*`53;P9It-q=cTO zu%jj!eku>fs69Lxib)@JICO!YYqxVb8RANhcQ^{H0w1-rQ{_{b9)o?)rpiajT&zcZ z5O6)Jnj9Rz3KnpEEQ>ILdH5xLh-XkPJ+kA_l?txg_RM)0H+{V0u$Kxx_;w$5NQf;& z?9eV7;>gYkcJRJUcI+r*8$y}R0d}yWP3H7SCL7Yx&PH~Gy{(TL!C^KGv>gQvut{6c z8X-SzSjswT8{pYCNteUbZK$leP8vX+Hffh5+-&GhyI!0@5H<<8L;KFzGuz{wp=Gx) zxWo3&xwYEEoT2EpzO;nqID=a2p|5r>IYXRn^0Y)DoD=YN_T7KV+GKK%q&sJ6>!`nn zU)zFU{^b9)n>ZgO0J}dXHx_v(?^($kT7iI4O1PN(3z0IV3`sF@qdWzrv|%yzx9m`* z5K6@_fTABt5!}U`Guf$1VPTn^gw~e?R4h4#)8XzV9JTm-X5z0X z{H#@qh_!_}W?$f=I0faKc;_MV;TifZriU3NP5mR=aW*hL){uFj5 z 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 gYU Date: Fri, 6 Mar 2026 15:26:05 +0100 Subject: [PATCH 18/20] changing of names --- ...ncingIons.m => matRad_ParticleSequencer.m} | 240 +++++++++--------- ...act.m => matRad_PhotonSequencerAbstract.m} | 4 +- ...equencingBase.m => matRad_SequencerBase.m} | 131 +++++----- .../matRad_SequencingPhotonsEngelLeaf.m | 4 +- .../matRad_SequencingPhotonsSiochiLeaf.m | 4 +- .../matRad_SequencingPhotonsXiaLeaf.m | 4 +- 6 files changed, 194 insertions(+), 193 deletions(-) rename matRad/sequencing/{matRad_SequencingIons.m => matRad_ParticleSequencer.m} (60%) rename matRad/sequencing/{matRad_SequencingPhotonsAbstract.m => matRad_PhotonSequencerAbstract.m} (99%) rename matRad/sequencing/{matRad_SequencingBase.m => matRad_SequencerBase.m} (71%) diff --git a/matRad/sequencing/matRad_SequencingIons.m b/matRad/sequencing/matRad_ParticleSequencer.m similarity index 60% rename from matRad/sequencing/matRad_SequencingIons.m rename to matRad/sequencing/matRad_ParticleSequencer.m index 9a575fd35..a66963953 100644 --- a/matRad/sequencing/matRad_SequencingIons.m +++ b/matRad/sequencing/matRad_ParticleSequencer.m @@ -1,159 +1,158 @@ -classdef matRad_SequencingIons < matRad_SequencingBase - %UNTITLED2 Summary of this class goes here +classdef matRad_ParticleSequencer < matRad_SequencerBase + % UNTITLED2 Summary of this class goes here % Detailed explanation goes here properties (Constant) - name = 'Particle IMPT Scanning Sequencing'; - shortName = 'IMPT'; - possibleRadiationModes = {'protons','helium','carbon'}; - weightPencilBeam = 1e6 - end + name = 'Particle IMPT Scanning Sequencing' + shortName = 'IMPT' + possibleRadiationModes = {'protons', 'helium', 'carbon'} + weightPencilBeam = 1e6 + end properties - esTime = 3 * 10^6; % [\mu s] time required for synchrotron to recharge it' spill - spillRechargeTime = 2 * 10^6; % [\mu s] number of particles generated in each spill - spillSize = 4 * 10 ^ 10; - scanSpeed = 10; % [m/s] speed of synchrotron's lateral scanning in an IES - spillIntensity = 4 * 10 ^ 8; % number of particles per second + esTime = 3 * 10^6 % [\mu s] time required for synchrotron to recharge it' spill + spillRechargeTime = 2 * 10^6 % [\mu s] number of particles generated in each spill + spillSize = 4 * 10^10 + scanSpeed = 10 % [m/s] speed of synchrotron's lateral scanning in an IES + spillIntensity = 4 * 10^8 % number of particles per second end methods - - function sequence = sequence(this,w,stf) + + function sequence = sequence(this, w, stf) sequence = this.calcSpotOrder(stf); - sequence = this.calcSpotTime(sequence,w,stf); + sequence = this.calcSpotTime(sequence, w, stf); end - function sequence = calcSpotOrder(~,stf) + function sequence = calcSpotOrder(~, stf) sequence = struct; wOffset = 0; % first loop loops over all bixels to store their position and ray number in each IES - for i = 1:length(stf) - + for i = 1:length(stf) + usedEnergies = unique([stf(i).ray(:).energy]); usedEnergiesSorted = sort(usedEnergies, 'descend'); - + sequence(i).orderToSTF = zeros(stf(i).totalNumOfBixels, 1); sequence(i).orderToSS = zeros(stf(i).totalNumOfBixels, 1); sequence(i).time = zeros(stf(i).totalNumOfBixels, 1); sequence(i).e = zeros(stf(i).totalNumOfBixels, 1); - - + for e = 1:length(usedEnergies) % looping over IES's - + s = 1; - + for j = 1:stf(i).numOfRays % looping over all rays - + % find the rays which are active in current IES - if(any(stf(i).ray(j).energy == usedEnergiesSorted(e))) - + if any(stf(i).ray(j).energy == usedEnergiesSorted(e)) + x = stf(i).ray(j).rayPos_bev(1); y = stf(i).ray(j).rayPos_bev(3); - + sequence(i).IES(e).x(s) = x; % store x position sequence(i).IES(e).y(s) = y; % store y position sequence(i).IES(e).wIndex(s) = wOffset + ... - sum(stf(i).numOfBixelsPerRay(1:(j-1))) + ... + sum(stf(i).numOfBixelsPerRay(1:(j - 1))) + ... find(stf(i).ray(j).energy == usedEnergiesSorted(e)); % store index - + s = s + 1; - + end end end - + wOffset = wOffset + sum(stf(i).numOfBixelsPerRay); - + end end - function sequence = calcSpotTime(this,sequence,w,stf) - steerTime = [stf.bixelWidth] * (10 ^ 3)/ this.scanSpeed; % [\mu s] + function sequence = calcSpotTime(this, sequence, w, stf) + steerTime = [stf.bixelWidth] * (10^3) / this.scanSpeed; % [\mu s] % after storing all the required information, % same loop over all bixels will put each bixel in it's order - - spillUsage= 0; + + spillUsage = 0; offset = 0; - + for i = 1:length(stf) - + usedEnergies = unique([stf(i).ray(:).energy]); - + t = 0; orderCount = 1; - - for e = 1: length(usedEnergies) - + + for e = 1:length(usedEnergies) + % sort the y positions from high to low (backforth is up do down) y_sorted = sort(unique(sequence(i).IES(e).y), 'descend'); x_sorted = sort(sequence(i).IES(e).x, 'ascend'); - + for k = 1:length(y_sorted) - + y = y_sorted(k); % find indexes corresponding to current y position % in other words, number of bixels in the current row ind_y = find(sequence(i).IES(e).y == y); - + % since backforth fasion is zig zag like, flip the order every % second row - if ~rem(k,2) + if ~rem(k, 2) ind_y = fliplr(ind_y); end - + % loop over all the bixels in the row for is = 1:length(ind_y) - + s = ind_y(is); - + x = x_sorted(s); - + wIndex = sequence(i).IES(e).wIndex(s); - + % in case there were holes inside the plan "multi" % multiplies the steertime to take it into account: - if(k == 1 && is == 1) + if k == 1 && is == 1 x_prev = x; y_prev = y; end % x direction - multi = abs(x_prev - x)/stf(i).bixelWidth; + multi = abs(x_prev - x) / stf(i).bixelWidth; % y direction - multi = multi + abs(y_prev - y)/stf(i).bixelWidth; + multi = multi + abs(y_prev - y) / stf(i).bixelWidth; % x_prev = x; y_prev = y; - + % calculating the time: - + % required spot fluence - numOfParticles = w(wIndex)*this.weightPencilBeam; + numOfParticles = w(wIndex) * this.weightPencilBeam; % time spent to spill the required spot fluence spillTime = numOfParticles * 10^6 / this.spillIntensity; - + % spotTime:time spent to steer scan along IES per bixel t = t + multi * steerTime(i) + spillTime; - + % taking account of the time to recharge the spill in case % the required fluence was more than spill size - if(spillUsage+ numOfParticles > this.spillSize) + if spillUsage + numOfParticles > this.spillSize t = t + this.spillRechargeTime; - spillUsage= 0; + spillUsage = 0; end - + % used amount of fluence from current spill - spillUsage= spillUsage + numOfParticles; - + spillUsage = spillUsage + numOfParticles; + % storing the time and the order of bixels - + % make the both counter and index 'per beam' - help index wInd = wIndex - offset; - + % timeline according to the spot scanning order sequence(i).time(orderCount) = t; % IES of bixels according to the spot scanning order @@ -162,100 +161,99 @@ % bixels, use this order to transfer STF order to Spot % Scanning order sequence(i).orderToSS(orderCount) = wInd; - + % according to STF order, gives us order of irradiation of % each bixel, use this order to transfer Spot Scanning % order to STF order % orderToSTF(orderToSS) = orderToSS(orderToSTF) = 1:#bixels sequence(i).orderToSTF(wInd) = orderCount; - + orderCount = orderCount + 1; - + end end - + t = t + this.esTime; - + end - + % storing the fluence per beam - sequence(i).w = w(offset + 1: offset + stf(i).totalNumOfBixels); - + sequence(i).w = w(offset + 1:offset + stf(i).totalNumOfBixels); + offset = offset + stf(i).totalNumOfBixels; end end - - end + end methods (Static) - function [available,msg] = isAvailable(pln,machine) - % see superclass for information - - if nargin < 2 - machine = matRad_loadMachine(pln); - end - %checkBasic - available = isfield(machine,'meta') && isfield(machine,'data'); - - available = available && any(isfield(machine.meta,{'machine','radiationMode'})); - - if ~available - msg = 'Your machine file is invalid and does not contain the basic field (meta/data/radiationMode)!'; - else - msg = []; - end - - %check modality - checkModality = any(strcmp(matRad_SequencingIons.possibleRadiationModes, machine.meta.radiationMode)) && any(strcmp(matRad_SequencingIons.possibleRadiationModes, pln.radiationMode)); - - %Sanity check compatibility - if checkModality - checkModality = strcmp(machine.meta.radiationMode,pln.radiationMode); - end - - available = available && checkModality; - + + function [available, msg] = isAvailable(pln, machine) + % see superclass for information + + if nargin < 2 + machine = matRad_loadMachine(pln); + end + % checkBasic + available = isfield(machine, 'meta') && isfield(machine, 'data'); + + available = available && any(isfield(machine.meta, {'machine', 'radiationMode'})); + + if ~available + msg = 'Your machine file is invalid and does not contain the basic field (meta/data/radiationMode)!'; + else + msg = []; + end + + % check modality + checkModality = any(strcmp(matRad_ParticleSequencer.possibleRadiationModes, machine.meta.radiationMode)) && any(strcmp(matRad_ParticleSequencer.possibleRadiationModes, pln.radiationMode)); + + % Sanity check compatibility + if checkModality + checkModality = strcmp(machine.meta.radiationMode, pln.radiationMode); + end + + available = available && checkModality; + end function sequence = makePhaseMatrix(sequence, numOfPhases, motionPeriod) - phaseTime = motionPeriod * 10 ^ 6/numOfPhases; % time of each phase [/mu s] - + phaseTime = motionPeriod * 10^6 / numOfPhases; % time of each phase [/mu s] + for i = 1:length(sequence) - + realTime = phaseTime; - sequence(i).phaseMatrix = zeros(length(sequence(i).time),numOfPhases); - + sequence(i).phaseMatrix = zeros(length(sequence(i).time), numOfPhases); + iPhase = 1; iTime = 1; - - while (iTime <= length(sequence(i).time)) - if(sequence(i).time(iTime) < realTime) - while(iTime <= length(sequence(i).time) && sequence(i).time(iTime) < realTime) + + while iTime <= length(sequence(i).time) + if sequence(i).time(iTime) < realTime + while iTime <= length(sequence(i).time) && sequence(i).time(iTime) < realTime sequence(i).phaseMatrix(iTime, iPhase) = 1; iTime = iTime + 1; - end + end else - + iPhase = iPhase + 1; % back to 1 after going over all phases - if(iPhase > numOfPhases) + if iPhase > numOfPhases iPhase = 1; end realTime = realTime + phaseTime; end end - + % permuatation of phaseMatrix from SS order to STF order - sequence(i).phaseMatrix = sequence(i).phaseMatrix(sequence(i).orderToSTF,:); - [sequence(i).phaseNum,~] = find(sequence(i).phaseMatrix'); + sequence(i).phaseMatrix = sequence(i).phaseMatrix(sequence(i).orderToSTF, :); + sequence(i).phaseNum = find(sequence(i).phaseMatrix'); % inserting the fluence in phaseMatrix sequence(i).phaseMatrix = sequence(i).phaseMatrix .* sequence(i).w; end end - - end + end -end \ No newline at end of file +end diff --git a/matRad/sequencing/matRad_SequencingPhotonsAbstract.m b/matRad/sequencing/matRad_PhotonSequencerAbstract.m similarity index 99% rename from matRad/sequencing/matRad_SequencingPhotonsAbstract.m rename to matRad/sequencing/matRad_PhotonSequencerAbstract.m index d58c876a9..4cb67c955 100644 --- a/matRad/sequencing/matRad_SequencingPhotonsAbstract.m +++ b/matRad/sequencing/matRad_PhotonSequencerAbstract.m @@ -1,4 +1,4 @@ -classdef (Abstract) matRad_SequencingPhotonsAbstract < matRad_SequencingBase +classdef (Abstract) matRad_PhotonSequencerAbstract < matRad_SequencerBase % UNTITLED Summary of this class goes here % Detailed explanation goes her @@ -317,7 +317,7 @@ function plotSegments(this, sequencing) % The collimator limits are infered here from the apertureInfo. This could % be handled differently by explicitly storing collimator info in the base % data? - symmetricMLClimits = vertcat(apertureInfo.beam.MLCWindow); + symmetricMLClimits = vertcat(sequence.apertureInfo.beam.MLCWindow); symmetricMLClimits = max(abs(symmetricMLClimits)); fieldWidth = 2 * max(symmetricMLClimits); diff --git a/matRad/sequencing/matRad_SequencingBase.m b/matRad/sequencing/matRad_SequencerBase.m similarity index 71% rename from matRad/sequencing/matRad_SequencingBase.m rename to matRad/sequencing/matRad_SequencerBase.m index 54f352e90..07ee44d08 100644 --- a/matRad/sequencing/matRad_SequencingBase.m +++ b/matRad/sequencing/matRad_SequencerBase.m @@ -1,24 +1,25 @@ -classdef (Abstract) matRad_SequencingBase < handle - %UNTITLED2 Summary of this class goes here +classdef (Abstract) matRad_SequencerBase < handle + % UNTITLED2 Summary of this class goes here % Detailed explanation goes here properties (Constant) - isSequencer = true; % const boolean for inheritance quick check + isSequencer = true % const boolean for inheritance quick check end properties (Constant, Abstract) - name; %Descriptive Name - shortName; %Short name for referencing - possibleRadiationModes; %Possible radiation modes for the respective Sequencer + name % Descriptive Name + shortName % Short name for referencing + possibleRadiationModes % Possible radiation modes for the respective Sequencer end properties (Access = public) - radiationMode; %Radiation Mode - visMode = 0; % vis bool + radiationMode % Radiation Mode + visMode = 0 % vis bool end methods - function this = matRad_SequencingBase(pln) + + function this = matRad_SequencerBase(pln) % Constructs standalone sequencer with or without pln this.setDefaults(); @@ -36,17 +37,17 @@ function setDefaults(this) fields = fieldnames(defaultPropSeq); for i = 1:numel(fields) fName = fields{i}; - if matRad_ispropCompat(this,fName) + if matRad_ispropCompat(this, fName) try this.(fName) = defaultPropSeq.(fName); catch - matRad_cfg.dispWarning('Could not assign default property %s',fName); + matRad_cfg.dispWarning('Could not assign default property %s', fName); end end end end - function warnDeprecatedProperty(this,oldProp,msg,newProp) + function warnDeprecatedProperty(this, oldProp, msg, newProp) matRad_cfg = MatRad_Config.instance(); if nargin < 3 || isempty(msg) msg = ''; @@ -55,20 +56,20 @@ function warnDeprecatedProperty(this,oldProp,msg,newProp) if nargin < 4 dep2 = ''; else - dep2 = sprintf('Use Property ''%s'' instead!',newProp); + dep2 = sprintf('Use Property ''%s'' instead!', newProp); end - matRad_cfg.dispDeprecationWarning('Property ''%s'' of sequencer ''%s'' is deprecated! %s%s',oldProp,this.name,msg,dep2); + matRad_cfg.dispDeprecationWarning('Property ''%s'' of sequencer ''%s'' is deprecated! %s%s', oldProp, this.name, msg, dep2); end - - function assignPropertiesFromPln(this,pln,warnWhenPropertyChanged) + + function assignPropertiesFromPln(this, pln, warnWhenPropertyChanged) % Assign properties from pln.propSeq to the sequencer matRad_cfg = MatRad_Config.instance(); - - %Must haves in pln struct - %Set/validate radiation Mode - if ~isfield(pln,'radiationMode') && isempty(this.radiationMode) + + % Must haves in pln struct + % Set/validate radiation Mode + if ~isfield(pln, 'radiationMode') && isempty(this.radiationMode) matRad_cfg.dispError('No radiation mode specified in pln struct!'); else this.radiationMode = pln.radiationMode; @@ -78,14 +79,14 @@ function assignPropertiesFromPln(this,pln,warnWhenPropertyChanged) warnWhenPropertyChanged = false; end - %Overwrite default properties within the sequencer with the - %ones given in the propSeq struct - if isfield(pln,'propSeq') && isstruct(pln.propSeq) - plnStruct = pln.propSeq; %get remaining fields - if isfield(plnStruct,'sequencer') && ~isempty(plnStruct.sequencer) && ~any(strcmp(plnStruct.sequencer,this.shortName)) - matRad_cfg.dispWarning('Inconsistent sequencers given! pln asks for ''%s'', but you are using ''%s''!',plnStruct.sequencer,this.shortName); + % Overwrite default properties within the sequencer with the + % ones given in the propSeq struct + if isfield(pln, 'propSeq') && isstruct(pln.propSeq) + plnStruct = pln.propSeq; % get remaining fields + if isfield(plnStruct, 'sequencer') && ~isempty(plnStruct.sequencer) && ~any(strcmp(plnStruct.sequencer, this.shortName)) + matRad_cfg.dispWarning('Inconsistent sequencers given! pln asks for ''%s'', but you are using ''%s''!', plnStruct.sequencer, this.shortName); end - if isfield(plnStruct,'sequencer') + if isfield(plnStruct, 'sequencer') plnStruct = rmfield(plnStruct, 'sequencer'); % sequencer field is no longer needed and would throw an exception end else @@ -94,7 +95,7 @@ function assignPropertiesFromPln(this,pln,warnWhenPropertyChanged) fields = fieldnames(plnStruct); - %Set up warning message + % Set up warning message if warnWhenPropertyChanged warningMsg = 'Property in sequencer overwritten from pln.propSeq'; else @@ -104,16 +105,16 @@ function assignPropertiesFromPln(this,pln,warnWhenPropertyChanged) % iterate over all fieldnames and try to set the % corresponding properties inside the sequencer if matRad_cfg.isOctave - c2sWarningState = warning('off','Octave:classdef-to-struct'); + c2sWarningState = warning('off', 'Octave:classdef-to-struct'); end for i = 1:length(fields) try field = fields{i}; - if matRad_ispropCompat(this,field) - this.(field) = matRad_recursiveFieldAssignment(this.(field),plnStruct.(field),true,warningMsg); + if matRad_ispropCompat(this, field) + this.(field) = matRad_recursiveFieldAssignment(this.(field), plnStruct.(field), true, warningMsg); else - matRad_cfg.dispWarning('Not able to assign property ''%s'' from pln.propSeq to sequencer',field); + matRad_cfg.dispWarning('Not able to assign property ''%s'' from pln.propSeq to sequencer', field); end catch ME % catch exceptions when the sequencing has no @@ -125,20 +126,20 @@ function assignPropertiesFromPln(this,pln,warnWhenPropertyChanged) matRad_cfg = MatRad_Config.instance(); switch ME.identifier case 'MATLAB:noPublicFieldForClass' - matRad_cfg.dispWarning('Not able to assign property from pln.propSeq to sequencing: %s',ME.message); + matRad_cfg.dispWarning('Not able to assign property from pln.propSeq to sequencing: %s', ME.message); otherwise - matRad_cfg.dispWarning('Problem while setting up sequencing from struct:%s %s',field,ME.message); + matRad_cfg.dispWarning('Problem while setting up sequencing from struct:%s %s', field, ME.message); end end end end if matRad_cfg.isOctave - warning(c2sWarningState.state,'Octave:classdef-to-struct'); + warning(c2sWarningState.state, 'Octave:classdef-to-struct'); end end - function sequence = sequence(this,w,stf) + function sequence = sequence(this, w, stf) matRad_cfg = MatRad_Config.instance(); matRad_cfg.dispError('This is an Abstract Base class! Function needs to be called for instantiable subclasses!'); @@ -146,8 +147,9 @@ function assignPropertiesFromPln(this,pln,warnWhenPropertyChanged) end methods (Static) + function sequencer = getSequencerFromPln(pln, warnDefault) - %GETENGINE Summary of this function goes here + % GETENGINE Summary of this function goes here % Detailed explanation goes here if nargin < 2 @@ -159,18 +161,18 @@ function assignPropertiesFromPln(this,pln,warnWhenPropertyChanged) sequencer = []; initDefaultSequencer = false; - %get all available Sequencers for given pln struct, could be done conditional - classList = matRad_SequencingBase.getAvailableSequencers(pln); + % get all available Sequencers for given pln struct, could be done conditional + classList = matRad_SequencerBase.getAvailableSequencers(pln); % Check for a valid engine, and if the given engine isn't valid set boolean % to initiliaze default engine at the end of this function - if isfield(pln,'propSeq') && isa(pln.propSeq, mfilename('class')) + if isfield(pln, 'propSeq') && isa(pln.propSeq, mfilename('class')) sequencer = pln.propSeq; - elseif isfield(pln,'propSeq') && isstruct(pln.propSeq) && isfield(pln.propSeq,'sequencer') + elseif isfield(pln, 'propSeq') && isstruct(pln.propSeq) && isfield(pln.propSeq, 'sequencer') if ischar(pln.propSeq.sequencer) || isstring(pln.propSeq.sequencer) - matchSequencers = strcmpi({classList(:).shortName},pln.propSeq.sequencer); + matchSequencers = strcmpi({classList(:).shortName}, pln.propSeq.sequencer); if any(matchSequencers) - %instantiate engine + % instantiate engine sequencerHandle = classList(matchSequencers).handle; sequencer = sequencerHandle(pln); else @@ -188,7 +190,7 @@ function assignPropertiesFromPln(this,pln,warnWhenPropertyChanged) % the given radiation mode, when no valid engine was defined. % Default Engines are defined in matRad_Config. if initDefaultSequencer - matchSequencers = ismember({classList(:).shortName},matRad_cfg.defaults.propSeq.sequencer); + matchSequencers = ismember({classList(:).shortName}, matRad_cfg.defaults.propSeq.sequencer); if any(matchSequencers) sequencerHandle = classList(matchSequencers).handle; @@ -215,11 +217,11 @@ function assignPropertiesFromPln(this,pln,warnWhenPropertyChanged) end - function classList = getAvailableSequencers(pln,optionalPaths) - + function classList = getAvailableSequencers(pln, optionalPaths) + matRad_cfg = MatRad_Config.instance(); - %Parse inputs + % Parse inputs if nargin < 2 optionalPaths = {fileparts(mfilename("fullpath"))}; else @@ -227,7 +229,7 @@ function assignPropertiesFromPln(this,pln,warnWhenPropertyChanged) matRad_cfg.dispError('Invalid path array!'); end - optionalPaths = horzcat(fileparts(mfilename("fullpath")),optionalPaths); + optionalPaths = horzcat(fileparts(mfilename("fullpath")), optionalPaths); end if nargin < 1 @@ -238,24 +240,24 @@ function assignPropertiesFromPln(this,pln,warnWhenPropertyChanged) end end - %Get available, valid classes through call to matRad helper function - %for finding subclasses + % Get available, valid classes through call to matRad helper function + % for finding subclasses persistent allAvailableSequencers lastOptionalPaths - - %First we do a sanity check if persistently stored metaclasses are valid - if ~matRad_cfg.isOctave && ~isempty(allAvailableSequencers) && ~all(cellfun(@isvalid,allAvailableSequencers)) + + % First we do a sanity check if persistently stored metaclasses are valid + if ~matRad_cfg.isOctave && ~isempty(allAvailableSequencers) && ~all(cellfun(@isvalid, allAvailableSequencers)) matRad_cfg.dispWarning('Found invalid Sequencing Sequencers, updating cache.'); allAvailableSequencers = []; end if isempty(allAvailableSequencers) || (~isempty(lastOptionalPaths) && ~isequal(lastOptionalPaths, optionalPaths)) lastOptionalPaths = optionalPaths; - allAvailableSequencers = matRad_findSubclasses(mfilename('class'),'folders',optionalPaths,'includeAbstract',false); + allAvailableSequencers = matRad_findSubclasses(mfilename('class'), 'folders', optionalPaths, 'includeAbstract', false); end - availableSequencers = allAvailableSequencers; + availableSequencers = allAvailableSequencers; - %Now filter for pln + % Now filter for pln ix = []; if nargin >= 1 && ~isempty(pln) @@ -265,18 +267,18 @@ function assignPropertiesFromPln(this,pln,warnWhenPropertyChanged) for cIx = 1:length(availableSequencers) mc = availableSequencers{cIx}; availabilityFuncStr = [mc.Name '.isAvailable']; - %availabilityFunc = str2func(availabilityFuncStr); %str2func does not seem to work on static class functions in Octave 5.2.0 + % availabilityFunc = str2func(availabilityFuncStr); %str2func does not seem to work on static class functions in Octave 5.2.0 try - %available = availabilityFunc(pln,machine); + % available = availabilityFunc(pln,machine); available = eval([availabilityFuncStr '(pln,machine)']); catch available = false; mpList = mc.PropertyList; if matRad_cfg.isMatlab - loc = find(arrayfun(@(x) strcmp('possibleRadiationModes',x.Name),mpList)); + loc = find(arrayfun(@(x) strcmp('possibleRadiationModes', x.Name), mpList)); propValue = mpList(loc).DefaultValue; else - loc = find(cellfun(@(x) strcmp('possibleRadiationModes',x.Name),mpList)); + loc = find(cellfun(@(x) strcmp('possibleRadiationModes', x.Name), mpList)); propValue = mpList{loc}.DefaultValue; end @@ -284,7 +286,7 @@ function assignPropertiesFromPln(this,pln,warnWhenPropertyChanged) % get radiation mode from the in pln proposed basedata machine file % add current class to return lists if the % radiation mode is compatible - if(any(strcmp(propValue, machineMode))) + if any(strcmp(propValue, machineMode)) available = true; end @@ -298,15 +300,16 @@ function assignPropertiesFromPln(this,pln,warnWhenPropertyChanged) availableSequencers = availableSequencers (ix); end - classList = matRad_identifyClassesByConstantProperties(availableSequencers,'shortName','defaults',matRad_cfg.defaults.propSeq.sequencer,'additionalPropertyNames',{'name'}); + classList = matRad_identifyClassesByConstantProperties(availableSequencers, 'shortName', 'defaults', matRad_cfg.defaults.propSeq.sequencer, 'additionalPropertyNames', {'name'}); end - function [available,msg] = isAvailable(pln,machine) + function [available, msg] = isAvailable(pln, machine) matRad_cfg = MatRad_Config.instance(); matRad_cfg.dispError('This is an Abstract Base class! Function needs to be called for instantiable subclasses!'); end + end -end \ No newline at end of file +end diff --git a/matRad/sequencing/matRad_SequencingPhotonsEngelLeaf.m b/matRad/sequencing/matRad_SequencingPhotonsEngelLeaf.m index c1eac8d2c..cad85f904 100644 --- a/matRad/sequencing/matRad_SequencingPhotonsEngelLeaf.m +++ b/matRad/sequencing/matRad_SequencingPhotonsEngelLeaf.m @@ -1,4 +1,4 @@ -classdef matRad_SequencingPhotonsEngelLeaf < matRad_SequencingPhotonsAbstract +classdef matRad_SequencingPhotonsEngelLeaf < matRad_PhotonSequencerAbstract % multileaf collimator leaf sequencing algorithm % for intensity modulated beams with multiple static segments accroding @@ -264,7 +264,7 @@ end % Check superclass availability - [available, msg] = matRad_SequencingPhotonsAbstract.isAvailable(pln, machine); + [available, msg] = matRad_PhotonSequencerAbstract.isAvailable(pln, machine); if ~available return diff --git a/matRad/sequencing/matRad_SequencingPhotonsSiochiLeaf.m b/matRad/sequencing/matRad_SequencingPhotonsSiochiLeaf.m index 0d5b9e2b0..6fa373e39 100644 --- a/matRad/sequencing/matRad_SequencingPhotonsSiochiLeaf.m +++ b/matRad/sequencing/matRad_SequencingPhotonsSiochiLeaf.m @@ -1,4 +1,4 @@ -classdef matRad_SequencingPhotonsSiochiLeaf < matRad_SequencingPhotonsAbstract +classdef matRad_SequencingPhotonsSiochiLeaf < matRad_PhotonSequencerAbstract % UNTITLED Summary of this class goes here % Detailed explanation goes here @@ -214,7 +214,7 @@ end % Check superclass availability - [available, msg] = matRad_SequencingPhotonsAbstract.isAvailable(pln, machine); + [available, msg] = matRad_PhotonSequencerAbstract.isAvailable(pln, machine); if ~available return diff --git a/matRad/sequencing/matRad_SequencingPhotonsXiaLeaf.m b/matRad/sequencing/matRad_SequencingPhotonsXiaLeaf.m index 6092e7a80..124235d98 100644 --- a/matRad/sequencing/matRad_SequencingPhotonsXiaLeaf.m +++ b/matRad/sequencing/matRad_SequencingPhotonsXiaLeaf.m @@ -1,4 +1,4 @@ -classdef matRad_SequencingPhotonsXiaLeaf < matRad_SequencingPhotonsAbstract +classdef matRad_SequencingPhotonsXiaLeaf < matRad_PhotonSequencerAbstract % multileaf collimator leaf sequence algorithm % for intensity modulated beams with multiple static segments according to @@ -149,7 +149,7 @@ end % Check superclass availability - [available, msg] = matRad_SequencingPhotonsAbstract.isAvailable(pln, machine); + [available, msg] = matRad_PhotonSequencerAbstract.isAvailable(pln, machine); if ~available return From d51140824a00dd340544d5f3c8ef74c60f096785 Mon Sep 17 00:00:00 2001 From: JenHardt <84377305+JenHardt@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:28:38 +0100 Subject: [PATCH 19/20] updates of old functions --- examples/matRad_example16_photonMC_MLC.m | 73 +- matRad.m | 61 +- matRad/4D/matRad_makeBixelTimeSeq.m | 171 +-- matRad/4D/matRad_makePhaseMatrix.m | 41 +- matRad/gui/widgets/matRad_WorkflowWidget.m | 991 +++++++++--------- matRad/matRad_sequencing.m | 34 +- matRad/matRad_sequencingOld.m | 71 -- .../sequencing/matRad_aperture2collimation.m | 118 +-- .../sequencing/matRad_engelLeafSequencing.m | 387 +------ .../sequencing/matRad_siochiLeafSequencing.m | 346 +----- matRad/sequencing/matRad_xiaLeafSequencing.m | 278 +---- 11 files changed, 657 insertions(+), 1914 deletions(-) delete mode 100644 matRad/matRad_sequencingOld.m diff --git a/examples/matRad_example16_photonMC_MLC.m b/examples/matRad_example16_photonMC_MLC.m index 858586753..1f51f365a 100644 --- a/examples/matRad_example16_photonMC_MLC.m +++ b/examples/matRad_example16_photonMC_MLC.m @@ -2,52 +2,52 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% 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/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 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/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. % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% In this example we will show +%% In this example we will show % (i) how to load patient data into matRad -% (ii) how to setup a photon dose calculation based on the VMC++ Monte Carlo algorithm -% (iii) how to inversely optimize the beamlet intensities directly from command window in MATLAB. +% (ii) how to setup a photon dose calculation based on the VMC++ Monte Carlo algorithm +% (iii) how to inversely optimize the beamlet intensities directly from command window in MATLAB. % (iv) how to visualize the result %% set matRad runtime configuration -matRad_rc %If this throws an error, run it from the parent directory first to set the paths +matRad_rc; % If this throws an error, run it from the parent directory first to set the paths %% Patient Data Import % Let's begin with a clear Matlab environment and import the boxphantom -% into your workspace. +% into your workspace. load('BOXPHANTOM.mat'); %% Treatment Plan -% The next step is to define your treatment plan labeled as 'pln'. This +% 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. -pln.radiationMode = 'photons'; +pln.radiationMode = 'photons'; pln.machine = 'Generic'; pln.numOfFractions = 30; pln.propStf.gantryAngles = [0:72:359]; pln.propStf.couchAngles = [0 0 0 0 0]; -%pln.propStf.gantryAngles = [0]; -%pln.propStf.couchAngles = [0]; +% pln.propStf.gantryAngles = [0]; +% pln.propStf.couchAngles = [0]; pln.propStf.bixelWidth = 10; pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); -pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0); +pln.propStf.isoCenter = ones(pln.propStf.numOfBeams, 1) * matRad_getIsoCenter(cst, ct, 0); -quantityOpt = 'physicalDose'; -modelName = 'none'; +quantityOpt = 'physicalDose'; +modelName = 'none'; % retrieve bio model parameters -pln.bioModel = matRad_bioModel(pln.radiationMode,quantityOpt, modelName); +pln.bioModel = matRad_bioModel(pln.radiationMode, quantityOpt, modelName); % retrieve scenarios for dose calculation and optimziation pln.multScen = matRad_NominalScenario(ct); @@ -57,40 +57,43 @@ pln.propDoseCalc.doseGrid.resolution.z = 3; % [mm] %% Generate Beam Geometry STF -stf = matRad_generateStf(ct,cst,pln); +stf = matRad_generateStf(ct, cst, pln); %% Dose Calculation -dij = matRad_calcDoseInfluence(ct,cst,stf,pln); +dij = matRad_calcDoseInfluence(ct, cst, stf, pln); %% Inverse Optimization for IMRT pln.propOpt.quantityOpt = quantityOpt; -resultGUI = matRad_fluenceOptimization(dij,cst,pln); +resultGUI = matRad_fluenceOptimization(dij, cst, pln); %% 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 +% 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. -resultGUI = matRad_sequencing(resultGUI,stf,pln, dij); +resultGUI = matRad_sequencing(resultGUI, stf, pln, dij); +[pln, stf] = matRad_aperture2collimation(pln, stf, resultGUI.sequencing, resultGUI.sequencing.apertureInfo); %% Aperture visualization % Use a matrad function to visualize the resulting aperture shapes -matRad_visApertureInfo(resultGUI.sequencing.apertureInfo) +matRad_visApertureInfo(resultGUI.sequencing.apertureInfo); %% Plot the Resulting Dose Slice % Just let's plot the transversal iso-center dose slice -slice = matRad_world2cubeIndex(pln.propStf.isoCenter(1,:),ct); +slice = matRad_world2cubeIndex(pln.propStf.isoCenter(1, :), ct); slice = slice(3); -figure, -imagesc(resultGUI.physicalDose(:,:,slice)),colorbar, colormap(jet) +figure; +imagesc(resultGUI.physicalDose(:, :, slice)); +colorbar; +colormap(jet); %% Dose Calculation -%resultGUI_MC = matRad_calcDoseInfluence(ct,cst,stf,pln); +% resultGUI_MC = matRad_calcDoseInfluence(ct,cst,stf,pln); pln.propDoseCalc.engine = 'TOPAS'; pln.propDoseCalc.beamProfile = 'phasespace'; pln.propDoseCalc.externalCalculation = 'write'; -resultGUI_MC = matRad_calcDoseForward(ct,cst,stf,pln,resultGUI.w); +resultGUI_MC = matRad_calcDoseForward(ct, cst, stf, pln, resultGUI.w); %% readout -waitforbuttonpress; %We will wait since we do need to do the external calculation first +waitforbuttonpress; % We will wait since we do need to do the external calculation first pln.propDoseCalc.externalCalculation = resultGUI_MC.meta.TOPASworkingDir; -resultGUI_MC = matRad_calcDoseForward(ct,cst,stf,pln,resultGUI.w); +resultGUI_MC = matRad_calcDoseForward(ct, cst, stf, pln, resultGUI.w); diff --git a/matRad.m b/matRad.m index 737dab26f..2eaa01f77 100644 --- a/matRad.m +++ b/matRad.m @@ -2,44 +2,44 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% 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/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 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/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. % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% set matRad runtime configuration -matRad_rc +matRad_rc; %% load patient data, i.e. ct, voi, cst -load TG119.mat -%load HEAD_AND_NECK -%load PROSTATE.mat -%load LIVER.mat -%load BOXPHANTOM.mat +load TG119.mat; +% load HEAD_AND_NECK +% load PROSTATE.mat +% load LIVER.mat +% load BOXPHANTOM.mat % meta information for treatment plan pln.numOfFractions = 30; pln.radiationMode = 'photons'; % either photons / protons / helium / carbon / brachy / VHEE pln.machine = 'Generic'; % generic for RT / LDR or HDR for BT / Generic or Focused for VHEE -pln.bioModel = 'none'; % none: for all % constRBE: constant RBE for photons and protons - % MCN: McNamara-variable RBE model for protons % WED: Wedenberg-variable RBE model for protons - % LEM: Local Effect Model for carbon ions % HEL: data-driven RBE parametrization for helium +pln.bioModel = 'none'; % none: for all % constRBE: constant RBE for photons and protons +% MCN: McNamara-variable RBE model for protons % WED: Wedenberg-variable RBE model for protons +% LEM: Local Effect Model for carbon ions % HEL: data-driven RBE parametrization for helium -pln.multScen = 'nomScen'; % scenario creation type 'nomScen' 'wcScen' 'impScen' 'rndScen' +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 = [0:72:359]; % [°] ; -pln.propStf.couchAngles = [0 0 0 0 0]; % [°] ; +pln.propStf.couchAngles = [0 0 0 0 0]; % [°] ; pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); -pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0); +pln.propStf.isoCenter = ones(pln.propStf.numOfBeams, 1) * matRad_getIsoCenter(cst, ct, 0); % dose calculation settings pln.propDoseCalc.doseGrid.resolution.x = 5; % [mm] @@ -52,32 +52,29 @@ pln.propOpt.runDAO = false; % 1/true: run DAO, 0/false: don't / will be ignored for particles pln.propSeq.runSequencing = true; % true: run sequencing, false: don't / will be ignored for particles and also triggered by runDAO below - %% initial visualization and change objective function settings if desired -matRadGUI +matRadGUI; -%% generate steering file -stf = matRad_generateStf(ct,cst,pln); +%% generate steering file +stf = matRad_generateStf(ct, cst, pln); %% dose calculation dij = matRad_calcDoseInfluence(ct, cst, stf, pln); %% inverse planning for imrt -resultGUI = matRad_fluenceOptimization(dij,cst,pln); +resultGUI = matRad_fluenceOptimization(dij, cst, pln); %% sequencing -resultGUI = matRad_sequencing(resultGUI,stf,dij,pln); - +resultGUI = matRad_sequencing(resultGUI, stf, pln, dij); %% DAO -if strcmp(pln.radiationMode,'photons') && pln.propOpt.runDAO - resultGUI = matRad_directApertureOptimization(dij,cst,resultGUI.apertureInfo,resultGUI,pln); - matRad_visApertureInfo(resultGUI.apertureInfo); +if strcmp(pln.radiationMode, 'photons') && pln.propOpt.runDAO + resultGUI = matRad_directApertureOptimization(dij, cst, resultGUI.apertureInfo, resultGUI, pln); + matRad_visApertureInfo(resultGUI.apertureInfo); end %% start gui for visualization of result -matRadGUI +matRadGUI; %% indicator calculation and show DVH and QI -resultGUI = matRad_planAnalysis(resultGUI,ct,cst,stf,pln); - +resultGUI = matRad_planAnalysis(resultGUI, ct, cst, stf, pln); diff --git a/matRad/4D/matRad_makeBixelTimeSeq.m b/matRad/4D/matRad_makeBixelTimeSeq.m index 1266a6997..ada6542b2 100644 --- a/matRad/4D/matRad_makeBixelTimeSeq.m +++ b/matRad/4D/matRad_makeBixelTimeSeq.m @@ -33,174 +33,11 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% defining the constant parameters -% -% time required for synchrotron to change energy - -es_time = 3 * 10^6; % [\mu s] -% time required for synchrotron to recharge it' spill -spill_recharge_time = 2 * 10^6; % [\mu s] -% number of particles generated in each spill -spill_size = 4 * 10^10; -% speed of synchrotron's lateral scanning in an IES -scan_speed = 10; % m/s -% number of particles per second -spill_intensity = 4 * 10^8; - -steerTime = [stf.bixelWidth] * (10^3) / scan_speed; % [\mu s] - -timeSequence = struct; - -% first loop loops over all bixels to store their position and ray number -% in each IES -wOffset = 0; -for i = 1:length(stf) % looping over all beams - - usedEnergies = unique([stf(i).ray(:).energy]); - usedEnergiesSorted = sort(usedEnergies, 'descend'); - - timeSequence(i).orderToSTF = zeros(stf(i).totalNumOfBixels, 1); - timeSequence(i).orderToSS = zeros(stf(i).totalNumOfBixels, 1); - timeSequence(i).time = zeros(stf(i).totalNumOfBixels, 1); - timeSequence(i).e = zeros(stf(i).totalNumOfBixels, 1); - - for e = 1:length(usedEnergies) % looping over IES's - - s = 1; - - for j = 1:stf(i).numOfRays % looping over all rays - - % find the rays which are active in current IES - if any(stf(i).ray(j).energy == usedEnergiesSorted(e)) - - x = stf(i).ray(j).rayPos_bev(1); - y = stf(i).ray(j).rayPos_bev(3); - % - timeSequence(i).IES(e).x(s) = x; % store x position - timeSequence(i).IES(e).y(s) = y; % store y position - timeSequence(i).IES(e).w_index(s) = wOffset + ... - sum(stf(i).numOfBixelsPerRay(1:(j - 1))) + ... - find(stf(i).ray(j).energy == usedEnergiesSorted(e)); % store index - - s = s + 1; - - end - end - end - - wOffset = wOffset + sum(stf(i).numOfBixelsPerRay); - -end - -% after storing all the required information, -% same loop over all bixels will put each bixel in it's order - -spill_usage = 0; -offset = 0; - -for i = 1:length(stf) - - usedEnergies = unique([stf(i).ray(:).energy]); - - t = 0; - order_count = 1; - - for e = 1:length(usedEnergies) - - % sort the y positions from high to low (backforth is up do down) - y_sorted = sort(unique(timeSequence(i).IES(e).y), 'descend'); - x_sorted = sort(timeSequence(i).IES(e).x, 'ascend'); +matRad_cfg = MatRad_Config.instance(); +matRad_cfg.dispWarning('This function is Outdated use new SequencingClass'); - for k = 1:length(y_sorted) +sequencer = matRad_ParticleSequencer(); - y = y_sorted(k); - % find indexes corresponding to current y position - % in other words, number of bixels in the current row - ind_y = find(timeSequence(i).IES(e).y == y); - - % since backforth fasion is zig zag like, flip the order every - % second row - if ~rem(k, 2) - ind_y = fliplr(ind_y); - end - - % loop over all the bixels in the row - for is = 1:length(ind_y) - - s = ind_y(is); - - x = x_sorted(s); - - w_index = timeSequence(i).IES(e).w_index(s); - - % in case there were holes inside the plan "multi" - % multiplies the steertime to take it into account: - if k == 1 && is == 1 - x_prev = x; - y_prev = y; - end - % x direction - multi = abs(x_prev - x) / stf(i).bixelWidth; - % y direction - multi = multi + abs(y_prev - y) / stf(i).bixelWidth; - % - x_prev = x; - y_prev = y; - - % calculating the time: - - % required spot fluence - numOfParticles = resultGUI.w(w_index) * 10^6; - % time spent to spill the required spot fluence - spillTime = numOfParticles * 10^6 / spill_intensity; - - % spotTime:time spent to steer scan along IES per bixel - t = t + multi * steerTime(i) + spillTime; - - % taking account of the time to recharge the spill in case - % the required fluence was more than spill size - if spill_usage + numOfParticles > spill_size - t = t + spill_recharge_time; - spill_usage = 0; - end - - % used amount of fluence from current spill - spill_usage = spill_usage + numOfParticles; - - % storing the time and the order of bixels - - % make the both counter and index 'per beam' - help index - w_ind = w_index - offset; - - % timeline according to the spot scanning order - timeSequence(i).time(order_count) = t; - % IES of bixels according to the spot scanning order - timeSequence(i).e(order_count) = e; - % according to spot scanning order, sorts w index of all - % bixels, use this order to transfer STF order to Spot - % Scanning order - timeSequence(i).orderToSS(order_count) = w_ind; - - % according to STF order, gives us order of irradiation of - % each bixel, use this order to transfer Spot Scanning - % order to STF order - % orderToSTF(orderToSS) = orderToSS(orderToSTF) = 1:#bixels - timeSequence(i).orderToSTF(w_ind) = order_count; - - order_count = order_count + 1; - - end - end - - t = t + es_time; - - end - - % storing the fluence per beam - timeSequence(i).w = resultGUI.w(offset + 1:offset + stf(i).totalNumOfBixels); - - offset = offset + stf(i).totalNumOfBixels; - -end +timeSequence = sequencer.sequence(stf, resultGUI.w); end diff --git a/matRad/4D/matRad_makePhaseMatrix.m b/matRad/4D/matRad_makePhaseMatrix.m index 07cc52d25..109f71015 100644 --- a/matRad/4D/matRad_makePhaseMatrix.m +++ b/matRad/4D/matRad_makePhaseMatrix.m @@ -34,43 +34,10 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% time of each phase [/mu s] -phaseTime = motionPeriod * 10^6 / numOfPhases; +matRad_cfg = MatRad_Config.instance(); +matRad_cfg.dispWarning('This function is Outdated use new SequencingClass'); -for i = 1:length(timeSequence) - - realTime = phaseTime; - timeSequence(i).phaseMatrix = zeros(length(timeSequence(i).time), numOfPhases); - - iPhase = 1; - iTime = 1; - - while iTime <= length(timeSequence(i).time) - if timeSequence(i).time(iTime) < realTime - - while iTime <= length(timeSequence(i).time) && timeSequence(i).time(iTime) < realTime - timeSequence(i).phaseMatrix(iTime, iPhase) = 1; - iTime = iTime + 1; - end - - else - - iPhase = iPhase + 1; - - % back to 1 after going over all phases - if iPhase > numOfPhases - iPhase = 1; - end - - realTime = realTime + phaseTime; - - end - end - - % permuatation of phaseMatrix from SS order to STF order - timeSequence(i).phaseMatrix = timeSequence(i).phaseMatrix(timeSequence(i).orderToSTF, :); - [timeSequence(i).phaseNum, ~] = find(timeSequence(i).phaseMatrix'); - % inserting the fluence in phaseMatrix - timeSequence(i).phaseMatrix = timeSequence(i).phaseMatrix .* timeSequence(i).w; +sequencer = matRad_ParticleSequencer(); +timeSequence = sequencer.makePhaseMatrix(timeSequence, numOfPhases, motionPeriod); end diff --git a/matRad/gui/widgets/matRad_WorkflowWidget.m b/matRad/gui/widgets/matRad_WorkflowWidget.m index 8fd42e079..13094c3bd 100644 --- a/matRad/gui/widgets/matRad_WorkflowWidget.m +++ b/matRad/gui/widgets/matRad_WorkflowWidget.m @@ -1,260 +1,259 @@ classdef matRad_WorkflowWidget < matRad_Widget % matRad_WorkflowWidget class to generate GUI widget to run through the % treatment planning workflow - % + % % % References % - % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % - % Copyright 2020 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 2020 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 - savedResultTag = {}; - end - + savedResultTag = {} + end + methods + function this = matRad_WorkflowWidget(handleParent) if nargin < 1 matRad_cfg = MatRad_Config.instance(); - handleParent = figure(... - 'Units','characters',... - 'Position',[130 45 150 15],... - 'Visible','on',... - 'Color',matRad_cfg.gui.backgroundColor,... - 'IntegerHandle','off',... - 'Colormap',[0 0 0.5625;0 0 0.625;0 0 0.6875;0 0 0.75;0 0 0.8125;0 0 0.875;0 0 0.9375;0 0 1;0 0.0625 1;0 0.125 1;0 0.1875 1;0 0.25 1;0 0.3125 1;0 0.375 1;0 0.4375 1;0 0.5 1;0 0.5625 1;0 0.625 1;0 0.6875 1;0 0.75 1;0 0.8125 1;0 0.875 1;0 0.9375 1;0 1 1;0.0625 1 1;0.125 1 0.9375;0.1875 1 0.875;0.25 1 0.8125;0.3125 1 0.75;0.375 1 0.6875;0.4375 1 0.625;0.5 1 0.5625;0.5625 1 0.5;0.625 1 0.4375;0.6875 1 0.375;0.75 1 0.3125;0.8125 1 0.25;0.875 1 0.1875;0.9375 1 0.125;1 1 0.0625;1 1 0;1 0.9375 0;1 0.875 0;1 0.8125 0;1 0.75 0;1 0.6875 0;1 0.625 0;1 0.5625 0;1 0.5 0;1 0.4375 0;1 0.375 0;1 0.3125 0;1 0.25 0;1 0.1875 0;1 0.125 0;1 0.0625 0;1 0 0;0.9375 0 0;0.875 0 0;0.8125 0 0;0.75 0 0;0.6875 0 0;0.625 0 0;0.5625 0 0],... - 'MenuBar','none',... - 'Name','MatRad Workflow',... - 'NumberTitle','off',... - 'HandleVisibility','callback',... - 'Tag','figure1',... - 'PaperSize',[20.99999864 29.69999902]); - - set(handleParent,'Units','normalized') - pos = get(handleParent,'Position'); - pos(1:2) = [0.5 0.5] - pos(3:4)./2; - set(handleParent,'Position',pos); - + handleParent = figure( ... + 'Units', 'characters', ... + 'Position', [130 45 150 15], ... + 'Visible', 'on', ... + 'Color', matRad_cfg.gui.backgroundColor, ... + 'IntegerHandle', 'off', ... + 'Colormap', [0 0 0.5625; 0 0 0.625; 0 0 0.6875; 0 0 0.75; 0 0 0.8125; 0 0 0.875; 0 0 0.9375; 0 0 1; 0 0.0625 1; 0 0.125 1; 0 0.1875 1; 0 0.25 1; 0 0.3125 1; 0 0.375 1; 0 0.4375 1; 0 0.5 1; 0 0.5625 1; 0 0.625 1; 0 0.6875 1; 0 0.75 1; 0 0.8125 1; 0 0.875 1; 0 0.9375 1; 0 1 1; 0.0625 1 1; 0.125 1 0.9375; 0.1875 1 0.875; 0.25 1 0.8125; 0.3125 1 0.75; 0.375 1 0.6875; 0.4375 1 0.625; 0.5 1 0.5625; 0.5625 1 0.5; 0.625 1 0.4375; 0.6875 1 0.375; 0.75 1 0.3125; 0.8125 1 0.25; 0.875 1 0.1875; 0.9375 1 0.125; 1 1 0.0625; 1 1 0; 1 0.9375 0; 1 0.875 0; 1 0.8125 0; 1 0.75 0; 1 0.6875 0; 1 0.625 0; 1 0.5625 0; 1 0.5 0; 1 0.4375 0; 1 0.375 0; 1 0.3125 0; 1 0.25 0; 1 0.1875 0; 1 0.125 0; 1 0.0625 0; 1 0 0; 0.9375 0 0; 0.875 0 0; 0.8125 0 0; 0.75 0 0; 0.6875 0 0; 0.625 0 0; 0.5625 0 0], ... + 'MenuBar', 'none', ... + 'Name', 'MatRad Workflow', ... + 'NumberTitle', 'off', ... + 'HandleVisibility', 'callback', ... + 'Tag', 'figure1', ... + 'PaperSize', [20.99999864 29.69999902]); + + set(handleParent, 'Units', 'normalized'); + pos = get(handleParent, 'Position'); + pos(1:2) = [0.5 0.5] - pos(3:4) ./ 2; + set(handleParent, 'Position', pos); + end this = this@matRad_Widget(handleParent); end - + function this = initialize(this) this.update(); end - - + % moved so it can be called from the toolbar button % H74 Callback function btnLoadMat_Callback(this, hObject, event) handles = this.handles; matRad_cfg = MatRad_Config.instance(); - [FileName, FilePath] = uigetfile(fullfile(matRad_cfg.matRadSrcRoot,'phantoms','*.mat')); + [FileName, FilePath] = uigetfile(fullfile(matRad_cfg.matRadSrcRoot, 'phantoms', '*.mat')); if FileName == 0 % user pressed cancel --> do nothing. - return; + return end try % delete existing workspace - parse variables from base workspace - AllVarNames = evalin('base','who'); - RefVarNames = {'ct','cst','pln','stf','dij','resultGUI'}; - + AllVarNames = evalin('base', 'who'); + RefVarNames = {'ct', 'cst', 'pln', 'stf', 'dij', 'resultGUI'}; + for i = 1:length(RefVarNames) - if sum(ismember(AllVarNames,RefVarNames{i}))>0 - evalin('base',['clear ', RefVarNames{i}]); + if sum(ismember(AllVarNames, RefVarNames{i})) > 0 + evalin('base', ['clear ', RefVarNames{i}]); end end - + % read new data load([FilePath FileName]); - + catch ME - this.handles=handles; + this.handles = handles; getFromWorkspace(this); - showError(this,'LoadMatFileFnc: Could not load *.mat file',ME); + showError(this, 'LoadMatFileFnc: Could not load *.mat file', ME); return end - + try - %cst = generateCstTable(this,cst); - %handles.TableChanged = false; - %set(handles.popupTypeOfPlot,'Value',1); - %cst = matRad_computeVoiContoursWrapper(cst,ct); - - assignin('base','ct',ct); - assignin('base','cst',cst); - + % cst = generateCstTable(this,cst); + % handles.TableChanged = false; + % set(handles.popupTypeOfPlot,'Value',1); + % cst = matRad_computeVoiContoursWrapper(cst,ct); + + assignin('base', 'ct', ct); + assignin('base', 'cst', cst); + catch ME - showError(this,'LoadMatFileFnc: Could not load *.mat file',ME); + showError(this, 'LoadMatFileFnc: Could not load *.mat file', ME); end - + % check if a optimized plan was loaded - if exist('stf','var') - assignin('base','stf',stf); + if exist('stf', 'var') + assignin('base', 'stf', stf); end - if exist('pln','var') - assignin('base','pln',pln); + if exist('pln', 'var') + assignin('base', 'pln', pln); end - if exist('dij','var') - assignin('base','dij',dij); + if exist('dij', 'var') + assignin('base', 'dij', dij); end - - if exist('resultGUI','var') - assignin('base','resultGUI',resultGUI); + + if exist('resultGUI', 'var') + assignin('base', 'resultGUI', resultGUI); end - - this.handles=handles; - %updateInWorkspace(this); + + this.handles = handles; + % updateInWorkspace(this); this.changedWorkspace(); - %getFromWorkspace(this); %update the buttons + % getFromWorkspace(this); %update the buttons end + end - + methods (Access = protected) + function this = createLayout(this) - + parent = this.widgetHandle; - + matRad_cfg = MatRad_Config.instance(); - - - h72 = this.addControlToGrid([2 4],... - 'Style','text',... - 'String','Status:',... - 'BackgroundColor',matRad_cfg.gui.backgroundColor,... - 'ForegroundColor',matRad_cfg.gui.textColor,... - 'Tag','txtStatus',... - 'FontSize',round(matRad_cfg.gui.fontSize*1.2)); - - - h73 = this.addControlToGrid([3 4],... - 'String','no data loaded',... - 'Style','text',... - 'BackgroundColor',matRad_cfg.gui.backgroundColor,... - 'ForegroundColor',matRad_cfg.gui.textColor,... - 'Tag','txtInfo',... - 'FontSize',round(matRad_cfg.gui.fontSize*1.2)); - - hMatLoad = this.addControlToGrid([2 1],... - 'String','Load *.mat data',... - 'BackgroundColor',matRad_cfg.gui.elementColor,... - 'ForegroundColor',matRad_cfg.gui.textColor,... - 'Callback',@(hObject,eventdata) btnLoadMat_Callback(this,hObject,eventdata),... - 'Tag','btnLoadMat'); - - hDijCalc = this.addControlToGrid([3 1],... - 'String','Calc. Dose Influence',... - 'BackgroundColor',matRad_cfg.gui.elementColor,... - 'ForegroundColor',matRad_cfg.gui.textColor,... - 'Callback',@(hObject,eventdata) btnCalcDose_Callback(this,hObject,eventdata),... - 'Tag','btnCalcDose'); - - hOpt = this.addControlToGrid([4 1],... - 'Parent',parent,... - 'String','Optimize',... - 'BackgroundColor',matRad_cfg.gui.elementColor,... - 'ForegroundColor',matRad_cfg.gui.textColor,... - 'Callback',@(hObject,eventdata) btnOptimize_Callback(this,hObject,eventdata),... - 'Tag','btnOptimize'); - - hLoadDicom = this.addControlToGrid([2 2],... - 'String','Load DICOM',... - 'BackgroundColor',matRad_cfg.gui.elementColor,... - 'ForegroundColor',matRad_cfg.gui.textColor,... - 'Callback',@(hObject,eventdata) btnLoadDicom_Callback(this,hObject,eventdata),... - 'Tag','btnLoadDicom'); - - - hRefresh = this.addControlToGrid([1 1],... - 'String','Refresh',... - 'BackgroundColor',matRad_cfg.gui.elementColor,... - 'ForegroundColor',matRad_cfg.gui.textColor,... - 'Callback',@(hObject,eventdata) btnRefresh_Callback(this,hObject,eventdata),... - 'Tag','btnRefresh'); - - hRecalc = this.addControlToGrid([4 2],... - 'String','Recalculate Dose',... - 'BackgroundColor',matRad_cfg.gui.elementColor,... - 'ForegroundColor',matRad_cfg.gui.textColor,... - 'Callback',@(hObject,eventdata) pushbutton_recalc_Callback(this,hObject,eventdata),... - 'Tag','pushbutton_recalc'); - - hKeep = this.addControlToGrid([5 1],... - 'String','Save/Keep Result',... - 'BackgroundColor',matRad_cfg.gui.elementColor,... - 'ForegroundColor',matRad_cfg.gui.textColor,... - 'Callback',@(hObject,eventdata) btnSaveToGUI_Callback(this,hObject,eventdata),... - 'Tag','btnSaveToGUI'); - - hExportBin = this.addControlToGrid([5 2],... - 'String','Export Binary',... - 'BackgroundColor',matRad_cfg.gui.elementColor,... - 'ForegroundColor',matRad_cfg.gui.textColor,... - 'Callback',@(hObject,eventdata) btn_export_Callback(this,hObject,eventdata),... - 'Children',[],... - 'Tag','btn_export'); - - hImportDose = this.addControlToGrid([4 3],... - 'String','Import Dose',... - 'BackgroundColor',matRad_cfg.gui.elementColor,... - 'ForegroundColor',matRad_cfg.gui.textColor,... - 'Callback',@(hObject,eventdata) importDoseButton_Callback(this, hObject,eventdata),... - 'Tag','importDoseButton'); - - hImportBin = this.addControlToGrid([2 3],... - 'String','Import from Binary',... - 'BackgroundColor',matRad_cfg.gui.elementColor,... - 'ForegroundColor',matRad_cfg.gui.textColor,... - 'Callback',@(hObject,eventdata) pushbutton_importFromBinary_Callback(this,hObject,eventdata),... - 'TooltipString','Imports a patient data set from binary datafiles describing CT and segmentations',... - 'Tag','pushbutton_importFromBinary'); - - hExportDicom = this.addControlToGrid([5 3],... - 'String','Export Dicom',... - 'BackgroundColor',matRad_cfg.gui.elementColor,... - 'ForegroundColor',matRad_cfg.gui.textColor,... - 'Callback',@(hObject,eventdata) exportDicomButton_Callback(this, hObject,eventdata),... - 'Tag','exportDicomButton'); - + + h72 = this.addControlToGrid([2 4], ... + 'Style', 'text', ... + 'String', 'Status:', ... + 'BackgroundColor', matRad_cfg.gui.backgroundColor, ... + 'ForegroundColor', matRad_cfg.gui.textColor, ... + 'Tag', 'txtStatus', ... + 'FontSize', round(matRad_cfg.gui.fontSize * 1.2)); + + h73 = this.addControlToGrid([3 4], ... + 'String', 'no data loaded', ... + 'Style', 'text', ... + 'BackgroundColor', matRad_cfg.gui.backgroundColor, ... + 'ForegroundColor', matRad_cfg.gui.textColor, ... + 'Tag', 'txtInfo', ... + 'FontSize', round(matRad_cfg.gui.fontSize * 1.2)); + + hMatLoad = this.addControlToGrid([2 1], ... + 'String', 'Load *.mat data', ... + 'BackgroundColor', matRad_cfg.gui.elementColor, ... + 'ForegroundColor', matRad_cfg.gui.textColor, ... + 'Callback', @(hObject, eventdata) btnLoadMat_Callback(this, hObject, eventdata), ... + 'Tag', 'btnLoadMat'); + + hDijCalc = this.addControlToGrid([3 1], ... + 'String', 'Calc. Dose Influence', ... + 'BackgroundColor', matRad_cfg.gui.elementColor, ... + 'ForegroundColor', matRad_cfg.gui.textColor, ... + 'Callback', @(hObject, eventdata) btnCalcDose_Callback(this, hObject, eventdata), ... + 'Tag', 'btnCalcDose'); + + hOpt = this.addControlToGrid([4 1], ... + 'Parent', parent, ... + 'String', 'Optimize', ... + 'BackgroundColor', matRad_cfg.gui.elementColor, ... + 'ForegroundColor', matRad_cfg.gui.textColor, ... + 'Callback', @(hObject, eventdata) btnOptimize_Callback(this, hObject, eventdata), ... + 'Tag', 'btnOptimize'); + + hLoadDicom = this.addControlToGrid([2 2], ... + 'String', 'Load DICOM', ... + 'BackgroundColor', matRad_cfg.gui.elementColor, ... + 'ForegroundColor', matRad_cfg.gui.textColor, ... + 'Callback', @(hObject, eventdata) btnLoadDicom_Callback(this, hObject, eventdata), ... + 'Tag', 'btnLoadDicom'); + + hRefresh = this.addControlToGrid([1 1], ... + 'String', 'Refresh', ... + 'BackgroundColor', matRad_cfg.gui.elementColor, ... + 'ForegroundColor', matRad_cfg.gui.textColor, ... + 'Callback', @(hObject, eventdata) btnRefresh_Callback(this, hObject, eventdata), ... + 'Tag', 'btnRefresh'); + + hRecalc = this.addControlToGrid([4 2], ... + 'String', 'Recalculate Dose', ... + 'BackgroundColor', matRad_cfg.gui.elementColor, ... + 'ForegroundColor', matRad_cfg.gui.textColor, ... + 'Callback', @(hObject, eventdata) pushbutton_recalc_Callback(this, hObject, eventdata), ... + 'Tag', 'pushbutton_recalc'); + + hKeep = this.addControlToGrid([5 1], ... + 'String', 'Save/Keep Result', ... + 'BackgroundColor', matRad_cfg.gui.elementColor, ... + 'ForegroundColor', matRad_cfg.gui.textColor, ... + 'Callback', @(hObject, eventdata) btnSaveToGUI_Callback(this, hObject, eventdata), ... + 'Tag', 'btnSaveToGUI'); + + hExportBin = this.addControlToGrid([5 2], ... + 'String', 'Export Binary', ... + 'BackgroundColor', matRad_cfg.gui.elementColor, ... + 'ForegroundColor', matRad_cfg.gui.textColor, ... + 'Callback', @(hObject, eventdata) btn_export_Callback(this, hObject, eventdata), ... + 'Children', [], ... + 'Tag', 'btn_export'); + + hImportDose = this.addControlToGrid([4 3], ... + 'String', 'Import Dose', ... + 'BackgroundColor', matRad_cfg.gui.elementColor, ... + 'ForegroundColor', matRad_cfg.gui.textColor, ... + 'Callback', @(hObject, eventdata) importDoseButton_Callback(this, hObject, eventdata), ... + 'Tag', 'importDoseButton'); + + hImportBin = this.addControlToGrid([2 3], ... + 'String', 'Import from Binary', ... + 'BackgroundColor', matRad_cfg.gui.elementColor, ... + 'ForegroundColor', matRad_cfg.gui.textColor, ... + 'Callback', @(hObject, eventdata) pushbutton_importFromBinary_Callback(this, hObject, eventdata), ... + 'TooltipString', 'Imports a patient data set from binary datafiles describing CT and segmentations', ... + 'Tag', 'pushbutton_importFromBinary'); + + hExportDicom = this.addControlToGrid([5 3], ... + 'String', 'Export Dicom', ... + 'BackgroundColor', matRad_cfg.gui.elementColor, ... + 'ForegroundColor', matRad_cfg.gui.textColor, ... + 'Callback', @(hObject, eventdata) exportDicomButton_Callback(this, hObject, eventdata), ... + 'Tag', 'exportDicomButton'); + this.createHandles(); - - handles=this.handles; + + handles = this.handles; matRad_cfg = MatRad_Config.instance(); if matRad_cfg.eduMode - %Visisbility in Educational Mode - eduHideHandles = {handles.pushbutton_importFromBinary,... - handles.btnLoadDicom,... - handles.btn_export,... - handles.exportDicomButton,... - handles.importDoseButton}; - cellfun(@(h) set(h,'Visible','Off'),eduHideHandles); - end - this.handles=handles; + % Visisbility in Educational Mode + eduHideHandles = {handles.pushbutton_importFromBinary, ... + handles.btnLoadDicom, ... + handles.btn_export, ... + handles.exportDicomButton, ... + handles.importDoseButton}; + cellfun(@(h) set(h, 'Visible', 'Off'), eduHideHandles); + end + this.handles = handles; end - - function this = doUpdate(this,evt) - %If the pln was changed, we do not do a consistency check (or - %at least we do not throw a warning when it is inconsistent) - if nargin < 2 || any(strcmp(evt.changedVariables,'pln')) + + function this = doUpdate(this, evt) + % If the pln was changed, we do not do a consistency check (or + % at least we do not throw a warning when it is inconsistent) + if nargin < 2 || any(strcmp(evt.changedVariables, 'pln')) noCheck = true; else noCheck = false; end - this.getFromWorkspace(noCheck); - end - - function this = getFromWorkspace(this,noCheck) + this.getFromWorkspace(noCheck); + end + + function this = getFromWorkspace(this, noCheck) if nargin < 2 noCheck = false; @@ -263,191 +262,186 @@ function btnLoadMat_Callback(this, hObject, event) handles = this.handles; matRad_cfg = MatRad_Config.instance(); % no data loaded, disable the buttons - set(handles.txtInfo,'String','no data loaded'); - set(handles.btnCalcDose,'Enable','off'); - set(handles.btnOptimize ,'Enable','off'); - set(handles.pushbutton_recalc,'Enable','off'); - set(handles.btnSaveToGUI,'Enable','off'); - set(handles.importDoseButton,'Enable','off'); - set(handles.btn_export,'Enable','off'); - set(handles.exportDicomButton,'Enable','off'); - - - if evalin('base','exist(''ct'')') && evalin('base','exist(''cst'')') - - set(handles.txtInfo,'String','loaded and ready'); - - if evalin('base','exist(''pln'')') - pln = evalin('base','pln'); - + set(handles.txtInfo, 'String', 'no data loaded'); + set(handles.btnCalcDose, 'Enable', 'off'); + set(handles.btnOptimize, 'Enable', 'off'); + set(handles.pushbutton_recalc, 'Enable', 'off'); + set(handles.btnSaveToGUI, 'Enable', 'off'); + set(handles.importDoseButton, 'Enable', 'off'); + set(handles.btn_export, 'Enable', 'off'); + set(handles.exportDicomButton, 'Enable', 'off'); + + if evalin('base', 'exist(''ct'')') && evalin('base', 'exist(''cst'')') + + set(handles.txtInfo, 'String', 'loaded and ready'); + + if evalin('base', 'exist(''pln'')') + pln = evalin('base', 'pln'); + % ct cst and pln available; ready for dose calculation - set(handles.txtInfo,'String','ready for dose calculation'); - set(handles.btnCalcDose,'Enable','on'); - set(handles.btn_export,'Enable','on'); - set(handles.exportDicomButton,'Enable','on'); + set(handles.txtInfo, 'String', 'ready for dose calculation'); + set(handles.btnCalcDose, 'Enable', 'on'); + set(handles.btn_export, 'Enable', 'on'); + set(handles.exportDicomButton, 'Enable', 'on'); % check if stf exists - if evalin('base','exist(''stf'')') - stf = evalin('base','stf'); + if evalin('base', 'exist(''stf'')') + stf = evalin('base', 'stf'); % check if dij, stf and pln match - [plnStfMatch, msg] = matRad_comparePlnStf(pln,stf); + [plnStfMatch, msg] = matRad_comparePlnStf(pln, stf); if plnStfMatch % plan is ready for optimization - set(handles.txtInfo,'String','ready for dose calculation'); - set(handles.btnOptimize ,'Enable','on'); - elseif ~noCheck + set(handles.txtInfo, 'String', 'ready for dose calculation'); + set(handles.btnOptimize, 'Enable', 'on'); + elseif ~noCheck this.showWarning(msg); else - %Nothing + % Nothing end % check if dij exist - conf3D = isfield(pln,'propOpt') && isfield(pln.propOpt,'conf3D') && pln.propOpt.conf3D; + conf3D = isfield(pln, 'propOpt') && isfield(pln.propOpt, 'conf3D') && pln.propOpt.conf3D; - if evalin('base','exist(''dij'')') && plnStfMatch && ~conf3D - dij = evalin('base','dij'); - [dijStfMatch, msg] = matRad_compareDijStf(dij,stf); + if evalin('base', 'exist(''dij'')') && plnStfMatch && ~conf3D + dij = evalin('base', 'dij'); + [dijStfMatch, msg] = matRad_compareDijStf(dij, stf); if dijStfMatch - set(handles.txtInfo,'String','ready for optimization'); - set(handles.btnOptimize ,'Enable','on'); - elseif ~noCheck + set(handles.txtInfo, 'String', 'ready for optimization'); + set(handles.btnOptimize, 'Enable', 'on'); + elseif ~noCheck this.showWarning(msg); else - %Nothing + % Nothing end end - + end % does resultGUI exist - if evalin('base','exist(''resultGUI'')') - set(handles.pushbutton_recalc,'Enable','on'); - set(handles.btnSaveToGUI,'Enable','on'); + if evalin('base', 'exist(''resultGUI'')') + set(handles.pushbutton_recalc, 'Enable', 'on'); + set(handles.btnSaveToGUI, 'Enable', 'on'); % resultGUI struct needs to be available to import dose % otherwise inconsistent states can be achieved - set(handles.importDoseButton,'Enable','on'); + set(handles.importDoseButton, 'Enable', 'on'); end end else % Do Nothing end - this.handles=handles; + this.handles = handles; end - - + end methods (Access = private) - - function h = addControlToGrid(this,gridPos,varargin) + + function h = addControlToGrid(this, gridPos, varargin) matRad_cfg = MatRad_Config.instance(); parent = this.widgetHandle; - - %Use a 5 x 5 grid - pos = this.computeGridPos(gridPos,[5 5]); - - h = uicontrol('Parent',parent,... - 'Units','normalized',... - 'Position',pos,... - 'FontSize',matRad_cfg.gui.fontSize,... - 'FontName',matRad_cfg.gui.fontName,... - 'FontWeight',matRad_cfg.gui.fontWeight,... - varargin{:}); + + % Use a 5 x 5 grid + pos = this.computeGridPos(gridPos, [5 5]); + + h = uicontrol('Parent', parent, ... + 'Units', 'normalized', ... + 'Position', pos, ... + 'FontSize', matRad_cfg.gui.fontSize, ... + 'FontName', matRad_cfg.gui.fontName, ... + 'FontWeight', matRad_cfg.gui.fontWeight, ... + varargin{:}); end - - + % H75 Callback function btnCalcDose_Callback(this, hObject, eventdata) % hObject handle to btnCalcDose (see GCBO) % eventdata reserved - to be defined in a future version of MATLAB % handles structure with handles and user data (see GUIDATA) - + % http://stackoverflow.com/questions/24703962/trigger-celleditcallback-before-button-callback % http://www.mathworks.com/matlabcentral/newsreader/view_thread/332613 % wait some time until the CallEditCallback is finished % Callback triggers the cst saving mechanism from GUI - + handles = this.handles; try % indicate that matRad is busy % change mouse pointer to hour glass - Figures = gcf;%findobj('type','figure'); + Figures = gcf; % findobj('type','figure'); set(Figures, 'pointer', 'watch'); drawnow; % disable all active objects - InterfaceObj = findobj(Figures,'Enable','on'); - set(InterfaceObj,'Enable','off'); - - + InterfaceObj = findobj(Figures, 'Enable', 'on'); + set(InterfaceObj, 'Enable', 'off'); + % read plan from gui and save it to workspace - %handles=getPlnFromGUI(this); - + % handles=getPlnFromGUI(this); + % get default iso center as center of gravity of all targets if not % already defined - pln = evalin('base','pln'); - + pln = evalin('base', 'pln'); + catch ME % change state from busy to normal set(Figures, 'pointer', 'arrow'); - set(InterfaceObj,'Enable','on'); + set(InterfaceObj, 'Enable', 'on'); this.handles = handles; - showError(this,'CalcDoseCallback: Error in preprocessing!',ME); - return; + showError(this, 'CalcDoseCallback: Error in preprocessing!', ME); + return end - + % generate steering file try - currPln = evalin('base','pln'); + currPln = evalin('base', 'pln'); % % if we run 3d conf opt -> hijack runDao to trigger computation of % % connected bixels % if strcmp(pln.radiationMode,'photons') && get(handles.radiobutton3Dconf,'Value') % currpln.propOpt.runDAO = true; % end - stf = matRad_generateStf(evalin('base','ct'),... - evalin('base','cst'),... - currPln); - assignin('base','stf',stf); + stf = matRad_generateStf(evalin('base', 'ct'), ... + evalin('base', 'cst'), ... + currPln); + assignin('base', 'stf', stf); catch ME % change state from busy to normal set(Figures, 'pointer', 'arrow'); - set(InterfaceObj,'Enable','on'); + set(InterfaceObj, 'Enable', 'on'); this.handles = handles; - showError(this,'CalcDoseCallback: Error in steering file generation!',ME); - return; + showError(this, 'CalcDoseCallback: Error in steering file generation!', ME); + return end - + % carry out dose calculation try - dij = matRad_calcDoseInfluence(evalin('base','ct'),evalin('base','cst'),stf,pln); - + dij = matRad_calcDoseInfluence(evalin('base', 'ct'), evalin('base', 'cst'), stf, pln); + % prepare dij for 3d conformal - if isfield(pln,'propOpt') && isfield(pln.propOpt,'conf3D') && pln.propOpt.conf3D - dij = matRad_collapseDij(dij); - stf = matRad_collapseStf(stf); + if isfield(pln, 'propOpt') && isfield(pln.propOpt, 'conf3D') && pln.propOpt.conf3D + dij = matRad_collapseDij(dij); + stf = matRad_collapseStf(stf); end % assign results to base worksapce - assignin('base','dij',dij); - assignin('base','stf',stf); - - + assignin('base', 'dij', dij); + assignin('base', 'stf', stf); + catch ME % change state from busy to normal set(Figures, 'pointer', 'arrow'); - set(InterfaceObj,'Enable','on'); + set(InterfaceObj, 'Enable', 'on'); this.handles = handles; - showError(this,'CalcDoseCallback: Error in dose calculation!',ME); - return; + showError(this, 'CalcDoseCallback: Error in dose calculation!', ME); + return end - + % change state from busy to normal set(Figures, 'pointer', 'arrow'); - set(InterfaceObj,'Enable','on'); + set(InterfaceObj, 'Enable', 'on'); this.handles = handles; - this.changedWorkspace('stf','dij'); - %getFromWorkspace(this); + this.changedWorkspace('stf', 'dij'); + % getFromWorkspace(this); end - + % H76 Callback function btnOptimize_Callback(this, hObject, eventdata) % hObject handle to btnOptimize (see GCBO) @@ -459,27 +453,27 @@ function btnOptimize_Callback(this, hObject, eventdata) try % indicate that matRad is busy % change mouse pointer to hour glass - Figures = gcf;%findobj('type','figure'); + Figures = gcf; % findobj('type','figure'); set(Figures, 'pointer', 'watch'); drawnow; % disable all active objects - InterfaceObj = findobj(Figures,'Enable','on'); - set(InterfaceObj,'Enable','off'); - - pln = evalin('base','pln'); - dij = evalin('base','dij'); - cst = evalin('base','cst'); + InterfaceObj = findobj(Figures, 'Enable', 'on'); + set(InterfaceObj, 'Enable', 'off'); + + pln = evalin('base', 'pln'); + dij = evalin('base', 'dij'); + cst = evalin('base', 'cst'); % optimize - [resultGUIcurrentRun,usedOptimizer] = matRad_fluenceOptimization(dij,cst,pln); - if isfield(pln,'propOpt') && isfield(pln.propOpt,'conf3D') && pln.propOpt.conf3D && strcmp(pln.radiationMode,'photons') - resultGUIcurrentRun.w = resultGUIcurrentRun.w .* ones(dij.totalNumOfBixels,1); + [resultGUIcurrentRun, usedOptimizer] = matRad_fluenceOptimization(dij, cst, pln); + if isfield(pln, 'propOpt') && isfield(pln.propOpt, 'conf3D') && pln.propOpt.conf3D && strcmp(pln.radiationMode, 'photons') + resultGUIcurrentRun.w = resultGUIcurrentRun.w .* ones(dij.totalNumOfBixels, 1); resultGUIcurrentRun.wUnsequenced = resultGUIcurrentRun.w; end - - %if resultGUI already exists then overwrite the "standard" fields - AllVarNames = evalin('base','who'); - if ismember('resultGUI',AllVarNames) - resultGUI = evalin('base','resultGUI'); + + % if resultGUI already exists then overwrite the "standard" fields + AllVarNames = evalin('base', 'who'); + if ismember('resultGUI', AllVarNames) + resultGUI = evalin('base', 'resultGUI'); oldNames = fieldnames(resultGUI); if ~isempty(this.savedResultTag) @@ -490,79 +484,77 @@ function btnOptimize_Callback(this, hObject, eventdata) end end end - end + end end resultGUI = resultGUIcurrentRun; - assignin('base','resultGUI',resultGUI); + assignin('base', 'resultGUI', resultGUI); - if ~runDAO|| ~strcmp(pln.radiationMode,'photons') - CheckOptimizerStatus(this,usedOptimizer,'Fluence') + if ~runDAO || ~strcmp(pln.radiationMode, 'photons') + CheckOptimizerStatus(this, usedOptimizer, 'Fluence'); end - + catch ME % change state from busy to normal set(Figures, 'pointer', 'arrow'); - set(InterfaceObj,'Enable','on'); + set(InterfaceObj, 'Enable', 'on'); this.handles = handles; - showError(this,'OptimizeCallback: Could not optimize!',ME); - return; + showError(this, 'OptimizeCallback: Could not optimize!', ME); + return end - + % perform sequencing and DAO try %% sequencing - - resultGUI = matRad_sequencing(resultGUI,evalin('base','stf'),pln,dij); - assignin('base','resultGUI',resultGUI); - - + + resultGUI = matRad_sequencing(resultGUI, evalin('base', 'stf'), pln, dij); + assignin('base', 'resultGUI', resultGUI); + catch ME % change state from busy to normal set(Figures, 'pointer', 'arrow'); - set(InterfaceObj,'Enable','on'); + set(InterfaceObj, 'Enable', 'on'); this.handles = handles; - showError(this,'OptimizeCallback: Could not perform sequencing',ME); - return; + showError(this, 'OptimizeCallback: Could not perform sequencing', ME); + return end - + try %% DAO - if strcmp(pln.radiationMode,'photons') && runDAO + if strcmp(pln.radiationMode, 'photons') && runDAO - showWarning(this,['Observe: You are running direct aperture optimization' filesep 'This is experimental code that has not been thoroughly debugged - especially in combination with constrained optimization.']); % was assigned to handles WHY ? - [resultGUI,usedOptimizer] = matRad_directApertureOptimization(evalin('base','dij'),evalin('base','cst'),... - resultGUI.sequencing.apertureInfo,resultGUI,pln); - assignin('base','resultGUI',resultGUI); + showWarning(this, ['Observe: You are running direct aperture optimization' filesep 'This is experimental code that has not been thoroughly debugged - especially in combination with constrained optimization.']); % was assigned to handles WHY ? + [resultGUI, usedOptimizer] = matRad_directApertureOptimization(evalin('base', 'dij'), evalin('base', 'cst'), ... + resultGUI.sequencing.apertureInfo, pln); + assignin('base', 'resultGUI', resultGUI); % check IPOPT status and return message for GUI user - CheckOptimizerStatus(this,usedOptimizer,'DAO'); + CheckOptimizerStatus(this, usedOptimizer, 'DAO'); end - - if strcmp(pln.radiationMode,'photons') && runSeq|| runDAO + if strcmp(pln.radiationMode, 'photons') && runSeq || runDAO matRad_visApertureInfo(resultGUI.sequencing.apertureInfo); end - + catch ME % change state from busy to normal set(Figures, 'pointer', 'arrow'); - set(InterfaceObj,'Enable','on'); + set(InterfaceObj, 'Enable', 'on'); this.handles = handles; - showError(this,'OptimizeCallback: Could not perform direct aperture optimization',ME); - return; + showError(this, 'OptimizeCallback: Could not perform direct aperture optimization', ME); + return end - + % change state from busy to normal set(Figures, 'pointer', 'arrow'); - set(InterfaceObj,'Enable','on'); + set(InterfaceObj, 'Enable', 'on'); this.handles = handles; - + this.changedWorkspace('resultGUI'); - %getFromWorkspace(this); + % getFromWorkspace(this); end - + % H77 Callback function btnLoadDicom_Callback(this, hObject, event) % hObject handle to btnLoadDicom (see GCBO) @@ -571,35 +563,34 @@ function btnLoadDicom_Callback(this, hObject, event) handles = this.handles; try % delete existing workspace - parse variables from base workspace - AllVarNames = evalin('base','who'); - RefVarNames = {'ct','cst','pln','stf','dij','resultGUI'}; + AllVarNames = evalin('base', 'who'); + RefVarNames = {'ct', 'cst', 'pln', 'stf', 'dij', 'resultGUI'}; for i = 1:length(RefVarNames) - if sum(ismember(AllVarNames,RefVarNames{i}))>0 - evalin('base',['clear ', RefVarNames{i}]); + if sum(ismember(AllVarNames, RefVarNames{i})) > 0 + evalin('base', ['clear ', RefVarNames{i}]); end end matRad_importDicomWidget; - + catch ME - showError(this,'DicomImport: Could not import data', ME); + showError(this, 'DicomImport: Could not import data', ME); end - + this.handles = handles; end - + % H78 Callback - button: refresh function btnRefresh_Callback(this, hObject, event) % notify so all widgets refresh this.changedWorkspace(); end - - + % H79 Callback function pushbutton_recalc_Callback(this, hObject, eventdata) - + handles = this.handles; - + try % indicate that matRad is busy % change mouse pointer to hour glass @@ -607,300 +598,294 @@ function pushbutton_recalc_Callback(this, hObject, eventdata) set(Figures, 'pointer', 'watch'); drawnow; % disable all active objects - InterfaceObj = findobj(Figures,'Enable','on'); - set(InterfaceObj,'Enable','off'); - + InterfaceObj = findobj(Figures, 'Enable', 'on'); + set(InterfaceObj, 'Enable', 'off'); + % get all data from workspace - pln = evalin('base','pln'); - stf = evalin('base','stf'); - ct = evalin('base','ct'); - cst = evalin('base','cst'); - resultGUI = evalin('base','resultGUI'); - - - if sum([stf.totalNumOfBixels]) ~= length(resultGUI.w)%(['w' Suffix])) + pln = evalin('base', 'pln'); + stf = evalin('base', 'stf'); + ct = evalin('base', 'ct'); + cst = evalin('base', 'cst'); + resultGUI = evalin('base', 'resultGUI'); + + if sum([stf.totalNumOfBixels]) ~= length(resultGUI.w) % (['w' Suffix])) this.showWarning('weight vector does not corresponding to current steering file'); return end - + % change isocenter if that was changed and do _not_ recreate steering % information for i = 1:numel(pln.propStf.gantryAngles) - stf(i).isoCenter = pln.propStf.isoCenter(i,:); + stf(i).isoCenter = pln.propStf.isoCenter(i, :); end - - resultGUIreCalc = matRad_calcDoseForward(ct,cst,stf,pln,resultGUI.w); - + + resultGUIreCalc = matRad_calcDoseForward(ct, cst, stf, pln, resultGUI.w); + % delete old variables to avoid confusion - if isfield(resultGUI,'effect') - resultGUI = rmfield(resultGUI,'effect'); - resultGUI = rmfield(resultGUI,'RBExDose'); - resultGUI = rmfield(resultGUI,'RBE'); - resultGUI = rmfield(resultGUI,'alpha'); - resultGUI = rmfield(resultGUI,'beta'); + if isfield(resultGUI, 'effect') + resultGUI = rmfield(resultGUI, 'effect'); + resultGUI = rmfield(resultGUI, 'RBExDose'); + resultGUI = rmfield(resultGUI, 'RBE'); + resultGUI = rmfield(resultGUI, 'alpha'); + resultGUI = rmfield(resultGUI, 'beta'); end - if isfield(resultGUI,'LET') - resultGUI = rmfield(resultGUI,'LET'); + if isfield(resultGUI, 'LET') + resultGUI = rmfield(resultGUI, 'LET'); end - + % overwrite the "standard" fields sNames = fieldnames(resultGUIreCalc); for j = 1:length(sNames) resultGUI.(sNames{j}) = resultGUIreCalc.(sNames{j}); end - + % assign results to base worksapce - %assignin('base','dij',dij); - assignin('base','resultGUI',resultGUI); + % assignin('base','dij',dij); + assignin('base', 'resultGUI', resultGUI); - % change state from busy to normal set(Figures, 'pointer', 'arrow'); - set(InterfaceObj,'Enable','on'); - + set(InterfaceObj, 'Enable', 'on'); + this.handles = handles; this.changedWorkspace('resultGUI'); - + catch ME % change state from busy to normal set(Figures, 'pointer', 'arrow'); - set(InterfaceObj,'Enable','on'); + set(InterfaceObj, 'Enable', 'on'); this.handles = handles; - showError(this,'CalcDoseCallback: Error in dose recalculation!',ME); - return; - + showError(this, 'CalcDoseCallback: Error in dose recalculation!', ME); + return + end - + end - + % H80 Callback function btnSaveToGUI_Callback(this, hObject, eventdata) handles = this.handles; - + Width = 400; Height = 200; - ScreenSize = get(0,'ScreenSize'); - + ScreenSize = get(0, 'ScreenSize'); + % show "Provide result name" window - figHandles = get(0,'Children'); + figHandles = get(0, 'Children'); if ~isempty(figHandles) - IdxHandle = strcmp(get(figHandles,'Name'),'Provide result name'); + IdxHandle = strcmp(get(figHandles, 'Name'), 'Provide result name'); else IdxHandle = []; end - - %check if window is already exists + + % check if window is already exists if any(IdxHandle) figDialog = figHandles(IdxHandle); - %set focus + % set focus figure(figDialog); else - figDialog = dialog('Position',[ceil(ScreenSize(3)/2) ceil(ScreenSize(4)/2) Width Height],'Name','Provide result name','Color',[0.5 0.5 0.5]); - - uicontrol('Parent',figDialog,... - 'Style','text',... - 'Position',[20 Height - (0.35*Height) 350 60],... - 'String','Please provide a decriptive name for your optimization result:','FontSize',10,'BackgroundColor',[0.5 0.5 0.5]); - - try - pln = evalin('base','pln'); + figDialog = dialog('Position', [ceil(ScreenSize(3) / 2) ceil(ScreenSize(4) / 2) Width Height], 'Name', 'Provide result name', 'Color', [0.5 0.5 0.5]); + + uicontrol('Parent', figDialog, ... + 'Style', 'text', ... + 'Position', [20 Height - (0.35 * Height) 350 60], ... + 'String', 'Please provide a decriptive name for your optimization result:', 'FontSize', 10, 'BackgroundColor', [0.5 0.5 0.5]); + + try + pln = evalin('base', 'pln'); numOfBeams = pln.propStf.numOfBeams; radMode = pln.radiationMode; fractions = pln.numOfFractions; - saveString = sprintf('%s_%dbeams_%dfrac',radMode,numOfBeams,fractions); + saveString = sprintf('%s_%dbeams_%dfrac', radMode, numOfBeams, fractions); catch - saveString = datestr(now,'mmddyyHHMM'); + saveString = datestr(now, 'mmddyyHHMM'); end - - hFocus = uicontrol('Parent',figDialog,... - 'Style','edit',... - 'Position',[30 60 350 60],... - 'String',saveString,'FontSize',10,'BackgroundColor',[0.55 0.55 0.55],... - 'Callback', @(hpb,eventdata)SaveResultToGUI(this,hpb,eventdata)); - - uicontrol('Parent', figDialog,'Style', 'pushbutton', 'String', 'Save','FontSize',10,... - 'Position', [0.42*Width 0.1 * Height 70 30],... - 'Callback', @(hpb,eventdata)SaveResultToGUI(this,hpb,eventdata)); + + hFocus = uicontrol('Parent', figDialog, ... + 'Style', 'edit', ... + 'Position', [30 60 350 60], ... + 'String', saveString, 'FontSize', 10, 'BackgroundColor', [0.55 0.55 0.55], ... + 'Callback', @(hpb, eventdata)SaveResultToGUI(this, hpb, eventdata)); + + uicontrol('Parent', figDialog, 'Style', 'pushbutton', 'String', 'Save', 'FontSize', 10, ... + 'Position', [0.42 * Width 0.1 * Height 70 30], ... + 'Callback', @(hpb, eventdata)SaveResultToGUI(this, hpb, eventdata)); uicontrol(hFocus); end - + uiwait(figDialog); this.handles = handles; end - + function SaveResultToGUI(this, ~, ~) - AllFigHandles = get(0,'Children'); - ixHandle = strcmp(get(AllFigHandles,'Name'),'Provide result name'); - uiEdit = get(AllFigHandles(ixHandle),'Children'); - - + AllFigHandles = get(0, 'Children'); + ixHandle = strcmp(get(AllFigHandles, 'Name'), 'Provide result name'); + uiEdit = get(AllFigHandles(ixHandle), 'Children'); + % delete special characters - Suffix = get(uiEdit(2),'String'); - logIx = isstrprop(Suffix,'alphanum'); + Suffix = get(uiEdit(2), 'String'); + logIx = isstrprop(Suffix, 'alphanum'); Suffix = ['_' Suffix(logIx)]; - this.savedResultTag{end+1}= Suffix; + this.savedResultTag{end + 1} = Suffix; + + pln = evalin('base', 'pln'); + resultGUI = evalin('base', 'resultGUI'); - pln = evalin('base','pln'); - resultGUI = evalin('base','resultGUI'); - - if isfield(resultGUI,'physicalDose') + if isfield(resultGUI, 'physicalDose') resultGUI.(['physicalDose' Suffix]) = resultGUI.physicalDose; end - if isfield(resultGUI,'w') + if isfield(resultGUI, 'w') resultGUI.(['w' Suffix]) = resultGUI.w; end - if isfield(resultGUI,'LET') + if isfield(resultGUI, 'LET') resultGUI.(['LET' Suffix]) = resultGUI.LET; end - - - if isfield(pln,'propOpt') && ~strcmp(pln.propOpt.quantityOpt,'none') - - if isfield(resultGUI,'RBExDose') + + if isfield(pln, 'propOpt') && ~strcmp(pln.propOpt.quantityOpt, 'none') + + if isfield(resultGUI, 'RBExDose') resultGUI.(['RBExDose' Suffix]) = resultGUI.RBExDose; end - - if strcmp(pln.radiationMode,'carbon') == 1 - if isfield(resultGUI,'effect') - resultGUI.(['effect' Suffix])= resultGUI.effect; + + if strcmp(pln.radiationMode, 'carbon') == 1 + if isfield(resultGUI, 'effect') + resultGUI.(['effect' Suffix]) = resultGUI.effect; end - - if isfield(resultGUI,'RBE') + + if isfield(resultGUI, 'RBE') resultGUI.(['RBE' Suffix]) = resultGUI.RBE; end - if isfield(resultGUI,'alpha') + if isfield(resultGUI, 'alpha') resultGUI.(['alpha' Suffix]) = resultGUI.alpha; end - if isfield(resultGUI,'beta') + if isfield(resultGUI, 'beta') resultGUI.(['beta' Suffix]) = resultGUI.beta; end end end - + close(AllFigHandles(ixHandle)); - assignin('base','resultGUI',resultGUI); - + assignin('base', 'resultGUI', resultGUI); + this.changedWorkspace('resultGUI'); - %getFromWorkspace(this); + % getFromWorkspace(this); end - + % H81 Callback function btn_export_Callback(this, hObject, eventdata) % hObject handle to btn_export (see GCBO) % eventdata reserved - to be defined in a future version of MATLAB % handles structure with handles and user data (see GUIDATA) - + try matRad_exportWidget; catch ME - showError(this,'Could not export data. Reason: ', ME); + showError(this, 'Could not export data. Reason: ', ME); end end - + % H82 Callback function importDoseButton_Callback(this, hObject, eventdata) % hObject handle to importDoseButton (see GCBO) % eventdata reserved - to be defined in a future version of MATLAB % handles structure with handles and user data (see GUIDATA) handles = this.handles; - + extensions{1} = '*.nrrd'; - [filenames,filepath,~] = uigetfile(extensions,'MultiSelect','on'); - - %Import aborted + [filenames, filepath, ~] = uigetfile(extensions, 'MultiSelect', 'on'); + + % Import aborted if filenames == 0 - return; + return end - - %Something was selected + + % Something was selected try if ~iscell(filenames) tmp = filenames; filenames = cell(1); filenames{1} = tmp; end - - ct = evalin('base','ct'); - resultGUI = evalin('base','resultGUI'); - + + ct = evalin('base', 'ct'); + resultGUI = evalin('base', 'resultGUI'); + for filename = filenames - [~,name,~] = fileparts(filename{1}); - [cube,~] = matRad_readCube(fullfile(filepath,filename{1})); + [~, name, ~] = fileparts(filename{1}); + [cube, ~] = matRad_readCube(fullfile(filepath, filename{1})); if ~isequal(ct.cubeDim, size(cube)) - this.showError('Dimensions of the imported cube do not match with ct','Import failed!','modal'); - continue; + this.showError('Dimensions of the imported cube do not match with ct', 'Import failed!', 'modal'); + continue end - - fieldname = ['import_' matlab.lang.makeValidName(name, 'ReplacementStyle','delete')]; + + fieldname = ['import_' matlab.lang.makeValidName(name, 'ReplacementStyle', 'delete')]; resultGUI.(fieldname) = cube; end - - assignin('base','resultGUI',resultGUI); + + assignin('base', 'resultGUI', resultGUI); catch ME this.handles = handles; - showError(this,'Dose Import: Could not import data. Reason: ', ME); - return; + showError(this, 'Dose Import: Could not import data. Reason: ', ME); + return end this.handles = handles; this.changedWorkspace('resultGUI'); - %getFromWorkspace(this); + % getFromWorkspace(this); end - + % H83 Callback function pushbutton_importFromBinary_Callback(this, hObject, eventdata) % hObject handle to pushbutton_importFromBinary (see GCBO) % eventdata reserved - to be defined in a future version of MATLAB % handles structure with handles and user data (see GUIDATA) handles = this.handles; - - try - %call the gui - h=matRad_importWidget; + + try + % call the gui + h = matRad_importWidget; uiwait(h.widgetHandle); - + this.handles = handles; this.changedWorkspace(); - catch ME + catch ME this.handles = handles; getFromWorkspace(this); - showError(this,'Binary Patient Import: Could not import data. Reason: ', ME); - return; + showError(this, 'Binary Patient Import: Could not import data. Reason: ', ME); + return end - - - %getFromWorkspace(this); + + % getFromWorkspace(this); end - + % H84 Callback function exportDicomButton_Callback(this, hObject, eventdata) % hObject handle to exportDicom (see GCBO) % eventdata reserved - to be defined in a future version of MATLAB % handles structure with handles and user data (see GUIDATA) - try + try matRad_exportDicomWidget; catch ME - showError(this,'DicomImport: Could not export data', ME); + showError(this, 'DicomImport: Could not export data', ME); end end - - function CheckOptimizerStatus(this, usedOptimizer,OptCase) - - [statusmsg,statusflag] = usedOptimizer.GetStatus(); - + + function CheckOptimizerStatus(this, usedOptimizer, OptCase) + + [statusmsg, statusflag] = usedOptimizer.GetStatus(); + if statusflag == 0 || statusflag == 1 statusIcon = 'none'; else statusIcon = 'warn'; end - - this.showMessage(sprintf('Optimizer finished with status %d (%s)',statusflag,statusmsg),'Optimization finished!',statusIcon,'modal'); + + this.showMessage(sprintf('Optimizer finished with status %d (%s)', statusflag, statusmsg), 'Optimization finished!', statusIcon, 'modal'); end + end end - - diff --git a/matRad/matRad_sequencing.m b/matRad/matRad_sequencing.m index cad3cbf9f..54f1078ab 100644 --- a/matRad/matRad_sequencing.m +++ b/matRad/matRad_sequencing.m @@ -1,6 +1,6 @@ -function resultGUI = matRad_sequencing(resultGUI,stf,pln,dij,visMode) +function resultGUI = matRad_sequencing(resultGUI, stf, pln, dij, visMode) % matRad inverse planning wrapper function -% +% % call % resultGUI = matRad_sequencing(resultGUI,stf,dij,pln) % @@ -20,37 +20,35 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% 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/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 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/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_cfg = MatRad_Config.instance(); - -sequencer = matRad_SequencingBase.getSequencerFromPln(pln); + +sequencer = matRad_SequencerBase.getSequencerFromPln(pln); % Handle optional inputs if nargin == 5 && ~isempty(visMode) - sequencer.visMode = visMode; + sequencer.visMode = visMode; end if nargin < 4 || isempty(dij) - dij = []; + dij = []; end -sequence = sequencer.sequence(resultGUI.w,stf); +sequence = sequencer.sequence(resultGUI.w, stf); if ~isempty(dij) - resultGUI = matRad_calcCubes(sequence.w,dij); + resultGUI = matRad_calcCubes(sequence.w, dij); else - matRad_cfg.dispWarning('Dose not recalcaulted with sequenced fluence'); + matRad_cfg.dispWarning('Dose not recalcaulted with sequenced fluence'); end resultGUI.sequencing = sequence; end - - diff --git a/matRad/matRad_sequencingOld.m b/matRad/matRad_sequencingOld.m deleted file mode 100644 index 51207ada2..000000000 --- a/matRad/matRad_sequencingOld.m +++ /dev/null @@ -1,71 +0,0 @@ -function resultGUI = matRad_sequencing(resultGUI,stf,dij,pln,visBool) -% matRad inverse planning wrapper function -% -% call -% resultGUI = matRad_sequencing(resultGUI,stf,dij,pln) -% -% input -% dij: matRad dij struct -% stf: matRad stf struct -% pln: matRad pln struct -% resultGUI: struct containing optimized fluence vector, dose, and (for -% biological optimization) RBE-weighted dose etc. -% -% output -% resultGUI: struct containing optimized fluence vector, dose, and (for -% biological optimization) RBE-weighted dose etc. -% -% References -% - -% -% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% -% 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/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_cfg = MatRad_Config.instance(); - -if nargin < 5 - visBool = 0; -end - -if ~isfield(pln,'propSeq') - pln.propSeq = struct('runSequencing',false); -end - -if strcmp(pln.radiationMode,'photons') && (pln.propSeq.runSequencing || pln.propOpt.runDAO) - - if ~isfield(pln.propSeq, 'sequencer') - pln.propSeq.sequencer = 'siochi'; % default: siochi sequencing algorithm - 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).') - end - - switch pln.propSeq.sequencer - case 'xia' - resultGUI = matRad_xiaLeafSequencing(resultGUI,stf,dij,pln.propSeq.sequencingLevel,visBool); - case 'engel' - resultGUI = matRad_engelLeafSequencing(resultGUI,stf,dij,pln.propSeq.sequencingLevel,visBool); - case 'siochi' - resultGUI = matRad_siochiLeafSequencing(resultGUI,stf,dij,pln.propSeq.sequencingLevel,visBool); - otherwise - matRad_cfg.dispError('Could not find specified sequencing algorithm ''%s''',pln.propSeq.sequencer); - end -elseif (pln.propSeq.runSequencing || pln.propOpt.runDAO) && ~strcmp(pln.radiationMode,'photons') - matRad_cfg.dispWarning('Sequencing is only specified for pln.radiationMode = "photons". Continuing with out sequencing ... ') -end -end - - diff --git a/matRad/sequencing/matRad_aperture2collimation.m b/matRad/sequencing/matRad_aperture2collimation.m index 934daf8e4..50698ed8c 100644 --- a/matRad/sequencing/matRad_aperture2collimation.m +++ b/matRad/sequencing/matRad_aperture2collimation.m @@ -1,8 +1,8 @@ -function [pln,stf] = matRad_aperture2collimation(pln,stf,sequencing,apertureInfo) +function [pln, stf] = matRad_aperture2collimation(pln, stf, sequencing, apertureInfo) % matRad function to convert sequencing information / aperture information % into collimation information in pln and stf for field-based dose % calculation -% +% % call % [pln,stf] = matRad_aperture2collimation(pln,stf,sequencing,apertureInfo) % @@ -14,118 +14,36 @@ % % output % pln: matRad pln struct with collimation information -% stf: matRad stf struct with shapes instead of beamlets +% stf: matRad stf struct with shapes instead of beamlets % % References % - % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2022 the matRad development team. +% Copyright 2022 the matRad development team. % Author: wahln -% -% 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 +% +% 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. % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%Debug visualization -visBool = false; - -bixelWidth = apertureInfo.bixelWidth; -leafWidth = bixelWidth; -convResolution = 0.5; %[mm] - -%The collimator limits are infered here from the apertureInfo. This could -%be handled differently by explicitly storing collimator info in the base -%data? -symmetricMLClimits = vertcat(apertureInfo.beam.MLCWindow); -symmetricMLClimits = max(abs(symmetricMLClimits)); -fieldWidth = 2*max(symmetricMLClimits); - -%modify basic pln variables -pln.propStf.bixelWidth = 'field'; -pln.propStf.collimation.convResolution = 0.5; %[mm] -pln.propStf.collimation.fieldWidth = fieldWidth; -pln.propStf.collimation.leafWidth = leafWidth; - -% -%[bixelFieldX,bixelFieldY] = ndgrid(-fieldWidth/2:bixelWidth:fieldWidth/2,-fieldWidth/2:leafWidth:fieldWidth/2); -[convFieldX,convFieldY] = meshgrid(-fieldWidth/2:convResolution:fieldWidth/2); - -%TODO: Not used in calcPhotonDose but imported from DICOM -%pln.propStf.collimation.Devices ... -%pln.propStf.collimation.numOfFields -%pln.propStf.collimation.beamMeterset +matRad_cfg = MatRad_Config.instance(); -for iBeam = 1:numel(stf) - stfTmp = stf(iBeam); - beamSequencing = sequencing.beam(iBeam); - beamAperture = apertureInfo.beam(iBeam); - - stfTmp.bixelWidth = 'field'; - - nShapes = beamSequencing.numOfShapes; +matRad_cfg.dispWarning('This function is outdated use, class intead'); - stfTmp.numOfRays = 1;% - stfTmp.numOfBixelsPerRay = nShapes; - stfTmp.totalNumOfBixels = nShapes; - - ray = struct(); - ray.rayPos_bev = [0 0 0]; - ray.targetPoint_bev = [0 stfTmp.SAD 0]; - ray.weight = 1; - ray.energy = stfTmp.ray(1).energy; - ray.beamletCornersAtIso = stfTmp.ray(1).beamletCornersAtIso; - ray.rayCorners_SCD = stfTmp.ray(1).rayCorners_SCD; +sequencer = matRad_SequencerBase.getSequencerFromPln(pln); - %ray.shape = beamSequencing.sum; - shapeTotalF = zeros(size(convFieldX)); - - ray.shapes = struct(); - for iShape = 1:nShapes - currShape = beamAperture.shape(iShape); - activeLeafPairPosY = beamAperture.leafPairPos; - F = zeros(size(convFieldX)); - if visBool - hF = figure; imagesc(F); title(sprintf('Beam %d, Shape %d',iBeam,iShape)); hold on; - end - for iLeafPair = 1:numel(activeLeafPairPosY) - posY = activeLeafPairPosY(iLeafPair); - ixY = convFieldY >= posY-leafWidth/2 & convFieldY < posY + leafWidth/2; - ixX = convFieldX >= currShape.leftLeafPos(iLeafPair) & convFieldX < currShape.rightLeafPos(iLeafPair); - ix = ixX & ixY; - F(ix) = 1; - if visBool - figure(hF); imagesc(F); drawnow; pause(0.1); - end - end - - if visBool - pause(1); close(hF); - end - - F = F*currShape.weight; - shapeTotalF = shapeTotalF + F; - - ray.shapes(iShape).convFluence = F; - ray.shapes(iShape).shapeMap = currShape.shapeMap; - ray.shapes(iShape).weight = currShape.weight; - ray.shapes(iShape).leftLeafPos = currShape.leftLeafPos; - ray.shapes(iShape).rightLeafPos = currShape.rightLeafPos; - ray.shapes(iShape).leafPairCenterPos = activeLeafPairPosY; +if ~exist("apertureInfo") + if ~isfield(sequencing, 'apertureInfo') + sequencing.aperatureInfo = aperaturInfo; end - - ray.shape = shapeTotalF; - ray.weight = ones(1,nShapes); - ray.collimation = pln.propStf.collimation; - stfTmp.ray = ray; - - stf(iBeam) = stfTmp; end +[pln, stf] = sequencer.aperture2collimation(pln, stf, sequencing); - +end diff --git a/matRad/sequencing/matRad_engelLeafSequencing.m b/matRad/sequencing/matRad_engelLeafSequencing.m index bc1436574..1552c5de2 100644 --- a/matRad/sequencing/matRad_engelLeafSequencing.m +++ b/matRad/sequencing/matRad_engelLeafSequencing.m @@ -1,8 +1,8 @@ -function resultGUI = matRad_engelLeafSequencing(resultGUI,stf,dij,numOfLevels,visBool) -% multileaf collimator leaf sequencing algorithm -% for intensity modulated beams with multiple static segments accroding +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 -% +% % call % resultGUI = matRad_engelLeafSequencing(resultGUI,stf,dij,numOfLevels,visBool) % @@ -23,373 +23,32 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% 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/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 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/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. % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -% if visBool not set toogle off visualization -if nargin < 5 - visBool = 0; -end - -numOfBeams = numel(stf); - -if visBool - % create the sequencing figure - sz = [800 1000]; % figure size - 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)]); -end - -offset = 0; - -for i = 1:numOfBeams - - numOfRaysPerBeam = stf(i).numOfRays; - - % get relevant weights for current beam - wOfCurrBeams = resultGUI.w(1+offset:numOfRaysPerBeam+offset).* ones(size(stf(i).ray,2),1); - - X = ones(size(stf(i).ray,2),1)*NaN; %this way it also works with3dconformal - Z = ones(size(stf(i).ray,2),1)*NaN; - - for j=1:size(stf(i).ray,2) - X(j) = stf(i).ray(j).rayPos_bev(:,1); - Z(j) = stf(i).ray(j).rayPos_bev(:,3); - end - - % sort bixels into matrix - minX = min(X); - maxX = max(X); - minZ = min(Z); - maxZ = max(Z); - - dimOfFluenceMxX = (maxX-minX)/stf(i).bixelWidth + 1; - dimOfFluenceMxZ = (maxZ-minZ)/stf(i).bixelWidth + 1; - - %Create the fluence matrix. - fluenceMx = zeros(dimOfFluenceMxZ,dimOfFluenceMxX); - - % Calculate X and Z position of every fluence's matrix spot - % z axis = axis of leaf movement! - xPos = (X-minX)/stf(i).bixelWidth+1; - zPos = (Z-minZ)/stf(i).bixelWidth+1; - - % Make subscripts for fluence matrix - indInFluenceMx = zPos + (xPos-1)*dimOfFluenceMxZ; - - %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); - - 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 - - % start sequencer - while max(D_k(:) > 0) - - %calculate the difference matrix diffMat - diffMat = diff([zeros(size(D_k,1),1) D_k zeros(size(D_k,1),1)],[],2); - - %calculate complexities - c = sum(max(0,diffMat),2); %TNMU-row-complexity - com = max(c); %TNMU complexity - g = com - c; %row complexity gap - - %initialize segment - segment = zeros(size(D_k)); - - k = k + 1; - - %Plot residual intensity matrix. - if visBool - seqSubPlots(2) = subplot(2,2,2,'parent',seqFig); - imagesc(D_k,'parent',seqSubPlots(2)); - set(seqSubPlots(2),'CLim',[0 numOfLevels],'YDir','normal'); - title(seqSubPlots(2),['k = ' num2str(k)]); - colorbar - drawnow - end - - - %loop over all rows - for j=1:size(D_0,1) - - %determine essential intervals - data(j).left(1) = 0; %left interval limit, actual for an empty interval - data(j).right(1) = 0; %right interal limit, actual for an empty interval - data(j).v(1) = g(j); %greatest number such that the inequalities (6) resp. (7) is satisfied with u=v - data(j).w(1) = inf; %smallest number in the interval - data(j).u(1) = data(j).v(1); %min(v,w) - - [~, pos, ~] = find(diffMat(j,:) > 0); % indices of all positive elements in the j. row of diffmat - [~, neg, ~] = find(diffMat(j,:) < 0); % indices of all negative elements in the j. row of diffMat - - n=2; - - %loop over the positive elements in the j. row of diffmat -> - %possible left interval limits - for m=1:size(pos,2) - - %loop over the negative elements in the j. row of diffMat -> - %possible right interval limit - for l=1:size(neg,2) - - %take only intervals I=[l,r] with l<=r - if pos(m) <= neg(l)-1 - - %set interval limits - data(j).left(n) = pos(m); - data(j).right(n) = neg(l)-1; - - %calculate v according to Lemma 8 - if g(j) <= abs( diffMat(j,pos(m)) + diffMat(j,neg(l)) ) - data(j).v(n) = min( diffMat(j,pos(m)), -diffMat(j,neg(l)) ) + g(j); - else - data(j).v(n) = ( diffMat(j, pos(m)) - diffMat(j, neg(l)) + g(j)) / 2; - end - - %calculate w and u according to equality (11) and - %(12) - data(j).w(n) = min(D_k(j,pos(m):(neg(l)-1))); - data(j).u(n) = min(data(j).v(n), data(j).w(n)); - - n = n+1; - end - end - end - - u(j) = max(data(j).u); - - end - - %calculate u_max from theorem 9 - d_k = min(u); - - %loop over all rows - for j=1:size(D_0,1) - - %find all possible (and essential) intervals - candidate = find(data(j).u >= d_k); - - %calculate the potential of the possible intervals - - %initialize p as -Inf - data(j).p(1:length(data(j).left)) = -Inf; - - %loop over all possible intervals - for s=1:size(candidate,2) - - if (s==1 && data(j).left(candidate(s)) == 0) - data(j).p(candidate(1)) = 0; - - - else - %calculate p1 according to equality (17) - if (d_k == diffMat(j, data(j).left(candidate(s))) && d_k ~= D_k(j, data(j).left(candidate(s)))) - p1 = 1; - - else - p1 = 0; - - end - - %calculate p2 according to equalitiy (18) - % if data(j).right(candidate(s)) < size(D_0, 2) - - if (d_k == -diffMat(j, data(j).right(candidate(s))+1) && d_k ~= D_k(j, data(j).right(candidate(s)))) - p2 = 1; - else - p2 = 0; - end - -% else -% -% if d_k == -diffMat(j, data(j).right(candidate(s))+1) -% p2 = 1; -% else -% p2 = 0; -% end -% -% end - - %calculate p3 according to equality (19) - p3 = size(find(D_k(j, data(j).left(candidate(s)):data(j).right(candidate(s))) == d_k),2); - - data(j).p(candidate(s)) = p1 + p2+ p3; - - end - - end - - %determinate intervals with maximum potential - maxPot = find(data(j).p == max(data(j).p)); - - %if several intervals have maximum potential, select - %the interval which has maximum length - if size(maxPot,2) > 1 - - for t=1:size(maxPot,2) - if t==1 && data(j).left(maxPot(t)) == 0 - data(j).l(1) = 0; - else - data(j).l(maxPot(t)) = data(j).right(maxPot(t)) - data(j).left(maxPot(t)) + 1; - end - end - - %data(j).l(maxPot) = data(j).right(maxPot) - data(j).left(maxPot) + 1; - - maxLength = find(data(j).l == max(data(j).l)); - - %left and right interval limits of the selected - %interval - leftIntLimit(j) = data(j).left(maxLength(1)); - rightIntLimit(j) = data(j).right(maxLength(1)); - - - else - - %left and right interval limits of the selected - %interval - leftIntLimit(j) = data(j).left(maxPot); - rightIntLimit(j) = data(j).right(maxPot); - - - end - - %create segment associated by the selected interval - if leftIntLimit(j) ~= 0 - - segment(j,leftIntLimit(j):rightIntLimit(j)) = 1; - - end - - end - - %write the segment in shape_k - shape_k = segment; - - %show the leaf positions - if visBool - seqSubPlots(4) = subplot(2,2,3.5,'parent',seqFig); - imagesc(shape_k,'parent',seqSubPlots(4)); - hold(seqSubPlots(4),'on'); - set(seqSubPlots(4),'YDir','normal') - xlabel(seqSubPlots(4),'x - direction parallel to leaf motion ') - ylabel(seqSubPlots(4),'z - direction perpendicular to leaf motion ') - title(seqSubPlots(4),['beam # ' num2str(i) ' shape # ' num2str(k) ' d_k = ' num2str(d_k)]); - for j = 1:dimOfFluenceMxZ - leftLeafIx = find(shape_k(j,:)>0,1,'first'); - rightLeafIx = find(shape_k(j,:)>0,1,'last'); - if leftLeafIx > 1 - plot(seqSubPlots(4),[.5 leftLeafIx-.5],j-[.5 .5] ,'w','LineWidth',2) - plot(seqSubPlots(4),[.5 leftLeafIx-.5],j+[.5 .5] ,'w','LineWidth',2) - plot(seqSubPlots(4),[ leftLeafIx-.5 leftLeafIx-.5],j+[.5 -.5] ,'w','LineWidth',2) - end - if rightLeafIx0 - - 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; - - 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); - end - - sequencing.w(1+offset:numOfRaysPerBeam+offset,1) = D_0(indInFluenceMx)/numOfLevels*calFac; - - offset = offset + numOfRaysPerBeam; - -end - -resultGUI.w = sequencing.w; -resultGUI.wSequenced = sequencing.w; +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +matRad_cfg = MatRad_Config.instance(); -resultGUI.sequencing = sequencing; -resultGUI.apertureInfo = matRad_sequencing2ApertureInfo(sequencing,stf); +matRad_cfg.dispWarning('This function is Outdated use new SequencingClass'); -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, ... - doseSequencedDoseGrid, ... - dij.ctGrid.x,dij.ctGrid.y',dij.ctGrid.z); +sequencer = matRad_SequencingPhotonsEngelLeaf(); +sequencer.visMode = visBool; +sequencer.sequencingLevel = numOfLevels; -% if weights exists from an former DAO remove it -if isfield(resultGUI,'wDao') - resultGUI = rmfield(resultGUI,'wDao'); +sequence = sequencer.sequence(resultGUI.w, stf); +if ~isempty(dij) + resultGUI = matRad_calcCubes(sequence.w, dij); +else + matRad_cfg.dispWarning('Dose not recalcaulted with sequenced fluence'); end +resultGUI.sequencing = sequence; end - diff --git a/matRad/sequencing/matRad_siochiLeafSequencing.m b/matRad/sequencing/matRad_siochiLeafSequencing.m index 02841d5b0..dbaedf746 100644 --- a/matRad/sequencing/matRad_siochiLeafSequencing.m +++ b/matRad/sequencing/matRad_siochiLeafSequencing.m @@ -1,6 +1,6 @@ -function resultGUI = matRad_siochiLeafSequencing(resultGUI,stf,dij,numOfLevels,visBool) -% multileaf collimator leaf sequencing algorithm -% for intensity modulated beams with multiple static segments according to +function resultGUI = matRad_siochiLeafSequencing(resultGUI, stf, dij, numOfLevels, visBool) +% multileaf collimator leaf sequencing algorithm +% for intensity modulated beams with multiple static segments according to % Siochi (1999)International Journal of Radiation Oncology * Biology * Physics, % originally implemented in PLUNC (https://sites.google.com/site/planunc/) % @@ -40,340 +40,20 @@ % LICENSE file. % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +matRad_cfg = MatRad_Config.instance(); -% if visBool not set toogle off visualization -if nargin < 5 - visBool = 0; -end - -numOfBeams = numel(stf); - -if visBool - % create the sequencing figure - sz = [800 1000]; % figure size - 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)]); -end - -offset = 0; - -if ~isfield(resultGUI,'wUnsequenced') - wUnsequenced = resultGUI.w; -else - wUnsequenced = resultGUI.wUnsequenced; -end - -for i = 1:numOfBeams - - numOfRaysPerBeam = stf(i).numOfRays; - - % get relevant weights for current beam - wOfCurrBeams = wUnsequenced(1+offset:numOfRaysPerBeam+offset).* ones(size(stf(i).ray,2),1);%REVIEW OFFSET - - X = ones(size(stf(i).ray,2),1)*NaN; %this way it also works with3dconformal - Z = ones(size(stf(i).ray,2),1)*NaN; - - for j = 1:size(stf(i).ray,2) - X(j) = stf(i).ray(j).rayPos_bev(:,1); - Z(j) = stf(i).ray(j).rayPos_bev(:,3); - end - - % sort bixels into matrix - minX = min(X); - maxX = max(X); - minZ = min(Z); - maxZ = max(Z); - - dimOfFluenceMxX = (maxX-minX)/stf(i).bixelWidth + 1; - dimOfFluenceMxZ = (maxZ-minZ)/stf(i).bixelWidth + 1; - - %Create the fluence matrix. - fluenceMx = zeros(dimOfFluenceMxZ,dimOfFluenceMxX); - - % Calculate X and Z position of every fluence's matrix spot z axis = - % axis of leaf movement! - xPos = (X-minX)/stf(i).bixelWidth+1; - zPos = (Z-minZ)/stf(i).bixelWidth+1; - - % Make subscripts for fluence matrix - indInFluenceMx = zPos + (xPos-1)*dimOfFluenceMxZ; - - %Save weights in fluence matrix. - fluenceMx(indInFluenceMx) = wOfCurrBeams; - - % Stratification - calFac = max(fluenceMx(:)); - D_k = round(fluenceMx/calFac*numOfLevels); - - % Save the stratification in the initial intensity matrix D_0. - D_0 = D_k; - - % container to remember generated shapes; allocate space for 10000 - % shapes - shapes = NaN*ones(dimOfFluenceMxZ,dimOfFluenceMxX,10000); - shapesWeight = zeros(10000,1); - k = 0; - - if visBool - clf(seqFig); - colormap(seqFig,'jet'); - - seqSubPlots(1) = subplot(2,2,1,'parent',seqFig); - imagesc(D_k,'parent',seqSubPlots(1)); - set(seqSubPlots(1),'CLim',[0 numOfLevels],'YDir','normal'); - title(seqSubPlots(1),['Beam # ' num2str(i) ': max(D_0) = ' num2str(max(D_0(:))) ' - ' num2str(numel(unique(D_0))) ' intensity levels']); - xlabel(seqSubPlots(1),'x - direction parallel to leaf motion ') - ylabel(seqSubPlots(1),'z - direction perpendicular to leaf motion ') - colorbar; - drawnow - 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); - - 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 - - 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 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 - offset = offset + numOfRaysPerBeam; - -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 -resultGUI.physicalDose = matRad_interp3(dij.doseGrid.x,dij.doseGrid.y',dij.doseGrid.z, ... - doseSequencedDoseGrid, ... - dij.ctGrid.x,dij.ctGrid.y',dij.ctGrid.z); - -% if weights exists from an former DAO remove it -if isfield(resultGUI,'wDao') - resultGUI = rmfield(resultGUI,'wDao'); -end +matRad_cfg.dispWarning('This function is Outdated use new SequencingClass'); -end - -function [tops, bases] = matRad_siochiDecomposePort(map,dimZ,dimX,minZ,maxZ,minX,maxX) -%Returns tops and bases of a fluence matrix "map" for Siochi leaf -%sequencing algorithm (rod pushing part). Accounts for collisions and -%tongue and groove (Tng) effects. - -tops = zeros(dimZ, dimX); -bases = zeros(dimZ, dimX); - -for i = minX:maxX - maxTop = -1; - TnG = 1; - for j = minZ:maxZ - if i == minX - bases(j,i) = 1; - tops(j,i) = bases(j,i)+map(j,i)-1; - else %assign trial base positions - if map(j,i) >= map(j,i-1) %current rod >= previous, match the bases - bases(j,i) = bases(j,i-1); - tops(j,i) = bases(j,i)+map(j,i)-1; - else %current rod maxTop - maxTop = tops(j,i); - maxRow = j; - end - end - - %Correct for collision and tongue and groove error - while(TnG) - %go from maxRow down checking for TnG. This occurs when a shorter - %rod is "peeking over" a longer one in the direction transverse to - %the leaf motion. To fix this, match either the tops or bases of - %the rods. - for j = (maxRow-1):-1:minZ - if map(j,i) < map(j+1,i) - if tops(j,i) > tops(j+1,i) - tops(j+1,i) = tops(j,i); - bases(j+1,i) = tops(j+1,i)-map(j+1,i)+1; - elseif bases(j,i) < bases(j+1,i) - bases(j,i) = bases(j+1,i); - tops(j,i) = bases(j,i)+map(j,i)-1; - end - else - if tops(j,i) < tops(j+1,i) - tops(j,i) = tops(j+1,i); - bases(j,i) = tops(j,i)-map(j,i)+1; - elseif bases(j,i) > bases(j+1,i) - bases(j+1,i) = bases(j,i); - tops(j+1,i) = bases(j+1,i)+map(j+1,i)-1; - end - end - end - %go from maxRow up checking for TnG - for j = (maxRow+1):maxZ - if map(j,i) < map(j-1,i) - if tops(j,i) > tops(j-1,i) - tops(j-1,i) = tops(j,i); - bases(j-1,i) = tops(j-1,i)-map(j-1,i)+1; - elseif bases(j,i) < bases(j-1,i) - bases(j,i) = bases(j-1,i); - tops(j,i) = bases(j,i)+map(j,i)-1; - end - else - if tops(j,i) < tops(j-1,i) - tops(j,i) = tops(j-1,i); - bases(j,i) = tops(j,i)-map(j,i)+1; - elseif bases(j,i) > bases(j-1,i) - bases(j-1,i) = bases(j,i); - tops(j-1,i) = bases(j-1,i)+map(j-1,i)-1; - end - end - end - %now check if all TnG conditions have been removed - TnG = 0; - for j = (minZ+1):maxZ - if map(j,i) < map(j-1,i); - if tops(j,i) > tops(j-1,i) - TnG = 1; - elseif bases(j,i) < bases(j-1,i) - TnG = 1; - end - else - if tops(j,i) < tops(j-1,i) - TnG = 1; - elseif bases(j,i) > bases(j-1,i) - TnG = 1; - end - end - end - end -end - -end - -function [shapes,shapesWeight,k,D_k] = matRad_siochiConvertToSegments(shapes,shapesWeight,k,tops,bases,visBool,i,D_k,numOfLevels,seqFig,seqSubPlots) -%Convert tops and bases to shape matrices. These are taken as to be the -%shapes of uniform level/elevation after the rods are pushed. -if nargin < 6 - visBool = 0; -end +sequencer = matRad_SequencingPhotonsSiochiLeaf(); +sequencer.visMode = visBool; +sequencer.sequencingLevel = numOfLevels; - -levels = max(tops(:)); - -for level = 1:levels - %check if slab is new - if matRad_siochiDifferentSlab(tops,bases,level) - k = k+1; %increment number of unique slabs - shape_k = (bases <= level).*(level <= tops); %shape of current slab - shapes(:,:,k) = shape_k; - end - shapesWeight(k) = shapesWeight(k)+1; %if slab is not unique, this increments weight again - - if visBool - %show the leaf positions - [dimZ,dimX] = size(tops); - seqSubPlots(4) = subplot(2,2,3.5,'parent',seqFig); - imagesc(shape_k,'parent',seqSubPlots(4)); - hold(seqSubPlots(4),'on'); - set(seqSubPlots(4),'YDir','normal') - xlabel(seqSubPlots(4),'x - direction parallel to leaf motion ') - ylabel(seqSubPlots(4),'z - direction perpendicular to leaf motion ') - title(seqSubPlots(4),['beam # ' num2str(i) ' shape # ' num2str(k) ' d_k = ' num2str(shapesWeight(k))]); - for j = 1:dimZ - leftLeafIx = find(shape_k(j,:)>0,1,'first'); - rightLeafIx = find(shape_k(j,:)>0,1,'last'); - if leftLeafIx > 1 - plot(seqSubPlots(4),[.5 leftLeafIx-.5],j-[.5 .5] ,'w','LineWidth',2) - plot(seqSubPlots(4),[.5 leftLeafIx-.5],j+[.5 .5] ,'w','LineWidth',2) - plot(seqSubPlots(4),[ leftLeafIx-.5 leftLeafIx-.5],j+[.5 -.5] ,'w','LineWidth',2) - end - if rightLeafIx 0 - - k = k + 1; - - %Plot residual intensity matrix. - if visBool - seqSubPlots(2) = subplot(2,2,2,'parent',seqFig); - imagesc(D_k,'parent',seqSubPlots(2)); - set(seqSubPlots(2),'CLim',[0 L_0],'YDir','normal'); - title(seqSubPlots(2),['k = ' num2str(k) ' - ' num2str(numel(unique(D_k))) ' intensity levels remaining...']); - xlabel(seqSubPlots(2),'x - direction parallel to leaf motion '); - ylabel(seqSubPlots(2),'z - direction perpendicular to leaf motion '); - colorbar - drawnow - end - - %Rounded off integer. Equation 7. - m = floor(log2(L_k)); - - % Convert m=1 if is less than 1. This happens when L_k belong to ]0,2[ - if m < 1 - m = 1; - end - - %Calculate the delivery intensity unit. Equation 6. - d_k = floor(2^(m-1)); - - % Opening matrix. - openingMx = D_k >= d_k; - - % Plot opening matrix. - if visBool - seqSubPlots(3) = subplot(2,2,3,'parent',seqFig); - imagesc(openingMx,'parent',seqSubPlots(3)); - set(seqSubPlots(3),'YDir','normal') - xlabel(seqSubPlots(3),'x - direction parallel to leaf motion ') - ylabel(seqSubPlots(3),'z - direction perpendicular to leaf motion ') - title(seqSubPlots(3),'Opening matrix'); - drawnow - end - - if strcmp(mode,'sw') % sliding window technique! - for j = 1:dimOfFluenceMxZ - openIx = find(openingMx(j,:) == 1,1,'first'); - if ~isempty(openIx) - closeIx = find(openingMx(j,openIx+1:end) == 0,1,'first'); - if ~isempty(closeIx) - openingMx(j,openIx+closeIx:end) = 0; - end - end - - end - elseif strcmp(mode,'rl') % reducing levels technique! - for j = 1:dimOfFluenceMxZ - [maxVal,maxIx] = max(openingMx(j,:) .* D_k(j,:)); - if maxVal > 0 - closeIx = maxIx + find(openingMx(j,maxIx+1:end) == 0,1,'first'); - if ~isempty(closeIx) - openingMx(j,closeIx:end) = 0; - end - openIx = find(openingMx(j,1:maxIx-1) == 0,1,'last'); - if ~isempty(openIx) - openingMx(j,1:openIx) = 0; - end - end - - end - - end - - shape_k = openingMx * d_k; - - if visBool - seqSubPlots(4) = subplot(2,2,4,'parent',seqFig); - imagesc(shape_k,'parent',seqSubPlots(4)); - set(seqSubPlots(4),'YDir','normal') - hold(seqSubPlots(4),'on'); - xlabel(seqSubPlots(4),'x - direction parallel to leaf motion ') - ylabel(seqSubPlots(4),'z - direction perpendicular to leaf motion ') - title(seqSubPlots(4),['beam # ' num2str(i) ' shape # ' num2str(k) ' d_k = ' num2str(d_k)]); - for j = 1:dimOfFluenceMxZ - leftLeafIx = find(shape_k(j,:)>0,1,'first'); - rightLeafIx = find(shape_k(j,:)>0,1,'last'); - if leftLeafIx > 1 - plot(seqSubPlots(4),[.5 leftLeafIx-.5],j-[.5 .5] ,'w','LineWidth',2) - plot(seqSubPlots(4),[.5 leftLeafIx-.5],j+[.5 .5] ,'w','LineWidth',2) - plot(seqSubPlots(4),[ leftLeafIx-.5 leftLeafIx-.5],j+[.5 -.5] ,'w','LineWidth',2) - end - if rightLeafIx0 - - 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; - - 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); - end - - - sequencing.w(1+offset:numOfRaysPerBeam+offset,1) = D_0(indInFluenceMx)/numOfLevels*calFac; - - offset = offset + numOfRaysPerBeam; - -end - -resultGUI.w = sequencing.w; -resultGUI.wSequenced = sequencing.w; - -resultGUI.sequencing = sequencing; -resultGUI.apertureInfo = matRad_sequencing2ApertureInfo(sequencing,stf); +matRad_cfg.dispWarning('This function is Outdated use new SequencingClass'); -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, ... - doseSequencedDoseGrid, ... - dij.ctGrid.x,dij.ctGrid.y',dij.ctGrid.z); +sequencer = matRad_SequencingPhotonsXiaLeaf(); +sequencer.visMode = visBool; +sequencer.sequencingLevel = numOfLevels; -% if weights exists from an former DAO remove it -if isfield(resultGUI,'wDao') - resultGUI = rmfield(resultGUI,'wDao'); +sequence = sequencer.sequence(resultGUI.w, stf); +if ~isempty(dij) + resultGUI = matRad_calcCubes(sequence.w, dij); +else + matRad_cfg.dispWarning('Dose not recalcaulted with sequenced fluence'); end +resultGUI.sequencing = sequence; end - From 5b6e8a86811f53baf0e5f4a854df33bdb39fdd48 Mon Sep 17 00:00:00 2001 From: JenHardt <84377305+JenHardt@users.noreply.github.com> Date: Fri, 20 Mar 2026 21:13:17 +0100 Subject: [PATCH 20/20] octave6fix --- test/sequencing/test_engelLeafSequencing.m | 146 +++++++++---------- test/sequencing/test_siochiLeafSequencing.m | 147 +++++++++----------- test/sequencing/test_xiaLeafSequencing.m | 119 +++++++--------- 3 files changed, 190 insertions(+), 222 deletions(-) diff --git a/test/sequencing/test_engelLeafSequencing.m b/test/sequencing/test_engelLeafSequencing.m index 0902e10d0..a46cbcf40 100644 --- a/test/sequencing/test_engelLeafSequencing.m +++ b/test/sequencing/test_engelLeafSequencing.m @@ -1,79 +1,69 @@ function test_suite = test_engelLeafSequencing - %The output should always be test_suite, and the function name the same as - %your file name - - %To collect all tests defined below, this is needed in newer Matlab - %versions. test_functions will collect function handles to below test - %functions - test_functions=localfunctions(); - - % This will initialize the test suite, i.e., take the functions from - % test_functions, check if they contain "test", convert them into a MOxUnit - % Test Case, and add them to the test-runner - initTestSuite; - - function [resultGUI,stf,dij,pln] = helper_getTestData() - p = load('photons_testData.mat'); - pln = p.pln; - pln.propSeq.sequencer = 'engel'; - resultGUI = p.resultGUI; - stf = p.stf; - dij = p.dij; - - - function test_run_sequencing_basic - [resultGUI,stf,dij,pln] = helper_getTestData(); - fn_old = fieldnames(resultGUI); - - numOfLevels = [1,10]; - - for levels = numOfLevels - resultGUI_sequenced = matRad_sequencing(resultGUI,stf,pln); - - fn_new = fieldnames(resultGUI_sequenced); - for i = 1:numel(fn_old) - assertTrue(any(strcmp(fn_old{i},fn_new))); - assertElementsAlmostEqual(resultGUI.(fn_old{i}),resultGUI_sequenced.(fn_old{i})); - end - - % Basic additions to resultGUI - assertTrue(isstruct(resultGUI_sequenced.sequencing.apertureInfo)); - assertTrue(isstruct(resultGUI_sequenced.sequencing)); - - %Sequencing Struct - seq = resultGUI_sequenced.sequencing; - assertTrue(isstruct(seq.beam)); - assertTrue(numel(seq.beam) == numel(stf)); - for i = 1:size(seq.beam,2) - assertTrue(isscalar(seq.beam(i).numOfShapes)); - assertTrue(isnumeric(seq.beam(i).shapes)); - assertEqual(size(seq.beam(i).shapes,3),seq.beam(i).numOfShapes); - assertTrue(isvector(seq.beam(i).shapesWeight)); - assertTrue(isvector(seq.beam(i).bixelIx)); - assertTrue(ismatrix(seq.beam(i).fluence)); - assertEqual(size(seq.beam(i).fluence),size(seq.beam(i).shapes,[1,2])); - end - - %ApertureInfo Sturct - apInfo = resultGUI_sequenced.sequencing.apertureInfo; - assertTrue(isscalar(apInfo.bixelWidth)); - assertTrue(isscalar(apInfo.numOfMLCLeafPairs)); - assertTrue(isscalar(apInfo.totalNumOfBixels)); - assertTrue(isscalar(apInfo.totalNumOfShapes)); - assertTrue(isscalar(apInfo.totalNumOfLeafPairs)); - assertTrue(isvector(apInfo.apertureVector)); - assertTrue(ismatrix(apInfo.mappingMx)); - assertTrue(ismatrix(apInfo.limMx)); - assertTrue(isstruct(apInfo.beam)) - assertTrue(numel(apInfo.beam) == numel(stf)); - end - - - - - - - - - - \ No newline at end of file +% The output should always be test_suite, and the function name the same as +% your file name + +% To collect all tests defined below, this is needed in newer Matlab +% versions. test_functions will collect function handles to below test +% functions +test_functions = localfunctions(); + +% This will initialize the test suite, i.e., take the functions from +% test_functions, check if they contain "test", convert them into a MOxUnit +% Test Case, and add them to the test-runner +initTestSuite; + +function [resultGUI, stf, dij, pln] = helper_getTestData() +p = load('photons_testData.mat'); +pln = p.pln; +pln.propSeq.sequencer = 'engel'; +resultGUI = p.resultGUI; +stf = p.stf; +dij = p.dij; + +function test_run_sequencing_basic +[resultGUI, stf, dij, pln] = helper_getTestData(); +fn_old = fieldnames(resultGUI); + +numOfLevels = [1, 10]; + +for levels = numOfLevels + resultGUI_sequenced = matRad_sequencing(resultGUI, stf, pln); + + fn_new = fieldnames(resultGUI_sequenced); + for i = 1:numel(fn_old) + assertTrue(any(strcmp(fn_old{i}, fn_new))); + assertElementsAlmostEqual(resultGUI.(fn_old{i}), resultGUI_sequenced.(fn_old{i})); + end + + % Basic additions to resultGUI + assertTrue(isstruct(resultGUI_sequenced.sequencing.apertureInfo)); + assertTrue(isstruct(resultGUI_sequenced.sequencing)); + + % Sequencing Struct + seq = resultGUI_sequenced.sequencing; + assertTrue(isstruct(seq.beam)); + assertTrue(numel(seq.beam) == numel(stf)); + for i = 1:size(seq.beam, 2) + assertTrue(isscalar(seq.beam(i).numOfShapes)); + assertTrue(isnumeric(seq.beam(i).shapes)); + assertEqual(size(seq.beam(i).shapes, 3), seq.beam(i).numOfShapes); + assertTrue(isvector(seq.beam(i).shapesWeight)); + assertTrue(isvector(seq.beam(i).bixelIx)); + assertTrue(ismatrix(seq.beam(i).fluence)); + assertEqual(size(seq.beam(i).fluence, 1), size(seq.beam(i).shapes, 1)); + assertEqual(size(seq.beam(i).fluence, 2), size(seq.beam(i).shapes, 2)); + end + + % ApertureInfo Sturct + apInfo = resultGUI_sequenced.sequencing.apertureInfo; + assertTrue(isscalar(apInfo.bixelWidth)); + assertTrue(isscalar(apInfo.numOfMLCLeafPairs)); + assertTrue(isscalar(apInfo.totalNumOfBixels)); + assertTrue(isscalar(apInfo.totalNumOfShapes)); + assertTrue(isscalar(apInfo.totalNumOfLeafPairs)); + assertTrue(isvector(apInfo.apertureVector)); + assertTrue(ismatrix(apInfo.mappingMx)); + assertTrue(ismatrix(apInfo.limMx)); + assertTrue(isstruct(apInfo.beam)); + assertTrue(numel(apInfo.beam) == numel(stf)); +end diff --git a/test/sequencing/test_siochiLeafSequencing.m b/test/sequencing/test_siochiLeafSequencing.m index f6431ba5a..88f847357 100644 --- a/test/sequencing/test_siochiLeafSequencing.m +++ b/test/sequencing/test_siochiLeafSequencing.m @@ -1,80 +1,69 @@ function test_suite = test_xiaLeafSequencing - %The output should always be test_suite, and the function name the same as - %your file name - - %To collect all tests defined below, this is needed in newer Matlab - %versions. test_functions will collect function handles to below test - %functions - test_functions=localfunctions(); - - % This will initialize the test suite, i.e., take the functions from - % test_functions, check if they contain "test", convert them into a MOxUnit - % Test Case, and add them to the test-runner - initTestSuite; - -function [resultGUI,stf,dij,pln] = helper_getTestData() - p = load('photons_testData.mat'); - pln = p.pln; - pln.propSeq.sequencer = 'siochi'; - resultGUI = p.resultGUI; - stf = p.stf; - dij = p.dij; - - - function test_run_sequencing_basic - [resultGUI,stf,dij,pln] = helper_getTestData(); - fn_old = fieldnames(resultGUI); - - numOfLevels = [1,10]; - - for levels = numOfLevels - resultGUI_sequenced = matRad_sequencing(resultGUI,stf,pln); - - fn_new = fieldnames(resultGUI_sequenced); - for i = 1:numel(fn_old) - assertTrue(any(strcmp(fn_old{i},fn_new))); - assertElementsAlmostEqual(resultGUI.(fn_old{i}),resultGUI_sequenced.(fn_old{i})); - end - - % Basic additions to resultGUI - assertTrue(isstruct(resultGUI_sequenced.sequencing.apertureInfo)); - assertTrue(isstruct(resultGUI_sequenced.sequencing)); - - %Sequencing Struct - seq = resultGUI_sequenced.sequencing; - assertTrue(isstruct(seq.beam)); - assertTrue(numel(seq.beam) == numel(stf)); - for i = 1:size(seq.beam,2) - assertTrue(isscalar(seq.beam(i).numOfShapes)); - assertTrue(isnumeric(seq.beam(i).shapes)); - assertEqual(size(seq.beam(i).shapes,3),seq.beam(i).numOfShapes); - assertTrue(isvector(seq.beam(i).shapesWeight)); - assertTrue(isvector(seq.beam(i).bixelIx)); - assertTrue(ismatrix(seq.beam(i).fluence)); - assertEqual(size(seq.beam(i).fluence),size(seq.beam(i).shapes,[1,2])); - end - - %ApertureInfo Sturct - apInfo = resultGUI_sequenced.sequencing.apertureInfo; - assertTrue(isscalar(apInfo.bixelWidth)); - assertTrue(isscalar(apInfo.numOfMLCLeafPairs)); - assertTrue(isscalar(apInfo.totalNumOfBixels)); - assertTrue(isscalar(apInfo.totalNumOfShapes)); - assertTrue(isscalar(apInfo.totalNumOfLeafPairs)); - assertTrue(isvector(apInfo.apertureVector)); - assertTrue(ismatrix(apInfo.mappingMx)); - assertTrue(ismatrix(apInfo.limMx)); - assertTrue(isstruct(apInfo.beam)) - assertTrue(numel(apInfo.beam) == numel(stf)); - end - - - - - - - - - - - \ No newline at end of file +% The output should always be test_suite, and the function name the same as +% your file name + +% To collect all tests defined below, this is needed in newer Matlab +% versions. test_functions will collect function handles to below test +% functions +test_functions = localfunctions(); + +% This will initialize the test suite, i.e., take the functions from +% test_functions, check if they contain "test", convert them into a MOxUnit +% Test Case, and add them to the test-runner +initTestSuite; + +function [resultGUI, stf, dij, pln] = helper_getTestData() +p = load('photons_testData.mat'); +pln = p.pln; +pln.propSeq.sequencer = 'siochi'; +resultGUI = p.resultGUI; +stf = p.stf; +dij = p.dij; + +function test_run_sequencing_basic +[resultGUI, stf, dij, pln] = helper_getTestData(); +fn_old = fieldnames(resultGUI); + +numOfLevels = [1, 10]; + +for levels = numOfLevels + resultGUI_sequenced = matRad_sequencing(resultGUI, stf, pln); + + fn_new = fieldnames(resultGUI_sequenced); + for i = 1:numel(fn_old) + assertTrue(any(strcmp(fn_old{i}, fn_new))); + assertElementsAlmostEqual(resultGUI.(fn_old{i}), resultGUI_sequenced.(fn_old{i})); + end + + % Basic additions to resultGUI + assertTrue(isstruct(resultGUI_sequenced.sequencing.apertureInfo)); + assertTrue(isstruct(resultGUI_sequenced.sequencing)); + + % Sequencing Struct + seq = resultGUI_sequenced.sequencing; + assertTrue(isstruct(seq.beam)); + assertTrue(numel(seq.beam) == numel(stf)); + for i = 1:size(seq.beam, 2) + assertTrue(isscalar(seq.beam(i).numOfShapes)); + assertTrue(isnumeric(seq.beam(i).shapes)); + assertEqual(size(seq.beam(i).shapes, 3), seq.beam(i).numOfShapes); + assertTrue(isvector(seq.beam(i).shapesWeight)); + assertTrue(isvector(seq.beam(i).bixelIx)); + assertTrue(ismatrix(seq.beam(i).fluence)); + assertEqual(size(seq.beam(i).fluence, 1), size(seq.beam(i).shapes, 1)); + assertEqual(size(seq.beam(i).fluence, 2), size(seq.beam(i).shapes, 2)); + end + + % ApertureInfo Sturct + apInfo = resultGUI_sequenced.sequencing.apertureInfo; + assertTrue(isscalar(apInfo.bixelWidth)); + assertTrue(isscalar(apInfo.numOfMLCLeafPairs)); + assertTrue(isscalar(apInfo.totalNumOfBixels)); + assertTrue(isscalar(apInfo.totalNumOfShapes)); + assertTrue(isscalar(apInfo.totalNumOfLeafPairs)); + assertTrue(isvector(apInfo.apertureVector)); + assertTrue(ismatrix(apInfo.mappingMx)); + assertTrue(ismatrix(apInfo.limMx)); + assertTrue(isstruct(apInfo.beam)); + assertTrue(numel(apInfo.beam) == numel(stf)); +end diff --git a/test/sequencing/test_xiaLeafSequencing.m b/test/sequencing/test_xiaLeafSequencing.m index e4b146fd8..b00d0f3a9 100644 --- a/test/sequencing/test_xiaLeafSequencing.m +++ b/test/sequencing/test_xiaLeafSequencing.m @@ -1,80 +1,69 @@ function test_suite = test_xiaLeafSequencing -%The output should always be test_suite, and the function name the same as -%your file name - -%To collect all tests defined below, this is needed in newer Matlab -%versions. test_functions will collect function handles to below test -%functions -test_functions=localfunctions(); +% The output should always be test_suite, and the function name the same as +% your file name + +% To collect all tests defined below, this is needed in newer Matlab +% versions. test_functions will collect function handles to below test +% functions +test_functions = localfunctions(); % This will initialize the test suite, i.e., take the functions from % test_functions, check if they contain "test", convert them into a MOxUnit % Test Case, and add them to the test-runner initTestSuite; -function [resultGUI,stf,dij,pln] = helper_getTestData() - p = load('photons_testData.mat'); - pln = p.pln; - pln.propSeq.sequencer = 'xia'; - resultGUI = p.resultGUI; - stf = p.stf; - dij = p.dij; - +function [resultGUI, stf, dij, pln] = helper_getTestData() +p = load('photons_testData.mat'); +pln = p.pln; +pln.propSeq.sequencer = 'xia'; +resultGUI = p.resultGUI; +stf = p.stf; +dij = p.dij; function test_run_sequencing_basic - [resultGUI,stf,dij,pln] = helper_getTestData(); - fn_old = fieldnames(resultGUI); - - numOfLevels = [1,10]; - - for levels = numOfLevels - resultGUI_sequenced = matRad_sequencing(resultGUI,stf,pln); - - fn_new = fieldnames(resultGUI_sequenced); - for i = 1:numel(fn_old) - assertTrue(any(strcmp(fn_old{i},fn_new))); - assertElementsAlmostEqual(resultGUI.(fn_old{i}),resultGUI_sequenced.(fn_old{i})); - end - - % Basic additions to resultGUI - assertTrue(isstruct(resultGUI_sequenced.sequencing.apertureInfo)); - assertTrue(isstruct(resultGUI_sequenced.sequencing)); - - %Sequencing Struct - seq = resultGUI_sequenced.sequencing; - assertTrue(isstruct(seq.beam)); - assertTrue(numel(seq.beam) == numel(stf)); - for i = 1:size(seq.beam,2) - assertTrue(isscalar(seq.beam(i).numOfShapes)); - assertTrue(isnumeric(seq.beam(i).shapes)); - assertEqual(size(seq.beam(i).shapes,3),seq.beam(i).numOfShapes); - assertTrue(isvector(seq.beam(i).shapesWeight)); - assertTrue(isvector(seq.beam(i).bixelIx)); - assertTrue(ismatrix(seq.beam(i).fluence)); - assertEqual(size(seq.beam(i).fluence),size(seq.beam(i).shapes,[1,2])); - end - - %ApertureInfo Sturct - apInfo = resultGUI_sequenced.sequencing.apertureInfo; - assertTrue(isscalar(apInfo.bixelWidth)); - assertTrue(isscalar(apInfo.numOfMLCLeafPairs)); - assertTrue(isscalar(apInfo.totalNumOfBixels)); - assertTrue(isscalar(apInfo.totalNumOfShapes)); - assertTrue(isscalar(apInfo.totalNumOfLeafPairs)); - assertTrue(isvector(apInfo.apertureVector)); - assertTrue(ismatrix(apInfo.mappingMx)); - assertTrue(ismatrix(apInfo.limMx)); - assertTrue(isstruct(apInfo.beam)) - assertTrue(numel(apInfo.beam) == numel(stf)); - end +[resultGUI, stf, dij, pln] = helper_getTestData(); +fn_old = fieldnames(resultGUI); +numOfLevels = [1, 10]; - - - - +for levels = numOfLevels + resultGUI_sequenced = matRad_sequencing(resultGUI, stf, pln); + fn_new = fieldnames(resultGUI_sequenced); + for i = 1:numel(fn_old) + assertTrue(any(strcmp(fn_old{i}, fn_new))); + assertElementsAlmostEqual(resultGUI.(fn_old{i}), resultGUI_sequenced.(fn_old{i})); + end + % Basic additions to resultGUI + assertTrue(isstruct(resultGUI_sequenced.sequencing.apertureInfo)); + assertTrue(isstruct(resultGUI_sequenced.sequencing)); + % Sequencing Struct + seq = resultGUI_sequenced.sequencing; + assertTrue(isstruct(seq.beam)); + assertTrue(numel(seq.beam) == numel(stf)); + for i = 1:size(seq.beam, 2) + assertTrue(isscalar(seq.beam(i).numOfShapes)); + assertTrue(isnumeric(seq.beam(i).shapes)); + assertEqual(size(seq.beam(i).shapes, 3), seq.beam(i).numOfShapes); + assertTrue(isvector(seq.beam(i).shapesWeight)); + assertTrue(isvector(seq.beam(i).bixelIx)); + assertTrue(ismatrix(seq.beam(i).fluence)); + assertEqual(size(seq.beam(i).fluence, 1), size(seq.beam(i).shapes, 1)); + assertEqual(size(seq.beam(i).fluence, 2), size(seq.beam(i).shapes, 2)); + end - \ No newline at end of file + % ApertureInfo Sturct + apInfo = resultGUI_sequenced.sequencing.apertureInfo; + assertTrue(isscalar(apInfo.bixelWidth)); + assertTrue(isscalar(apInfo.numOfMLCLeafPairs)); + assertTrue(isscalar(apInfo.totalNumOfBixels)); + assertTrue(isscalar(apInfo.totalNumOfShapes)); + assertTrue(isscalar(apInfo.totalNumOfLeafPairs)); + assertTrue(isvector(apInfo.apertureVector)); + assertTrue(ismatrix(apInfo.mappingMx)); + assertTrue(ismatrix(apInfo.limMx)); + assertTrue(isstruct(apInfo.beam)); + assertTrue(numel(apInfo.beam) == numel(stf)); +end