diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..46879b073 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,13 @@ +# Auto-detect text files and normalize (LF in repo). +* text=auto + +# Force files to be binary +*.mat binary +*.pdf binary +*.mex* binary +*.zip binary +*.dll binary +*.exe binary + +# Ensure .m files are never marked as executable +*.m -x \ No newline at end of file diff --git a/.github/actions/test-matlab/action.yml b/.github/actions/test-matlab/action.yml index 4f38361b8..ff541189b 100644 --- a/.github/actions/test-matlab/action.yml +++ b/.github/actions/test-matlab/action.yml @@ -15,7 +15,7 @@ runs: uses: matlab-actions/setup-matlab@v2 with: release: ${{ inputs.matlab-version }} - products: Image_Processing_Toolbox Parallel_Computing_Toolbox Optimization_Toolbox + products: Image_Processing_Toolbox Parallel_Computing_Toolbox Optimization_Toolbox Global_Optimization_Toolbox # Runs test command - name: Run Tests diff --git a/.github/workflows/coverage-report.yml b/.github/workflows/coverage-report.yml index ab9b0847c..08c06b51d 100644 --- a/.github/workflows/coverage-report.yml +++ b/.github/workflows/coverage-report.yml @@ -64,6 +64,7 @@ jobs: - name: Add Coverage PR Comment uses: marocchino/sticky-pull-request-comment@v2 + continue-on-error: true if: github.event_name == 'pull_request' with: recreate: true diff --git a/.gitignore b/.gitignore index cd0e481c8..1e8d53e0a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ coverage.xml coverage.json *.asv build/ +.DS_Store diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 000000000..5b7b37775 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,51 @@ +variables: + GIT_SUBMODULE_STRATEGY: recursive + GIT_SUBMODULE_DEPTH: 1 + GIT_SUBMODULE_PATH: submodules/MOxUnit submodules/MOcov + +.matlab_defaults: + image: + name: '${CUSTOM_MATLAB_IMAGE}:r2024b' # Replace the value with the name of the MATLAB container image you want to use + entrypoint: [""] + variables: + MLM_LICENSE_FILE: $E040_MATLAB_LICENSE_SERVER # Replace the value with the port number and DNS address for your network license manager + resource_group: matlab + +test: + stage: test + extends: .matlab_defaults + script: matlab -batch "assert(matRad_runTests('test',true));" + artifacts: + reports: + junit: "./testresults.xml" + coverage_report: + coverage_format: cobertura + path: "./coverage.xml" + paths: + - "./*.xml" + - "./*.json" + coverage: '/TOTAL.*? (100(?:\.0+)?%|[1-9]?\d(?:\.\d+)?%)/' + + +package: + stage: deploy + extends: .matlab_defaults + script: + - | + if [ -n "$CI_COMMIT_TAG" ]; then + matlab -batch "matRad_buildStandalone('isRelease', true);" + PACKAGE_NAME="$CI_COMMIT_TAG" + else + matlab -batch "matRad_buildStandalone();" + PACKAGE_NAME="$CI_COMMIT_BRANCH" + fi + INSTALLER_NAME="matRad_installerLinux64_$PACKAGE_NAME" + - tar -czvf $INSTALLER_NAME.tar.gz build/installer + - 'curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file $INSTALLER_NAME.tar.gz "$CI_API_V4_URL/projects/$CI_PROJECT_ID/packages/generic/matRad_linux64/$CI_COMMIT_BRANCH/matRad/$PACKAGE_NAME/$INSTALLER_NAME.tar.gz"' + # - echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY -u $CI_REGISTRY_USER --password-stdin + # - docker image tag matRad:develop $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/matRad:develop + # - docker push $CI_REGISTRY/$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME/matRad:develop + rules: + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH + - if: $CI_COMMIT_BRANCH == "dev" + - if: $CI_COMMIT_TAG diff --git a/AUTHORS.txt b/AUTHORS.txt index 8d2dc3189..3ae8c88d6 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -1,4 +1,4 @@ -List of all matRad developers that contributed code (alphabetical) +List of all matRad developers that contributed code (alphabetical) * Nelly Abbani * Nabe Al-Hasnawi @@ -11,8 +11,10 @@ * Louis Charton * Eric Christiansen * Remo Cristoforetti +* Fabio D'Andrea * Marios Dallas * Edgardo Doerner +* Louis Ermeneux * Simona Facchiano * Hubert Gabrys * Josefine Handrack @@ -26,6 +28,7 @@ * Navid Khaledi * Thomas Klinge * Jeremias Kunz +* Mariia Lapaeva * Paul Anton Meder * Henning Mescher * Lucas-Raphael Müller @@ -36,6 +39,7 @@ * Claus Sarnighausen * Carsten Scholz * Camilo Sevilla +* Mateusz Sitarz * Alexander Stadler * Uwe Titt * Niklas Wahl diff --git a/CHANGELOG.md b/CHANGELOG.md index fd1ac42ab..069ee9629 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,37 @@ # Changelog +## Minor Update 3.2.0 + +### New Features +- FRED MC interface (if installed) +- VHEE planning with a Generic (unfocused) beam and a focused beam. The Generic beam can be forwarded to TOPAS as well. +- New matRad_plotSlice function with keyword / value synxtax for more intuitive plotting of slices + +### Bug Fixes & Performance +- DICOM Import widget allow selection of multiple RTDose files. +- DICOM Import Widget and importer handle selected patient more consistently and robustly. +- DICOM Exporter writes quantities beyond dose, importer tries to import them correctly. +- DICOM Exporter now always writes ReferencedRTPlanSequence. Importer can now survive without it. +- DVH widget does not throw a warning in updates, handle scenarios correctly / more robustly and missing xlabel axesHandle parameter. +- GUI fixes regarding setting of gantry angles and other parameters in the PlanningWidget +- EXTERNAL contours now correctly recognized +- performance improvement for obtaining jacobian structure in optimization +- Available Classes (e.g., dose engines) are now cached for faster loading + +### User Experience +- Added new examples for usage of FRED & VHEE and a workflow example for comparing dose calculation on synthetic CT to planning CT +- Updated examples to use matRad_plotSlice +- GUI fixes for use in Matlab Online +- The analyitcal functions from the Bortfeld Bragg Peak Model are now public and can be used to compute standard approximations (e.g. range-energy relationship) + +### Development and CI +- Added a new `.gitlab-ci.yml` file to support GitLab CI/CD, including test and package stages, artifact handling, and configuration for MATLAB container images and licensing. +- Added a `.gitattributes` file to standardize line endings, treat certain file types as binary, and ensure `.m` files are not marked as executable. +- In `.github/actions/test-matlab/action.yml`, added `Global_Optimization_Toolbox` to the list of MATLAB products for testing. +- In `.github/workflows/coverage-report.yml`, made the coverage PR comment step tolerant to errors to avoid workflow failures. +- More comprehensive dose calculation tests +- Added new contributors + ## Version 3.1.0 - "Cleve" ### Major Changes and New Features diff --git a/CITATION.cff b/CITATION.cff index 0019001d3..c0a967fc2 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -25,6 +25,8 @@ authors: given-names: "Eric" - family-names: "Cristoforetti" given-names: "Remo" +- family-names: "D'Andrea" + given-names: "Fabio" - family-names: "Dallas" given-names: "Marios" - family-names: "Doerner" @@ -33,6 +35,8 @@ authors: given-names: "Swantje" - family-names: "Ellerbrock" given-names: "Malte" +- family-names: "Ermeneux" + given-names: "Louis" - family-names: "Facchiano" given-names: "Simona" - family-names: "Gabryś" @@ -61,6 +65,8 @@ authors: given-names: "Thomas" - family-names: "Kunz" given-names: "Jeremias" +- family-names: "Lapaeva" + given-names: "Mariia" - family-names: "Mairani" given-names: "Andrea" - family-names: "Meder" @@ -85,6 +91,8 @@ authors: given-names: "Carsten" - family-names: "Sevilla" given-names: "Camilo" +- family-names: "Sitarz" + given-names: "Mateusz" - family-names: "Stadler" given-names: "Alexander" - family-names: "Ulrich" diff --git a/examples/matRad_example10_4DphotonRobust.m b/examples/matRad_example10_4DphotonRobust.m index 363a3797a..479622758 100644 --- a/examples/matRad_example10_4DphotonRobust.m +++ b/examples/matRad_example10_4DphotonRobust.m @@ -192,7 +192,7 @@ stf = matRad_generateStf(ct,cst,pln); %% Dose Calculation -dij = matRad_calcPhotonDose(ct,stf,pln,cst); +dij = matRad_calcDoseInfluence(ct,cst,stf,pln); %% Inverse Optimization for IMPT based on RBE-weighted dose % The goal of the fluence optimization is to find a set of bixel/spot @@ -241,30 +241,38 @@ maxDose = max([max(resultGUI.([pln.propOpt.quantityOpt])(:,:,slice)) max(resultGUIrobust.([pln.propOpt.quantityOpt])(:,:,slice))])+1e-4; doseIsoLevels = linspace(0.1 * maxDose,maxDose,10); figure, -subplot(121),matRad_plotSliceWrapper(gca,ct,cst,1,resultGUI.([pln.propOpt.quantityOpt '_' 'beam1']) ,plane,slice,[],[],colorcube,[],[0 maxDose],doseIsoLevels);title('conventional plan - beam1') -subplot(122),matRad_plotSliceWrapper(gca,ct,cst,1,resultGUI.([pln.propOpt.quantityOpt]) ,plane,slice,[],[],colorcube,[],[0 maxDose],doseIsoLevels);title('conventional plan') +subplot(121),matRad_plotSlice(ct, 'axesHandle', gca, 'cst', cst, 'cubeIdx', 1, 'dose', resultGUI.([pln.propOpt.quantityOpt '_' 'beam1']) ,'plane', plane, 'slice', slice, 'contourColorMap', colorcube, 'doseWindow', [0 maxDose], 'doseIsoLevels', doseIsoLevels);title('conventional plan - beam1') +subplot(122),matRad_plotSlice(ct, 'axesHandle', gca, 'cst', cst, 'cubeIdx', 1, 'dose', resultGUI.([pln.propOpt.quantityOpt]) ,'plane', plane, 'slice', slice, 'contourColorMap', colorcube, 'doseWindow', [0 maxDose], 'doseIsoLevels', doseIsoLevels);title('conventional plan') +%subplot(121),matRad_plotSliceWrapper(gca,ct,cst,1,resultGUI.([pln.propOpt.quantityOpt '_' 'beam1']) ,plane,slice,[],[],colorcube,[],[0 maxDose],doseIsoLevels);title('conventional plan - beam1') +%subplot(122),matRad_plotSliceWrapper(gca,ct,cst,1,resultGUI.([pln.propOpt.quantityOpt]) ,plane,slice,[],[],colorcube,[],[0 maxDose],doseIsoLevels);title('conventional plan') figure -subplot(121),matRad_plotSliceWrapper(gca,ct,cst,1,resultGUIrobust.([pln.propOpt.quantityOpt '_' 'beam1']),plane,slice,[],[],colorcube,[],[0 maxDose],doseIsoLevels);title('robust plan - beam1') -subplot(122),matRad_plotSliceWrapper(gca,ct,cst,1,resultGUIrobust.([pln.propOpt.quantityOpt]),plane,slice,[],[],colorcube,[],[0 maxDose],doseIsoLevels);title('robust plan') +subplot(121),matRad_plotSlice(ct, 'axesHandle', gca, 'cst', cst, 'cubeIdx', 1, 'dose', resultGUIrobust.([pln.propOpt.quantityOpt '_' 'beam1']) ,'plane', plane, 'slice', slice, 'contourColorMap', colorcube, 'doseWindow', [0 maxDose], 'doseIsoLevels', doseIsoLevels);title('robust plan - beam1') +subplot(122),matRad_plotSlice(ct, 'axesHandle', gca, 'cst', cst, 'cubeIdx', 1, 'dose', resultGUIrobust.([pln.propOpt.quantityOpt]) ,'plane', plane, 'slice', slice, 'contourColorMap', colorcube, 'doseWindow', [0 maxDose], 'doseIsoLevels', doseIsoLevels);title('robust plan') +%subplot(121),matRad_plotSliceWrapper(gca,ct,cst,1,resultGUIrobust.([pln.propOpt.quantityOpt '_' 'beam1']),plane,slice,[],[],colorcube,[],[0 maxDose],doseIsoLevels);title('robust plan - beam1') +%subplot(122),matRad_plotSliceWrapper(gca,ct,cst,1,resultGUIrobust.([pln.propOpt.quantityOpt]),plane,slice,[],[],colorcube,[],[0 maxDose],doseIsoLevels);title('robust plan') figure -subplot(131),matRad_plotSliceWrapper(gca,ct,cst,1,resultGUIrobust4D.([pln.propOpt.quantityOpt]),plane,slice,[],[],colorcube,[],[0 maxDose],doseIsoLevels);title('robust plan') -subplot(132),matRad_plotSliceWrapper(gca,ct,cst,1,resultGUIrobust4D.('accPhysicalDose'),plane,slice,[],[],colorcube,[],[0 maxDose],doseIsoLevels);title('robust plan dose accumulation') +subplot(131),matRad_plotSlice(ct, 'axesHandle', gca, 'cst', cst, 'cubeIdx', 1, 'dose', resultGUIrobust4D.([pln.propOpt.quantityOpt]) ,'plane', plane, 'slice', slice, 'contourColorMap', colorcube, 'doseWindow', [0 maxDose], 'doseIsoLevels', doseIsoLevels);title('robust plan') +subplot(132),matRad_plotSlice(ct, 'axesHandle', gca, 'cst', cst, 'cubeIdx', 1, 'dose', resultGUIrobust4D.('accPhysicalDose') ,'plane', plane, 'slice', slice, 'contourColorMap', colorcube, 'doseWindow', [0 maxDose], 'doseIsoLevels', doseIsoLevels);title('robust plan dose accumulation') +%subplot(131),matRad_plotSliceWrapper(gca,ct,cst,1,resultGUIrobust4D.([pln.propOpt.quantityOpt]),plane,slice,[],[],colorcube,[],[0 maxDose],doseIsoLevels);title('robust plan') +%subplot(132),matRad_plotSliceWrapper(gca,ct,cst,1,resultGUIrobust4D.('accPhysicalDose'),plane,slice,[],[],colorcube,[],[0 maxDose],doseIsoLevels);title('robust plan dose accumulation') % create an interactive plot to slide through individual scnearios f = figure; title('individual scenarios'); numScen = 1; maxDose = max(max(resultGUIrobust.([pln.propOpt.quantityOpt '_scen' num2str(round(numScen))])(:,:,slice)))+0.2; doseIsoLevels = linspace(0.1 * maxDose,maxDose,10); -matRad_plotSliceWrapper(gca,ct,cst,1,resultGUIrobust.([pln.propOpt.quantityOpt '_scen' num2str(round(numScen))]),plane,slice,[],[],colorcube,[],[0 maxDose],doseIsoLevels); +matRad_plotSlice(ct, 'axesHandle', gca, 'cst', cst, 'cubeIdx', 1, 'dose', resultGUIrobust.([pln.propOpt.quantityOpt '_scen' num2str(round(numScen))]) ,'plane', plane, 'slice', slice, 'contourColorMap', colorcube, 'doseWindow', [0 maxDose], 'doseIsoLevels', doseIsoLevels); +%matRad_plotSliceWrapper(gca,ct,cst,1,resultGUIrobust.([pln.propOpt.quantityOpt '_scen' num2str(round(numScen))]),plane,slice,[],[],colorcube,[],[0 maxDose],doseIsoLevels); [env,envver] = matRad_getEnvironment(); if strcmp(env,'MATLAB') || str2double(envver(1)) >= 5 b = uicontrol('Parent',f,'Style','slider','Position',[50,5,419,23],... 'value',numScen, 'min',1, 'max',pln.multScen.totNumScen,'SliderStep', [1/(pln.multScen.totNumScen-1) , 1/(pln.multScen.totNumScen-1)]); - set(b,'Callback',@(es,ed) matRad_plotSliceWrapper(gca,ct,cst,round(get(es,'Value')),resultGUIrobust.([pln.propOpt.quantityOpt '_scen' num2str(round(get(es,'Value')))]),plane,slice,[],[],colorcube,[],[0 maxDose],doseIsoLevels)); + set(b,'Callback',@(es,ed) matRad_plotSlice(ct, 'axesHandle', gca, 'cst', cst, 'cubeIdx', round(get(es,'Value')), 'dose', resultGUIrobust.([pln.propOpt.quantityOpt '_scen' num2str(round(get(es,'Value')))]) ,'plane', plane, 'slice', slice, 'contourColorMap', colorcube, 'doseWindow', [0 maxDose], 'doseIsoLevels', doseIsoLevels)); + %matRad_plotSliceWrapper(gca,ct,cst,round(get(es,'Value')),resultGUIrobust.([pln.propOpt.quantityOpt '_scen' num2str(round(get(es,'Value')))]),plane,slice,[],[],colorcube,[],[0 maxDose],doseIsoLevels)); end %% Indicator calculation and show DVH and QI @@ -282,10 +290,12 @@ [cstStatRob, resultGUISampRob, metaRob] = matRad_samplingAnalysis(ct,cst,plnSampRob,caSampRob, mSampDoseRob, resultGUInomScen); figure,title('std dose cube based on sampling - conventional') -matRad_plotSliceWrapper(gca,ct,cst,1,resultGUISamp.stdCube,plane,slice,[],[],colorcube,[],[0 max(resultGUISamp.stdCube(:))]); +matRad_plotSlice(ct, 'axesHandle', gca, 'cst', cst, 'cubeIdx', 1, 'dose', resultGUISamp.stdCube ,'plane', plane, 'slice', slice, 'contourColorMap', colorcube, 'doseWindow', [0 max(resultGUISamp.stdCube(:))]); +%matRad_plotSliceWrapper(gca,ct,cst,1,resultGUISamp.stdCube,plane,slice,[],[],colorcube,[],[0 max(resultGUISamp.stdCube(:))]); figure,title('std dose cube based on sampling - robust') -matRad_plotSliceWrapper(gca,ct,cst,1,resultGUISampRob.stdCube,plane,slice,[],[],colorcube,[],[0 max(resultGUISampRob.stdCube(:))]); +matRad_plotSlice(ct, 'axesHandle', gca, 'cst', cst, 'cubeIdx', 1, 'dose', resultGUISampRob.stdCube ,'plane', plane, 'slice', slice, 'contourColorMap', colorcube, 'doseWindow', [0 max(resultGUISampRob.stdCube(:))]); +%matRad_plotSliceWrapper(gca,ct,cst,1,resultGUISampRob.stdCube,plane,slice,[],[],colorcube,[],[0 max(resultGUISampRob.stdCube(:))]); diff --git a/examples/matRad_example15_brachy.m b/examples/matRad_example15_brachy.m index e27c8a5f8..1b718698c 100644 --- a/examples/matRad_example15_brachy.m +++ b/examples/matRad_example15_brachy.m @@ -84,14 +84,12 @@ % Here we will use HDR. By this means matRad will look for 'brachy_HDR.mat' % in our root directory and will use the data provided in there for % dose calculation. - pln.radiationMode = 'brachy'; pln.machine = 'HDR'; % 'LDR' or 'HDR' for brachy pln.bioModel = 'none'; pln.multScen = 'nomScen'; - %% II.1 - needle and template geometry % Now we have to set some parameters for the template and the needles. % Let's start with the needles: Seed distance is the distance between @@ -110,6 +108,7 @@ % The needles will be positioned right under the target volume pointing up. +pln.propStf.visMode = 1; %Enable visualization for stf generation pln.propStf.bixelWidth = 5; % [mm] template grid distance %Template Type @@ -151,16 +150,12 @@ % needles. % Calculation time will be reduced by one tenth when we define a dose % cutoff distance. - - pln.propDoseCalc.TG43approximation = '2D'; %'1D' or '2D' 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 simulannealbnd from the MATLAB Global Optimization Toolbox. First we % check if the simulannealbnd-Solver is available, and if it is, we set in in the @@ -172,7 +167,6 @@ pln.propOpt.optimizer = 'IPOPT'; end -pln.propOpt.optimizer = 'IPOPT'; %% II.1 - book keeping % Some field names have to be kept although they don't have a direct % relevance for brachy therapy. @@ -182,14 +176,12 @@ % Et voila! Our treatment plan structure is ready. Lets have a look: disp(pln); - %% II.2 Steering Seed Positions From STF % The steering file struct contains all needls/catheter geometry with the % target volume, number of needles, seeds and the positions of all needles % The one in the end enables visualization. stf = matRad_generateStf(ct,cst,pln); - %% II.2 - view stf % The 3D view is interesting, but we also want to know how the stf struct % looks like. diff --git a/examples/matRad_example18_FREDMC.m b/examples/matRad_example18_FREDMC.m new file mode 100644 index 000000000..3ed101830 --- /dev/null +++ b/examples/matRad_example18_FREDMC.m @@ -0,0 +1,92 @@ +%% Example: Proton Treatment Plan with FRED MC +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2023 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%% In this example we will show +% (i) how to compute a simple plan using the FRED MC engine +% (ii) how to compute LETd distributions and apply a biological model + +%% Setup the plan parameters +matRad_rc; +matRad_cfg = MatRad_Config.instance(); + +load('TG119.mat'); + +pln.radiationMode = 'protons'; +pln.machine = 'Generic'; + +pln.propDoseCalc.calcLET = 0; + +pln.numOfFractions = 30; +pln.propStf.gantryAngles = [30,330]; +pln.propStf.couchAngles = zeros(size(pln.propStf.gantryAngles)); +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] +pln.propDoseCalc.doseGrid.resolution.y = 5; % [mm] +pln.propDoseCalc.doseGrid.resolution.z = 5; % [mm] + +% Start with a simple physical dose model +pln.bioParam = matRad_bioModel(pln.radiationMode,'none'); + +pln.multScen = matRad_multScen(ct, 'nomScen'); + +stf = matRad_generateStf(ct,cst,pln); + +%% Let's start with the analytical dose calculation algorithm +pln.propDoseCalc.engine = 'HongPB'; + +% Compute the dose influence matrix +dij_PB = matRad_calcDoseInfluence(ct,cst,stf,pln); + +%% Fluence optimization +resultGUI_PB = matRad_fluenceOptimization(dij_PB,cst,pln); +wOptimized = resultGUI_PB.w; + +%% Let's now re-compute the dose distribution with FRED MC +pln.propDoseCalc.engine = 'FRED'; +pln.propDoseCalc.useGPU = false; + +% For illustraton, let's compute a dose influence matrix. +dij_FRED = matRad_calcDoseInfluence(ct,cst,stf,pln); + +% And compute the dose cube from the dij: +resultGUI_FRED = matRad_calcCubes(wOptimized,dij_FRED,1); + +% Alternative is to perform a direct dose calculation: +% resultGUI_FRED = matRad_calcDoseForward(ct,cst,stf,pln,wOptimized) + +%% Compare doses +matRad_compareDose(resultGUI_PB.physicalDose, resultGUI_FRED.physicalDose,ct,cst,[],'on'); +%% Tune the MC calculation parameters +% This example illustrates some of the parameters that can be tuned for the +% Engine. For more information run: +% help DoseEngines.matRad_ParticleFREDEngine + +pln.bioParam = matRad_bioModel(pln.radiationMode,'MCN'); + +pln.propDoseCalc.useGPU = true; +pln.propDoseCalc.sourceModel = 'gaussian'; %alternatives: {'gaussian', 'emittance', 'sigmaSqrModel'} +pln.propDoseCalc.HUtable = 'internal'; % default: matRad_default_FredMaterialConverter +pln.propDoseCalc.scorers = {'Dose', 'LETd'}; + +resultGUI_recalc = matRad_calcDoseForward(ct,cst,stf,pln, wOptimized); + +%% Compare physical dose and RBExD distributions +matRad_compareDose(resultGUI_recalc.physicalDose, resultGUI_recalc.RBExDose,ct,cst,[],'on'); \ No newline at end of file diff --git a/examples/matRad_example19_CT_sCT_DVH_difference_photons.m b/examples/matRad_example19_CT_sCT_DVH_difference_photons.m new file mode 100644 index 000000000..0502f88c2 --- /dev/null +++ b/examples/matRad_example19_CT_sCT_DVH_difference_photons.m @@ -0,0 +1,319 @@ +%% Example: Comparison of a CT and (fake) synthetic CT dose calculation +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% 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: +% (0) how to export .mat data into DICOM format to illustrate a synthetic CT data generation, +% as it is usually stored in DICOM format; +% (i) how to load DICOM patient data into matRad and modify dosimetric +% objectives and constraints; +% (ii) how to set up a photon dose calculation and optimization based on the real CT; +% (iii) how to translate the optimized plan to be applied to a synthetic CT image; and +% (iv) how to calculate the DVH differences between the plans. + +%% Preliminary step: DICOM data preparation and export from .mat format to .dcm for further use +% Start with a clean MATLAB environment. We will export Liver phantom data +% from .mat format to .dcm, consisting of 3D body volume slices as well as a structure file and use it as a real CT. +% Additionally, we will slightly modify the intensities of the phantom to generate +% synthetic CT images, illustrating dose difference. +matRad_cfg = matRad_rc; %If this throws an error, run it from the parent directory first to set the paths + +% Create directories to store DICOM real CT and synthetic (fake) CT data +patDir = [matRad_cfg.primaryUserFolder filesep 'syntheticCT' filesep]; % If you want to export your data, use "userdata" folder as it is ignored by git +name = 'LIVER'; +patDirRealCT = [name '_realCT']; +patDirFakeCT = [name '_fakeCT']; + +if ~mkdir(patDir,patDirRealCT) || ~mkdir(patDir,patDirFakeCT) + matRad_cfg.dispError('Error creating directories %s and %s in %s',patDirRealCT,patDirFakeCT,patDir); +end + +patDirRealCT = fullfile(patDir,patDirRealCT); +patDirFakeCT = fullfile(patDir,patDirFakeCT); + +% Now, as the directories are created, let us create the DICOM data. Here, the DICOM +% files will be created from the .mat format. +load('LIVER.mat'); +indicators = {'mean','std','max', 'min', 'D_2','D_5', 'D_95', 'D_98'}; +realCTct = ct; +realCTcst = cst; + +%review real CT volume +matRadGUI; +%saving as DICOMs +dcmExpRealCT = matRad_DicomExporter; % create instance of matRad_DicomExporter +dcmExpRealCT.dicomDir = patDirRealCT; % set the output path for the Dicom export +dcmExpRealCT.cst = cst; % set the structure set for the Dicom export +dcmExpRealCT.ct = realCTct; % set the image volume for the Dicom export +dcmExpRealCT.matRad_exportDicom(); + + +% In order to create a fake or synthetic CT volume, we will use real CT data and re-use its HU values +% for illustrative purposes. First, we extract the original 3D HU image data. +fakeCTct = ct; +fakeCTcubeHU = fakeCTct.cubeHU{1}; + +% Then, we define the range values to be coerced to create the fake CT volume. For this, we will +% coerce the soft tissue range and scale values in the range [0,100] to +% [100,300] and export them as .dcm files. +oldMin = 0; oldMax = 100; % Original range +newMin = 100; newMax = 300; % Target range +%scaling +mask = (fakeCTcubeHU >= oldMin) & (fakeCTcubeHU <= oldMax); +fakeCTcubeHU(mask) = newMin + (fakeCTcubeHU(mask) - oldMin) * (newMax - newMin) / (oldMax - oldMin); +fakeCTct.cubeHU{1} = fakeCTcubeHU; + +%review fake CT volume +ct = fakeCTct; +hGUI = matRadGUI; + +%saving as DICOMs +dcmExpFakeCT= matRad_DicomExporter; % create instance of matRad_DicomExporter +dcmExpFakeCT.dicomDir = patDirFakeCT; % set the output path for the Dicom export +dcmExpFakeCT.cst = []; % set the structure set for the Dicom export [we will keep it empty and use further the real CT cst] +dcmExpFakeCT.ct = fakeCTct; % set the image volume for the Dicom export +dcmExpFakeCT.matRad_exportDicom(); + +%clear all except of paths, close windows to start from clean space +delete(hGUI); + + +%% Patient Data Import from DICOM +% Here we are. Let us import prepared DICOM real CT data. If you +% have your data you can try to replace it with your data in the folder, +% mentioned above. +% Functions for importing DICOM files will search for .dcm files in the directory +% and automatically recognize 3D volumes and structure files. +% The DICOM files will then be read into the workspace as 'ct' and 'cst', +% representing the CT images and the structure set, respectively. +% Ensure that the matRad root directory, along with all its subdirectories, +% is added to the MATLAB search path. + +impRealCT = matRad_DicomImporter(patDirRealCT); +matRad_importDicom(impRealCT); + +% StructureSet Import does not work in octave +if matRad_cfg.isOctave + cst = realCTcst; +end + +% Review the exported file and automatically identified +% optimization objectives and constraints. +matRadGUI; + +%% Modifying Plan Optimization Objectives +% The constraints for all defined volumes of interest (VOIs) are stored in the cst cell. +% When importing DICOM structure files, matRad uses matRad_createCst() to generate this cell. +% Default regular expressions are used to define target objectives. However, +% additional constraints for organs at risk (OARs) need to be added for the plan. +% One of the methods to do this is by directly modifying the cst cell. +% If different volumes with varying optimization objectives need to be loaded, +% refer to the documentation for matRad_createCst() and choose suitable options +% to create a custom implementation. In this case, the plan will be modified directly here. + +for i = 1:size(cst, 1) + % Accessing each optimization objective and constain + % Read more about how to set it in matRad_DoseOptimizationFunction - Superclass for objectives and constraints. + % This is the superclass for all objectives and constraints to enable easy + % identification. + + if strcmp(cst{i,3},'OAR') + if ~isempty(regexpi(cst{i,2},'spinal', 'once')) + objective = DoseObjectives.matRad_MaxDVH; + objective.penalty = 1; + objective.parameters = {12,1}; %dose, to volume + cst{i,6} = {}; + cst{i,6}{1} = struct(objective); + + elseif ~isempty(regexpi(cst{i,2},'stomach','once')) || ... + ~isempty(regexpi(cst{i,2},'duodenum', 'once')) + objective = DoseObjectives.matRad_MaxDVH; + objective.penalty = 1; + objective.parameters = {30,1}; %dose, to volume + cst{i,6} = {}; + cst{i,6}{1} = struct(objective); + + end + end +end + +% Verify that the new objectives have been added and are visible in the +% user interface. We save it for further synthetic CT calculations +matRadGUI; +realCTcst=cst; + +%% Treatment Plan +% The next step is to define your treatment plan labeled as 'pln'. This +% matlab structure requires input from the treatment planner and defines +% the most important cornerstones of your treatment plan. + +%% +% First of all, we need to define what kind of radiation modality we would +% like to use. Possible values are photons, protons or carbon. In this case +% we want to use photons. Then, we need to define a treatment machine to +% correctly load the corresponding base data. matRad includes base data for +% generic photon linear accelerator called 'Generic'. By this means matRad +% will look for 'photons_Generic.mat' in our root directory and will use +% the data provided in there for dose calculation. +% The number of fractions is set to 30. Internally, matRad considers the +% fraction dose for optimization, however, objetives and constraints are +% defined for the entire treatment. + +pln.radiationMode = 'photons'; +pln.machine = 'Generic'; +pln.numOfFractions = 1; + +%% +% Define the biological model used for modeling biological dose (esp. for +% particles). +% Possible biological models are: +% none: use no specific biological model +% constRBE: use a constant RBE +% MCN: use the variable RBE McNamara model for protons +% WED: use the variable RBE Wedenberg model for protons +% LEM: use the biophysical variable RBE Local Effect model for carbons +% As we are using photons, we simply set the parameter to 'none' +pln.bioModel = 'none'; + +%% +% It is possible to request multiple error scenarios for robustness +% analysis and optimization. Here, we just use the "nominal scenario" +% (nomScen) +pln.multScen = 'nomScen'; + +%% +% Now we have to set some beam parameters. We can define multiple beam +% angles for the treatment which might be used in generic fashion for quick dose calculation. All corresponding couch angles are set to 0 at this +% point. Moreover, we set the bixelWidth to 4, which results in a beamlet +% size of 4 x 4 mm in the isocenter plane. + +pln.propStf.gantryAngles = [0 50 100 150 200 250 300]; +pln.propStf.couchAngles = zeros(1,numel(pln.propStf.gantryAngles)); +pln.propStf.bixelWidth = 5; + +% Obtain the number of beams and voxels from the existing variables and +% calculate the iso-center which is per default the center of gravity of +% all target voxels. +pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); +pln.propStf.isoCenter = matRad_getIsoCenter(cst,ct,0); + +%% Dose calculation settings +% set resolution of dose calculation and optimization +pln.propDoseCalc.doseGrid.resolution.x = 7; % [mm] +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. +stf = matRad_generateStf(ct,cst,pln); + +%% Dose Calculation for real CT +% Let's generate dosimetric information by pre-computing dose influence +% matrices for unit beamlet intensities for real CT of a patient. Having dose influences available +% allows subsequent inverse optimization. +dij = matRad_calcDoseInfluence(ct,cst,stf,pln); + +%% Inverse Optimization for IMRT for real CT +% The goal of the fluence optimization is to find a set of beamlet/pencil +% beam weights which yield the best possible dose distribution according to +% the 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); + +% Get the weights of the optimized plan to apply them to the synthetic CT image of the patient later. +% Call the matRad_planAnalysis function with the prepared arguments to extract DVH +% parameters calculated for the real CT image. +weights=resultGUI.w; +resultGUI = matRad_planAnalysis(resultGUI,ct,cst,stf,pln); + +%Get the plan parameters +dvh = resultGUI.dvh; +qi = resultGUI.qi; + +% Save DVH parameters calculated on the real CT to the table +% for further comparison with plans calculated on the synthetic CT. +% Include the patient number and indicate the CT type as "real" +% to facilitate delta calculations between the plans later. +if matRad_cfg.isMatlab %tables not supported by Octave + dvhTableReal=struct2table(qi); + % Select only DVH parameters from QI table you are interested in comparison + dvhTableReal=dvhTableReal(:,horzcat({'name'},indicators)); + dvhTableReal.patient= repmat(char(name),length(qi),1); + dvhTableReal.ct_type = repmat('real',length(qi),1); + % Check DVH table for real CT + disp(dvhTableReal); +end + +%% Now clear the data from the real CT image, except for the plan parameters, +% and load the synthetic (fake) CT image of the same patient. +% It is important that your image sets are compatible (i.e., same number of CT slices, +% same isocenter position, etc.). We will re-use the structure file from real CT with adjusted objectives +% for dose calculations. +delete(matRadGUI); +clear resultGUI ct cst idx qi* dvh dij; + + +impFakeCT = matRad_DicomImporter(patDirFakeCT); +matRad_importDicom(impFakeCT); +cst=realCTcst; +% Review the exported file and previously identified +% optimization objectives and constraints. +matRadGUI; + +%% Perform the dose calculation for synthetic CT by using the weights of plan, calculated on real CT +% (i.e., obtain the dij variable) for the synthetic CT image. +resultGUI = matRad_calcDoseDirect(ct,stf,pln,cst, weights); +resultGUI = matRad_planAnalysis(resultGUI,ct,cst,stf,pln); +matRadGUI; +%Get the plan parameters calculated on synthetic CT +dvh = resultGUI.dvh; +qi = resultGUI.qi; + +% In the similar fashion, save DVH parameters calculated on the synthetic CT to the table +if matRad_cfg.isMatlab %tables not supported by Octave + dvhTableFake=struct2table(qi); + % Select the same DVH parameters for further comparison + dvhTableFake=dvhTableFake(:,horzcat({'name'},indicators)); + dvhTableFake.patient= repmat(char(name),length(qi),1); + dvhTableFake.ct_type = repmat('fake',length(qi),1); +end + +%% Calculate the difference in between of DVH calculated on real CT (dvh_table_real) and synthetic CT (dvh_table_fake) +if matRad_cfg.isMatlab + dvhTableDiff = dvhTableReal; + for i = 1:height(dvhTableReal) + for j = indicators + if cell2mat(dvhTableReal{i,'name'}) == cell2mat(dvhTableFake{i,'name'}) + dvhTableDiff{i,j} = (dvhTableReal{i,j} - dvhTableFake{i,j}) / dvhTableReal{i,j}*100; + else + dvhTableDiff{i,j} = 'error'; + end + end + end + + disp(dvhTableDiff) + + %%% Save the results to CSV file + %file_path_dvh_diff = "YOUR PATH" + %writetable(dvh_table_diff, file_path_dvh_diff); +end \ No newline at end of file diff --git a/examples/matRad_example1_phantom.m b/examples/matRad_example1_phantom.m index 866e7ce10..2f16d265c 100644 --- a/examples/matRad_example1_phantom.m +++ b/examples/matRad_example1_phantom.m @@ -143,7 +143,8 @@ doseWindow = [0 max([resultGUI.physicalDose(:)])]; figure,title('phantom plan') -matRad_plotSliceWrapper(gca,ct,cst,1,resultGUI.physicalDose,plane,slice,[],[],colorcube,[],doseWindow,[]); +matRad_plotSlice(ct, 'axesHandle', gca, 'cst', cst, 'cubeIdx', 1, 'dose', resultGUI.physicalDose, 'plane', plane, 'slice', slice, 'contourColorMap', colorcube, 'doseWindow', doseWindow); +%matRad_plotSliceWrapper(gca,ct,cst,1,resultGUI.physicalDose,plane,slice,[],[],colorcube,[],doseWindow,[]); %% % We export the the created phantom & dose as dicom. This is handled by the diff --git a/examples/matRad_example20_VHEE.m b/examples/matRad_example20_VHEE.m new file mode 100644 index 000000000..2601513f8 --- /dev/null +++ b/examples/matRad_example20_VHEE.m @@ -0,0 +1,93 @@ +%% Example: VHEE Treatment Plan +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% 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. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% this example contributors +% Authors : F. D'Andrea ; A. Bennan ; L. Ermeneux ; N. Wahl +% +% Based on implementation proposed by M. Sitarz et al. (doi:10.1002/mp.17392) +% Generic machine using FermiEyges model based on work of M.G. Ronga et al. (doi:10.1002/mp.16697) +% Applied matRad for a VHEE study as described by F. D'andrea et al. (doi:10.1016/j.phro.2025.100732) +% Focused machine based on work of L. Whitmore et al. (doi:10.1038/s41598-021-93276-8) +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%% In this example we will show +% (i) how to load patient data into matRad +% (ii) how to setup a VHEE dose calculation +% (iii) how to inversely optimize the pencil beam intensities directly from +% command window in MATLAB. + +%% set matRad runtime configuration +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 prostate +% patient into your workspace +load('PROSTATE.mat'); + +%% Treatment Plan +% Here, we would like to use VHEE for treatment planning. Next, we need to +% define a treatment machine to correctly load the corresponding base data. +% matRad features two base data for VHEE, a divergent beam +% (VHEE_Generic.mat) based on a FermiEyges model that has to be called +% through 'Generic' and a Focused beam (VHEE_Focused.mat), to be called by +% 'Focused'. +pln.radiationMode = 'VHEE'; % either photons / protons / helium / carbon / brachy / VHEE +pln.machine = 'Generic'; % Generic / Focused VHEE - (Focused still in development) +pln.bioModel = 'none'; % 'none' for VHEE + +%% plan parameters +% Now we have to set the remaining plan parameters. +% beam geometry settings +pln.numOfFractions = 30; +pln.propStf.energy = 200; % set VHEE beam energy in MeV [100,150 or 200 MeV] +pln.propStf.bixelWidth = 5; % [mm] / also corresponds to lateral spot spacing for particles +pln.propStf.gantryAngles = [35, 110, 180, 250, 325]; % [°] ; +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); + +% 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.engine = 'HongPB'; + +% optimization settings +pln.propOpt.quantityOpt = 'physicalDose'; % Quantity to optimizer (could also be RBExDose, BED, effect) +pln.propOpt.optimizer = 'IPOPT'; % We can also utilize 'fmincon' from Matlab's optimization toolbox +pln.propOpt.runDAO = false; % 1/true: run DAO, 0/false: don't / will be ignored for particles +pln.propSeq.runSequencing = false; % true: run sequencing, false: don't / will be ignored for particles and also triggered by runDAO below + +%% 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); % Future work - remove low weighted spots to aid MC + +%% use the GUI widgets directly to visualize the result +viewer = matRad_ViewingWidget(); +viewer.doseOpacity = 0.35; %lets change the doseOpacity + +dvhwidget = matRad_DVHStatsWidget(); +dvhwidget.selectedDisplayOption = 'physicalDose'; + +%% Export parameter files for a TOPAS recalculation +% set number of histories lower than default for this example (default: 1e8) +pln.propDoseCalc.numHistoriesDirect = 5e6; +pln.propDoseCalc.engine = 'TOPAS'; +pln.propDoseCalc.externalCalculation = 'write'; +resultGUI_MC = matRad_calcDoseForward(ct,cst,stf,pln,resultGUI.w); + diff --git a/examples/matRad_example2_photons.m b/examples/matRad_example2_photons.m index e1b6536a4..8523795c5 100644 --- a/examples/matRad_example2_photons.m +++ b/examples/matRad_example2_photons.m @@ -182,13 +182,17 @@ pln.propStf.couchAngles = zeros(1,numel(pln.propStf.gantryAngles)); pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); - - +%Let's rerun the dose calculation and optimization stf = matRad_generateStf(ct,cst,pln); pln.propStf.isoCenter = vertcat(stf.isoCenter); dij = matRad_calcDoseInfluence(ct,cst,stf,pln); resultGUI_coarse = matRad_fluenceOptimization(dij,cst,pln); +%We append the new result to the resultGUI variable (recognized by the GUI) +%using the identifier coarse +resultGUI = matRad_appendResultGUI(resultGUI,resultGUI_coarse,false,'coarse'); +%A GUI update ensures the current values are updated +matRadGUI; %% Visual Comparison of results % Let's compare the new recalculation against the optimization result. @@ -198,9 +202,11 @@ doseWindow = [0 max([resultGUI.physicalDose(:); resultGUI_coarse.physicalDose(:)])]; figure,title('original plan - fine beam spacing') -matRad_plotSliceWrapper(gca,ct,cst,1,resultGUI.physicalDose,plane,slice,[],0.75,colorcube,[],doseWindow,[]); +matRad_plotSlice(ct, 'axesHandle', gca, 'cst', cst, 'cubeIdx', 1, 'dose', resultGUI.physicalDose, 'plane', plane, 'slice', slice, 'alpha', 0.75, 'contourColorMap', colorcube, 'doseWindow', doseWindow); +%matRad_plotSliceWrapper(gca,ct,cst,1,resultGUI.physicalDose,plane,slice,[],0.75,colorcube,[],doseWindow,[]); figure,title('modified plan - coarse beam spacing') -matRad_plotSliceWrapper(gca,ct,cst,1,resultGUI_coarse.physicalDose,plane,slice,[],0.75,colorcube,[],doseWindow,[]); +matRad_plotSlice(ct, 'axesHandle', gca, 'cst', cst, 'cubeIdx', 1, 'dose', resultGUI_coarse.physicalDose, 'plane', plane, 'slice', slice, 'alpha', 0.75, 'contourColorMap', colorcube, 'doseWindow', doseWindow); +%matRad_plotSliceWrapper(gca,ct,cst,1,resultGUI_coarse.physicalDose,plane,slice,[],0.75,colorcube,[],doseWindow,[]); %% % At this point we would like to see the absolute difference of the first @@ -208,7 +214,8 @@ % beam spacing) absDiffCube = resultGUI.physicalDose-resultGUI_coarse.physicalDose; figure,title( 'fine beam spacing plan - coarse beam spacing plan') -matRad_plotSliceWrapper(gca,ct,cst,1,absDiffCube,plane,slice,[],[],colorcube); +matRad_plotSlice(ct, 'axesHandle', gca, 'cst', cst, 'cubeIdx', 1, 'dose', absDiffCube, 'plane', plane, 'slice', slice, 'contourColorMap', colorcube); +%matRad_plotSliceWrapper(gca,ct,cst,1,absDiffCube,plane,slice,[],[],colorcube); %% Obtain dose statistics % Two more columns will be added to the cst structure depicting the DVH and diff --git a/examples/matRad_example5_protons.m b/examples/matRad_example5_protons.m index 82a7dfdde..b1be3bbcb 100644 --- a/examples/matRad_example5_protons.m +++ b/examples/matRad_example5_protons.m @@ -124,13 +124,16 @@ doseWindow = [0 max([resultGUI.RBExDose(:); resultGUI_isoShift.RBExDose(:)])]; figure,title('original plan') -matRad_plotSliceWrapper(gca,ct,cst,1,resultGUI.RBExDose,plane,slice,[],0.75,colorcube,[],doseWindow,[]); +matRad_plotSlice(ct, 'axesHandle', gca, 'cst', cst, 'cubeIdx', 1, 'dose', resultGUI.RBExDose, 'plane', plane, 'slice', slice, 'alpha', 0.75, 'contourColorMap', colorcube, 'doseWindow', doseWindow); +%matRad_plotSliceWrapper(gca,ct,cst,1,resultGUI.RBExDose,plane,slice,[],0.75,colorcube,[],doseWindow,[]); figure,title('shifted plan') -matRad_plotSliceWrapper(gca,ct,cst,1,resultGUI_isoShift.RBExDose,plane,slice,[],0.75,colorcube,[],doseWindow,[]); +matRad_plotSlice(ct, 'axesHandle', gca, 'cst', cst, 'cubeIdx', 1, 'dose', resultGUI_isoShift.RBExDose, 'plane', plane, 'slice', slice, 'alpha', 0.75, 'contourColorMap', colorcube, 'doseWindow', doseWindow); +%matRad_plotSliceWrapper(gca,ct,cst,1,resultGUI_isoShift.RBExDose,plane,slice,[],0.75,colorcube,[],doseWindow,[]); absDiffCube = resultGUI.RBExDose-resultGUI_isoShift.RBExDose; figure,title('absolute difference') -matRad_plotSliceWrapper(gca,ct,cst,1,absDiffCube,plane,slice,[],[],colorcube); +matRad_plotSlice(ct, 'axesHandle', gca, 'cst', cst, 'cubeIdx', 1, 'dose', absDiffCube, 'plane', plane, 'slice', slice, 'contourColorMap', colorcube); +%matRad_plotSliceWrapper(gca,ct,cst,1,absDiffCube,plane,slice,[],[],colorcube); % Let's plot single profiles that are perpendicular to the beam direction ixProfileY = matRad_world2cubeIndex(pln.propStf.isoCenter(1,:),ct); diff --git a/examples/matRad_example7_carbon.m b/examples/matRad_example7_carbon.m index 86426f511..d57cf9a93 100644 --- a/examples/matRad_example7_carbon.m +++ b/examples/matRad_example7_carbon.m @@ -92,7 +92,7 @@ %% Dose Calculation dij = matRad_calcDoseInfluence(ct,cst,stf,pln); -%% Inverse Optimization for IMPT based on RBE-weighted dose +%% Inverse Optimization for Carbon ion treatment based on RBE-weighted dose % The goal of the fluence optimization is to find a set of bixel/spot % weights which yield the best possible dose distribution according to the % clinical objectives and constraints underlying the radiation treatment. @@ -113,7 +113,7 @@ figure; imagesc(resultGUI.LET(:,:,slice)),colorbar, colormap(jet); -%% Inverse Optimization for IMPT based on biological effect +%% Inverse Optimization for Carbon ion treatment based on biological effect % To perform a dose optimization for carbon ions we can also use the % biological effect instead of the RBE-weighted dose. Therefore we have to % change the optimization mode and restart the optimization @@ -129,6 +129,22 @@ colorbar; colormap(jet); +%% Inverse Optimization for Carbon ion treatment based on BED +% To perform a dose optimization for carbon ions we can also use the +% BED instead of the RBE-weighted dose. Therefore we have to +% change the optimization mode and restart the optimization +pln.propOpt.quantityOpt = 'BED'; +resultGUI_BED = matRad_fluenceOptimization(dij,cst,pln); + +%% Visualize differences +% Through optimzation based on the biological effect we obtain a slightly +% different dose distribution as visualized by the following dose +% difference map +figure; +imagesc(resultGUI.RBExDose(:,:,slice)-resultGUI_BED.RBExDose(:,:,slice)); +colorbar; +colormap(jet); + %% Change Radiosensitivity % The previous treatment plan was optimized using an photon alpha-beta % ratio of 2 for all tissues. Now, Let's change the radiosensitivity by @@ -149,17 +165,20 @@ doseWindow = [0 max([resultGUI_effect.RBExDose(:); resultGUI_tissue.RBExDose(:)])]; figure, -matRad_plotSliceWrapper(gca,ct,cst,1,resultGUI_effect.RBExDose,plane,slice,[],[],colorcube,[],doseWindow,[]); +matRad_plotSlice(ct, 'axesHandle', gca, 'cst', cst, 'cubeIdx', 1, 'dose', resultGUI_effect.RBExDose, 'plane', plane, 'slice', slice, 'contourColorMap', colorcube, 'doseWindow', doseWindow); +%matRad_plotSliceWrapper(gca,ct,cst,1,resultGUI_effect.RBExDose,plane,slice,[],[],colorcube,[],doseWindow,[]); title('original plan') figure, -matRad_plotSliceWrapper(gca,ct,cst,1,resultGUI_tissue.RBExDose,plane,slice,[],[],colorcube,[],doseWindow,[]); +matRad_plotSlice(ct, 'axesHandle', gca, 'cst', cst, 'cubeIdx', 1, 'dose', resultGUI_tissue.RBExDose, 'plane', plane, 'slice', slice, 'contourColorMap', colorcube, 'doseWindow', doseWindow); +%matRad_plotSliceWrapper(gca,ct,cst,1,resultGUI_tissue.RBExDose,plane,slice,[],[],colorcube,[],doseWindow,[]); title('manipulated plan') %% % At this point we would like to see the absolute difference of the original optimization and the % recalculation. absDiffCube = resultGUI_effect.RBExDose-resultGUI_tissue.RBExDose; figure, -matRad_plotSliceWrapper(gca,ct,cst,1,absDiffCube,plane,slice,[],[],colorcube); +matRad_plotSlice(ct, 'axesHandle', gca, 'cst', cst, 'cubeIdx', 1, 'dose', absDiffCube, 'plane', plane, 'slice', slice, 'contourColorMap', colorcube); +%matRad_plotSliceWrapper(gca,ct,cst,1,absDiffCube,plane,slice,[],[],colorcube); title('absolute difference') %% % Plot both doses with absolute difference and gamma analysis diff --git a/examples/matRad_example8_protonsRobust.m b/examples/matRad_example8_protonsRobust.m index 7c1fb0a64..9e7dee90b 100644 --- a/examples/matRad_example8_protonsRobust.m +++ b/examples/matRad_example8_protonsRobust.m @@ -125,19 +125,23 @@ slice = matRad_world2cubeIndex(pln.propStf.isoCenter(1,:),ct); slice = slice(3); -figure,matRad_plotSliceWrapper(gca,ct,cst,1,resultGUI.RBExDose_beam1 ,plane,slice,[],[],colorcube,[],[0 max(resultGUI.RBExDose_beam1(:))],[]);title('conventional plan - beam1') -figure,matRad_plotSliceWrapper(gca,ct,cst,1,resultGUIrobust.RBExDose_beam1,plane,slice,[],[],colorcube,[],[0 max(resultGUIrobust.RBExDose_beam1(:))],[]);title('robust plan - beam1') +figure,matRad_plotSlice(ct, 'axesHandle', gca, 'cst', cst, 'cubeIdx', 1, 'dose', resultGUI.RBExDose_beam1, 'plane', plane, 'slice', slice, 'contourColorMap', colorcube, 'doseWindow', [0 max(resultGUI.RBExDose_beam1(:))]);title('conventional plan - beam1') +figure,matRad_plotSlice(ct, 'axesHandle', gca, 'cst', cst, 'cubeIdx', 1, 'dose', resultGUIrobust.RBExDose_beam1, 'plane', plane, 'slice', slice, 'contourColorMap', colorcube, 'doseWindow', [0 max(resultGUIrobust.RBExDose_beam1(:))]);title('robust plan - beam1') +%figure,matRad_plotSliceWrapper(gca,ct,cst,1,resultGUI.RBExDose_beam1 ,plane,slice,[],[],colorcube,[],[0 max(resultGUI.RBExDose_beam1(:))],[]);title('conventional plan - beam1') +%figure,matRad_plotSliceWrapper(gca,ct,cst,1,resultGUIrobust.RBExDose_beam1,plane,slice,[],[],colorcube,[],[0 max(resultGUIrobust.RBExDose_beam1(:))],[]);title('robust plan - beam1') % create an interactive plot to slide through individual scnearios f = figure;title('individual scenarios'); numScen = 1;doseWindow = [0 3.5]; -matRad_plotSliceWrapper(gca,ct,cst,1,resultGUIrobust.(['RBExDose_scen' num2str(round(numScen))]),plane,slice,[],[],colorcube,[],doseWindow,[]); +matRad_plotSlice(ct, 'axesHandle', gca, 'cst', cst, 'cubeIdx', 1, 'dose', resultGUIrobust.(['RBExDose_scen' num2str(round(numScen))]), 'plane', plane, 'slice', slice, 'contourColorMap', colorcube, 'doseWindow', doseWindow); +%matRad_plotSliceWrapper(gca,ct,cst,1,resultGUIrobust.(['RBExDose_scen' num2str(round(numScen))]),plane,slice,[],[],colorcube,[],doseWindow,[]); [env,envver] = matRad_getEnvironment(); if strcmp(env,'MATLAB') || str2double(envver(1)) >= 5 b = uicontrol('Parent',f,'Style','slider','Position',[50,5,419,23],... 'value',numScen, 'min',1, 'max',pln.multScen.totNumScen,'SliderStep', [1/(pln.multScen.totNumScen-1) , 1/(pln.multScen.totNumScen-1)]); - set(b,'Callback',@(es,ed) matRad_plotSliceWrapper(gca,ct,cst,1,resultGUIrobust.(['RBExDose_scen' num2str(round(get(es,'Value')))]),plane,slice,[],[],colorcube,[],doseWindow,[])); + set(b,'Callback',@(es,ed) matRad_plotSlice(ct, 'axesHandle', gca, 'cst', cst, 'cubeIdx', 1, 'dose', resultGUIrobust.(['RBExDose_scen' num2str(round(get(es,'Value')))]), 'plane', plane, 'slice', slice, 'contourColorMap', colorcube, 'doseWindow', doseWindow)); + %matRad_plotSliceWrapper(gca,ct,cst,1,resultGUIrobust.(['RBExDose_scen' num2str(round(get(es,'Value')))]),plane,slice,[],[],colorcube,[],doseWindow,[])); end %% Indicator calculation and show DVH and QI @@ -152,8 +156,10 @@ [cstStatRob, resultGUISampRob, metaRob] = matRad_samplingAnalysis(ct,cst,plnSampRob,caSampRob, mSampDoseRob, resultGUInomScen); figure,title('std dose cube based on sampling - conventional') -matRad_plotSliceWrapper(gca,ct,cst,1,resultGUISamp.stdCube,plane,slice,[],[],colorcube,[],[0 max(resultGUISamp.stdCube(:))]); +matRad_plotSlice(ct, 'axesHandle', gca, 'cst', cst, 'cubeIdx', 1, 'dose', resultGUISamp.stdCube, 'plane', plane, 'slice', slice, 'contourColorMap', colorcube, 'doseWindow', [0 max(resultGUISamp.stdCube(:))]); +%matRad_plotSliceWrapper(gca,ct,cst,1,resultGUISamp.stdCube,plane,slice,[],[],colorcube,[],[0 max(resultGUISamp.stdCube(:))]); figure,title('std dose cube based on sampling - robust') -matRad_plotSliceWrapper(gca,ct,cst,1,resultGUISampRob.stdCube,plane,slice,[],[],colorcube,[],[0 max(resultGUISampRob.stdCube(:))]); +matRad_plotSlice(ct, 'axesHandle', gca, 'cst', cst, 'cubeIdx', 1, 'dose', resultGUISampRob.stdCube, 'plane', plane, 'slice', slice, 'contourColorMap', colorcube, 'doseWindow', [0 max(resultGUISampRob.stdCube(:))]); +%matRad_plotSliceWrapper(gca,ct,cst,1,resultGUISampRob.stdCube,plane,slice,[],[],colorcube,[],[0 max(resultGUISampRob.stdCube(:))]); diff --git a/matRad.m b/matRad.m index f317e076e..737dab26f 100644 --- a/matRad.m +++ b/matRad.m @@ -25,10 +25,10 @@ % meta information for treatment plan pln.numOfFractions = 30; -pln.radiationMode = 'photons'; % either photons / protons / helium / carbon / brachy -pln.machine = 'Generic'; % generic for RT / LDR or HDR for BT +pln.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 photons, protons, carbon, brachy % constRBE: constant RBE for photons and protons +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 diff --git a/matRad/4D/matRad_addMovement.m b/matRad/4D/matRad_addMovement.m index db8551dff..606f5f1df 100644 --- a/matRad/4D/matRad_addMovement.m +++ b/matRad/4D/matRad_addMovement.m @@ -43,7 +43,7 @@ expectedDVF = {'pull', 'push'}; -p = inputParser; +p = inputParser; addParameter(p,'dvfType','pull', @(x) any(validatestring(x,expectedDVF))) addParameter(p,'visBool',false, @islogical); parse(p,varargin{:}); @@ -64,64 +64,64 @@ % generate scenarios for i = 1:numOfCtScen - + if isfield(ct,'hlut') padValue = min(ct.hlut(:,2)); else padValue = -1024; end - + ct.dvf{i} = zeros([ct.cubeDim, 3]); - + dVec = arrayfun(@(A) A*sin((i-1)*pi / numOfCtScen)^2, amp); - + ct.dvf{i}(:,:,:,1) = dVec(1); % deformation along x direction (i.e. 2nd coordinate in dose/ct) ct.dvf{i}(:,:,:,2) = dVec(2); ct.dvf{i}(:,:,:,3) = dVec(3); - + matRad_cfg.dispInfo('Deforming ct phase %d with [dx,dy,dz] = [%f,%f,%f] voxels\n',i,dVec(1),dVec(2),dVec(3)); - + % warp ct switch env case 'MATLAB' ct.cubeHU{i} = imwarp(ct.cubeHU{1}, ct.dvf{i},'FillValues',padValue); - + if isfield(ct,'cube') ct.cube{i} = imwarp(ct.cube{1}, ct.dvf{i},'FillValues',0); end - + % warp cst for j = 1:size(cst,1) tmp = zeros(ct.cubeDim); tmp(cst{j,4}{1}) = 1; tmpWarp = imwarp(tmp, ct.dvf{i}); - + cst{j,4}{i} = find(tmpWarp > .5); end case 'OCTAVE' ct.cubeHU{i} = displaceOctave(ct.cubeHU{1}, ct.dvf{i},'linear',padValue); - + if isfield(ct,'cube') ct.cube{i} = displaceOctave(ct.cube{1},ct.dvf{i},'linear',0); end - + % warp cst for j = 1:size(cst,1) tmp = zeros(ct.cubeDim); tmp(cst{j,4}{1}) = 1; tmpWarp = displaceOctave(tmp, ct.dvf{i},'linear',0); - + cst{j,4}{i} = find(tmpWarp > .5); end - otherwise + otherwise end - + % convert dvfs to [mm] ct.dvf{i}(:,:,:,1) = ct.dvf{i}(:,:,:,1).* ct.resolution.x; ct.dvf{i}(:,:,:,2) = ct.dvf{i}(:,:,:,2).*ct.resolution.y; ct.dvf{i}(:,:,:,3) = ct.dvf{i}(:,:,:,3).* ct.resolution.z; - - ct.dvf{i} = permute(ct.dvf{i}, [4,1,2,3]); + + ct.dvf{i} = permute(ct.dvf{i}, [4,1,2,3]); end @@ -139,8 +139,8 @@ end function newCube = displaceOctave(cube,vectorfield,interpMethod,fillValue) -x = 1:size(cube,1); -y = 1:size(cube,2); +x = 1:size(cube,2); +y = 1:size(cube,1); z = 1:size(cube,3); [X,Y,Z] = meshgrid(x,y,z); diff --git a/matRad/4D/matRad_calc4dDose.m b/matRad/4D/matRad_calc4dDose.m index 072b7296f..5aea56558 100644 --- a/matRad/4D/matRad_calc4dDose.m +++ b/matRad/4D/matRad_calc4dDose.m @@ -33,7 +33,7 @@ % LICENSE file. % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - +matRad_cfg = MatRad_Config.instance(); if ~exist('accType','var') accType = 'DDM'; end @@ -74,47 +74,85 @@ tmpResultGUI = matRad_calcCubes(totalPhaseMatrix(:,i),dij,i); % compute physical dose for physical opt - if isa(pln.bioModel,'matRad_EmptyBiologicalModel') - resultGUI.phaseDose{i} = tmpResultGUI.physicalDose; - % compute RBExDose with const RBE - elseif isa(pln.bioModel,'matRad_ConstantRBE') - resultGUI.phaseRBExDose{i} = tmpResultGUI.RBExDose; - % compute all fields - 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; - 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))); - else - matRad_cfg.dispError('Unsupported biological model %s!',pln.bioModel.model); + resultGUI.phaseDose{i} = tmpResultGUI.physicalDose; + if ~isa(pln.bioModel,'matRad_EmptyBiologicalModel') + % compute RBExDose + if isa(pln.bioModel,'matRad_ConstantRBE') + resultGUI.phaseRBExDose{i} = tmpResultGUI.RBExDose; + 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; + 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))); + else + matRad_cfg.dispError('Unsupported biological model %s!',pln.bioModel.model); + end + end + + for beamIx = 1:dij.numOfBeams + resultGUI.(['phaseDose_beam', num2str(beamIx)]){i} = tmpResultGUI.(['physicalDose_beam', num2str(beamIx)]); + if ~isa(pln.bioModel,'matRad_EmptyBiologicalModel') + % compute RBExD + if isa(pln.bioModel,'matRad_ConstantRBE') + resultGUI.(['phaseRBExDose_beam', num2str(beamIx)]){i} = tmpResultGUI.(['RBExDose_beam', num2str(beamIx)]); + 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; + 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))); + else + matRad_cfg.dispError('Unsupported biological model %s!',pln.bioModel.model); + end + end end end % accumulation -if isa(pln.bioModel,'matRad_EmptyBiologicalModel') - - resultGUI.accPhysicalDose = matRad_doseAcc(ct,resultGUI.phaseDose, cst, accType); - -elseif isa(pln.bioModel,'matRad_ConstantRBE') - - resultGUI.accRBExDose = matRad_doseAcc(ct,resultGUI.phaseRBExDose, cst, accType); - -elseif isa(pln.bioModel,'matRad_LQBasedModel') +resultGUI.accPhysicalDose = matRad_doseAcc(ct,resultGUI.phaseDose, cst, accType); - resultGUI.accAlphaDose = matRad_doseAcc(ct,resultGUI.phaseAlphaDose, cst,accType); - resultGUI.accSqrtBetaDose = matRad_doseAcc(ct,resultGUI.phaseSqrtBetaDose, 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 - % only compute where we have biologically defined tissue - ix = (ax{1} ~= 0); +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 - 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 diff --git a/matRad/4D/matRad_makePhaseMatrix.m b/matRad/4D/matRad_makePhaseMatrix.m index bfc89476c..861717ff9 100644 --- a/matRad/4D/matRad_makePhaseMatrix.m +++ b/matRad/4D/matRad_makePhaseMatrix.m @@ -70,7 +70,7 @@ % 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; diff --git a/matRad/IO/matRad_writeNifTI.m b/matRad/IO/matRad_writeNifTI.m index 53972e66c..2986e9a8f 100644 --- a/matRad/IO/matRad_writeNifTI.m +++ b/matRad/IO/matRad_writeNifTI.m @@ -63,6 +63,9 @@ info.Datatype = metadata.datatype; info.ImageSize = size(cube); info.Description = sprintf('Exported from matRad %s',matRad_version()); +if length(info.Description) >= 80 + info.Description = info.Description(1:79); +end if max(info.ImageSize) > 32767 info.Version = 'NIfTI2'; diff --git a/matRad/MatRad_Config.m b/matRad/MatRad_Config.m index 44a8d95bc..06e12316b 100644 --- a/matRad/MatRad_Config.m +++ b/matRad/MatRad_Config.m @@ -201,6 +201,8 @@ function setDefaultProperties(obj) obj.defaults.machine.carbon = 'Generic'; obj.defaults.machine.brachy = 'HDR'; obj.defaults.machine.fallback = 'Generic'; + obj.defaults.machine.VHEE = 'Generic'; + %Default Bio Model obj.defaults.bioModel.photons = 'none'; @@ -209,9 +211,10 @@ function setDefaultProperties(obj) obj.defaults.bioModel.carbon = 'LEM'; obj.defaults.bioModel.brachy = 'none'; obj.defaults.bioModel.fallback = 'none'; - + obj.defaults.bioModel.VHEE = 'none'; + %Default Steering/Geometry Properties - obj.defaults.propStf.generator = {'PhotonIMRT','ParticleIMPT','SimpleBrachy'}; + obj.defaults.propStf.generator = {'PhotonIMRT','ParticleIMPT','SimpleBrachy','VHEE'}; obj.defaults.propStf.longitudinalSpotSpacing = 2; obj.defaults.propStf.addMargin = true; %expand target for beamlet finding obj.defaults.propStf.bixelWidth = 5; diff --git a/matRad/basedata/VHEE_Focused.mat b/matRad/basedata/VHEE_Focused.mat new file mode 100644 index 000000000..d8af419fc Binary files /dev/null and b/matRad/basedata/VHEE_Focused.mat differ diff --git a/matRad/basedata/VHEE_Generic.mat b/matRad/basedata/VHEE_Generic.mat new file mode 100644 index 000000000..5276e16a9 Binary files /dev/null and b/matRad/basedata/VHEE_Generic.mat differ diff --git a/matRad/basedata/matRad_MCemittanceBaseData.m b/matRad/basedata/matRad_MCemittanceBaseData.m index d8871bfaf..e081dc28c 100644 --- a/matRad/basedata/matRad_MCemittanceBaseData.m +++ b/matRad/basedata/matRad_MCemittanceBaseData.m @@ -122,7 +122,7 @@ if isfield(machine.data(ixE), 'energySpectrum') && ~obj.forceSpectrumApproximation energySpectrum = machine.data(ixE).energySpectrum; if isfield(energySpectrum,'type') && strcmp(energySpectrum.type,'gaussian') - energyData.NominalEnergy = ones(1,4) * machine.data(ixE).energy(:); + energyData.NominalEnergy = machine.data(ixE).energy(:); energyData.MeanEnergy = machine.data(ixE).energySpectrum.mean(:); energyData.EnergySpread = machine.data(ixE).energySpectrum.sigma(:); else @@ -541,6 +541,21 @@ mcDataOptics.Divergence2y = 0; mcDataOptics.Correlation2y = 0; mcDataOptics.FWHMatIso = 2.355 * sigmaSqIso; + + % Parameters for parametrization of sigma squared model. + % sigma^2 = a + b*z + c*z^2; + mcDataOptics.sSQ_a = (sigmaSqIso/10)^2; + mcDataOptics.sSQ_b = (2*rho*sigmaT*sigmaSqIso)/10; + mcDataOptics.sSQ_c = sigmaT^2; + + % Parameters for emittance model + mcDataOptics.twissEpsilonX = sqrt(mcDataOptics.sSQ_a*mcDataOptics.sSQ_c - (mcDataOptics.sSQ_b^2)/4); + mcDataOptics.twissAlphaX = - mcDataOptics.sSQ_b/(2*mcDataOptics.twissEpsilonX); + mcDataOptics.twissBetaX = mcDataOptics.sSQ_a/mcDataOptics.twissEpsilonX; + + mcDataOptics.twissEpsilonY = mcDataOptics.twissEpsilonX; + mcDataOptics.twissAlphaY = mcDataOptics.twissAlphaX; + mcDataOptics.twissBetaY = mcDataOptics.twissBetaX; end diff --git a/matRad/bioModels/LQbasedModels/kernelBasedModels/matRad_LQKernelBasedModel.m b/matRad/bioModels/LQbasedModels/kernelBasedModels/matRad_LQKernelBasedModel.m index 8370ca7d9..6f9c5853a 100644 --- a/matRad/bioModels/LQbasedModels/kernelBasedModels/matRad_LQKernelBasedModel.m +++ b/matRad/bioModels/LQbasedModels/kernelBasedModels/matRad_LQKernelBasedModel.m @@ -80,4 +80,22 @@ end end + + methods (Static) + + + function [alphaX, betaX] = getAvailableTissueParameters(pln) + + % load machine + machine = matRad_loadMachine(pln); + if isfield(machine.data,'alphaX') && isfield(machine.data,'betaX') + alphaX = machine.data(1).alphaX; + betaX = machine.data(1).betaX; + else + matRad_cfg = MatRad_Config.instance(); + matRad_cfg.dispError('The selected biological model requires AlphaX and BetaX to be set in the machine file but none was found.'); + end + + end + end end \ No newline at end of file diff --git a/matRad/bioModels/matRad_BiologicalModel.m b/matRad/bioModels/matRad_BiologicalModel.m index 0dda39af1..283648ce4 100644 --- a/matRad/bioModels/matRad_BiologicalModel.m +++ b/matRad/bioModels/matRad_BiologicalModel.m @@ -126,7 +126,20 @@ %Use the root folder and the biomodel folder only folders = {fileparts(mfilename('fullpath'))}; folders = [folders matRad_cfg.userfolders]; - metaBioModels = matRad_findSubclasses(meta.class.fromName(mfilename('class')),'folders',folders,'includeSubfolders',true); + + persistent metaBioModels lastOptionalPaths + + %First we do a sanity check if persistently stored metaclasses are valid + if ~matRad_cfg.isOctave && ~isempty(metaBioModels) && ~all(cellfun(@isvalid,metaBioModels)) + matRad_cfg.dispWarning('Found invalid BioModels, updating model cache.'); + metaBioModels = []; + end + + if isempty(metaBioModels) || (~isempty(lastOptionalPaths) && ~isequal(lastOptionalPaths, folders)) + lastOptionalPaths = folders; + metaBioModels = matRad_findSubclasses(meta.class.fromName(mfilename('class')),'folders',folders,'includeSubfolders',true); + end + classList = matRad_identifyClassesByConstantProperties(metaBioModels,'model','defaults',{'none'}); if nargin > 0 @@ -238,6 +251,13 @@ end end + function [alphaX, betaX] = getAvailableTissueParameters(pln) + % empty values in standard implementation, needs to be + % overwritten in subclasses + alphaX = []; + betaX = []; + end + end diff --git a/matRad/bioModels/matRad_EmptyBiologicalModel.m b/matRad/bioModels/matRad_EmptyBiologicalModel.m index 54419cffe..daf7e4e27 100644 --- a/matRad/bioModels/matRad_EmptyBiologicalModel.m +++ b/matRad/bioModels/matRad_EmptyBiologicalModel.m @@ -16,7 +16,7 @@ % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% properties (Constant) model = 'none'; - possibleRadiationModes = {'photons', 'protons', 'carbon', 'helium', 'brachy'}; + possibleRadiationModes = {'photons', 'protons', 'carbon', 'helium', 'brachy','VHEE'}; requiredQuantities = {}; defaultReportQuantity = 'physicalDose'; end diff --git a/matRad/bioModels/matRad_bioModel.m b/matRad/bioModels/matRad_bioModel.m index 5bfa1ff9f..4d5bdb725 100644 --- a/matRad/bioModels/matRad_bioModel.m +++ b/matRad/bioModels/matRad_bioModel.m @@ -1,4 +1,4 @@ -function model = matRad_bioModel(sRadiationMode, sModel) +function model = matRad_bioModel(radiationMode, model, providedQuantities) % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % matRad_bioModel % This is a helper function to instantiate a matRad_BiologicalModel. This @@ -6,18 +6,21 @@ % Biological Models will follow a polymorphic software architecture % % call -% matRad_bioModel(sRadiationMode, sModel) +% matRad_bioModel(radiationMode, model) % % e.g. pln.bioModel = matRad_bioModel('protons','MCN') % % input -% sRadiationMode: radiation modality 'photons' 'protons' 'helium' 'carbon' 'brachy' +% radiationMode: radiation modality 'photons' 'protons' 'helium' 'carbon' 'brachy' % -% sModel: string to denote which biological model is used +% model: string to denote which biological model is used % 'none': for photons, protons, carbon '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 % +% providedQuantities: optional cell string of provided quantities to +% check if the model can be evaluated +% % output % model: instance of a biological model % @@ -36,51 +39,10 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -matRad_cfg = MatRad_Config.instance(); - -% Look for the correct inputs -p = inputParser; -addRequired(p, 'sRadiationMode', @ischar); -addRequired(p, 'sModel',@ischar); - -p.KeepUnmatched = true; - -%Check for the available models -mainFolder = fullfile(matRad_cfg.matRadSrcRoot,'bioModels'); -userDefinedFolder = fullfile(matRad_cfg.primaryUserFolder, 'bioModels'); - -if ~exist(userDefinedFolder,"dir") - folders = {mainFolder}; +if nargin < 3 + model = matRad_BiologicalModel.validate(model,radiationMode); else - folders = {mainFolder,userDefinedFolder}; -end - -availableBioModelsClassList = matRad_findSubclasses('matRad_BiologicalModel', 'folders', folders , 'includeSubfolders',true); -modelInfos = matRad_identifyClassesByConstantProperties(availableBioModelsClassList,'model'); -modelNames = {modelInfos.model}; - -if numel(unique({modelInfos.model})) ~= numel(modelInfos) - matRad_cfg.dispError('Multiple biological models with the same name available.'); + model = matRad_BiologicalModel.validate(model,radiationMode,providedQuantities); end - -selectedModelIdx = find(strcmp(sModel, modelNames)); - -% Create first instance of the selected model -if ~isempty(selectedModelIdx) - tmpBioParam = modelInfos(selectedModelIdx).handle(); -else - matRad_cfg.dispError('Unrecognized biological model: %s', sModel); -end - -% For the time being I do not assigne the model specific parameters, they -% can be assigned by the user later - -correctRadiationModality = any(strcmp(tmpBioParam.possibleRadiationModes, sRadiationMode)); - -if ~correctRadiationModality - matRad_cfg.dispError('Incorrect radiation modality for the required biological model'); -end - -model = tmpBioParam; end % end function \ No newline at end of file diff --git a/matRad/dicom/@matRad_DicomExporter/matRad_exportDicomRTDoses.m b/matRad/dicom/@matRad_DicomExporter/matRad_exportDicomRTDoses.m index ed420dc0e..c0d6380a1 100644 --- a/matRad/dicom/@matRad_DicomExporter/matRad_exportDicomRTDoses.m +++ b/matRad/dicom/@matRad_DicomExporter/matRad_exportDicomRTDoses.m @@ -44,15 +44,12 @@ meta.Modality = 'RTDOSE'; meta.Manufacturer = ''; -meta.DoseUnits = 'GY'; %Reference %ID of the CT meta.StudyInstanceUID = obj.StudyInstanceUID; meta.StudyID = obj.StudyID; - - %Dates & Times currDate = now; currDateStr = datestr(currDate,'yyyymmdd'); @@ -99,10 +96,11 @@ meta.GridFrameOffsetVector = transpose(ct.z - ct.z(1)); %Referenced Plan -%This does currently not work due to how Matlab creates UIDs by itself, -%we can not know the reference before it is written by the RTPlanExport, -%which itself needs the RTDose UIDs -%{ +%This does currently not work well due to how Matlab creates UIDs by +%itself, we can not know the reference before it is written by the +%RTPlanExport, which itself needs the RTDose UIDs. +%However, we need to set the ReferencedRTPlanSequence, because it is a +%conditionally required field according to the DICOM standard try rtPlanUID = obj.rtPlanMeta.SOPInstanceUID; catch @@ -111,38 +109,34 @@ obj.rtPlanMeta.SOPClassUID = obj.rtPlanClassUID; rtPlanUID = obj.rtPlanMeta.SOPInstanceUID; end -%} - - -%meta.ReferencedRTPlanSequence.Item_1.ReferencedSOPClassUID = obj.rtPlanClassUID; -%meta.ReferencedRTPlanSequence.Item_1.ReferencedSOPInstanceUID = rtPlanUID; - -if nargin < 4 || isempty(doseFieldNames) - doseFieldNames = cell(0); - fn = fieldnames(obj.resultGUI); - for i = 1:numel(fn) - if numel(size(obj.resultGUI.(fn{i}))) == 3 - doseFieldNames{end+1} = fn{i}; - end + +meta.ReferencedRTPlanSequence.Item_1.ReferencedSOPClassUID = obj.rtPlanClassUID; +meta.ReferencedRTPlanSequence.Item_1.ReferencedSOPInstanceUID = rtPlanUID; + +doseFieldNames = cell(0); +fn = fieldnames(obj.resultGUI); +for i = 1:numel(fn) + if numel(size(obj.resultGUI.(fn{i}))) == 3 + doseFieldNames{end+1} = fn{i}; end end - - - - obj.rtDoseMetas = struct([]); obj.rtDoseExportStatus = struct([]); for i = 1:numel(doseFieldNames) - doseName = doseFieldNames{i}; + doseName = doseFieldNames{i}; + doseUnits = 'GY'; %Now check if we export physical or RBE weighted dose, they are known %to dicom - if strncmp(doseName,'physicalDose',12) + if strncmp(doseName,'physicalDose',12) || strncmp(doseName,'LET',3) doseType = 'PHYSICAL'; - elseif strncmp(doseName,'RBExDose',8) + elseif strncmp(doseName,'RBExDose',8) || strncmp(doseName,'BED',3) || strncmp(doseName,'alpha',3) || strncmp(doseName,'beta',3) || strncmp(doseName,'effect',6) || strncmp(doseName,'RBE',3) doseType = 'EFFECTIVE'; + if strncmp(doseName,'RBE',3) + doseUnits = 'RELATIVE'; + end else matRad_cfg.dispInfo('Dose Cube ''%s'' of unknown type for DICOM. Not exported!\n',doseName); continue; @@ -174,6 +168,8 @@ metaCube = meta; metaCube.DoseType = doseType; metaCube.DoseSummationType = deliveryType; + metaCube.DoseComment = doseName; + metaCube.DoseUnits = doseUnits; %ID of the RTDose metaCube.SeriesInstanceUID = dicomuid; diff --git a/matRad/dicom/@matRad_DicomImporter/matRad_DicomImporter.m b/matRad/dicom/@matRad_DicomImporter/matRad_DicomImporter.m index c870cb100..1c3368182 100644 --- a/matRad/dicom/@matRad_DicomImporter/matRad_DicomImporter.m +++ b/matRad/dicom/@matRad_DicomImporter/matRad_DicomImporter.m @@ -26,7 +26,8 @@ % lists of all files allfiles; - patient; + patients; + selectedPatient; importFiles; % all the names (directories) of files, that will be imported % properties with data for import functions diff --git a/matRad/dicom/@matRad_DicomImporter/matRad_importDicom.m b/matRad/dicom/@matRad_DicomImporter/matRad_importDicom.m index 2d209052b..2dcdba1ee 100644 --- a/matRad/dicom/@matRad_DicomImporter/matRad_importDicom.m +++ b/matRad/dicom/@matRad_DicomImporter/matRad_importDicom.m @@ -189,7 +189,9 @@ function matRad_importDicom(obj) obj.resultGUI.w = [obj.resultGUI.w; [obj.stf(i).ray.weight]']; end end - +if any(ishandle(h)) + close(h) +end %% put ct, cst, pln, stf, resultGUI to the workspace ct = obj.ct; cst = obj.cst; diff --git a/matRad/dicom/@matRad_DicomImporter/matRad_importDicomRTDose.m b/matRad/dicom/@matRad_DicomImporter/matRad_importDicomRTDose.m index 52672fab6..93f49a979 100644 --- a/matRad/dicom/@matRad_DicomImporter/matRad_importDicomRTDose.m +++ b/matRad/dicom/@matRad_DicomImporter/matRad_importDicomRTDose.m @@ -62,11 +62,24 @@ else doseInstanceHelper = []; end - + + doseComment = ''; + if isfield(dose.(itemName).dicomInfo,'DoseComment') + doseComment = dose.(itemName).dicomInfo.DoseComment; + end + if strncmpi(doseTypeHelper,'PHYSICAL',6) - doseTypeHelper = 'physicalDose'; + if any(strcmp(doseComment,{'physicalDose','LET'})) + doseTypeHelper = doseComment; + else + doseTypeHelper = 'physicalDose'; + end elseif strncmpi(doseTypeHelper,'EFFECTIVE',6) - doseTypeHelper = 'RBExDose'; + if any(strcmp(doseComment,{'RBExDose','BED','alpha','beta','effect','RBE'})) + doseTypeHelper = doseComment; + else + doseTypeHelper = 'RBExDose'; + end end %If given as plan and not per fraction diff --git a/matRad/dicom/@matRad_DicomImporter/matRad_interpDicomDoseCube.m b/matRad/dicom/@matRad_DicomImporter/matRad_interpDicomDoseCube.m index 516eedc46..0fd4847f5 100644 --- a/matRad/dicom/@matRad_DicomImporter/matRad_interpDicomDoseCube.m +++ b/matRad/dicom/@matRad_DicomImporter/matRad_interpDicomDoseCube.m @@ -102,7 +102,9 @@ %always given obj.importRTDose.dose.dicomInfo.SOPClassUID = doseInfo.SOPClassUID; obj.importRTDose.dose.dicomInfo.SOPInstanceUID = doseInfo.SOPInstanceUID; -obj.importRTDose.dose.dicomInfo.ReferencedRTPlanSequence = doseInfo.ReferencedRTPlanSequence; +if isfield(doseInfo,'ReferencedRTPlanSequence') + obj.importRTDose.dose.dicomInfo.ReferencedRTPlanSequence = doseInfo.ReferencedRTPlanSequence; +end end diff --git a/matRad/dicom/@matRad_DicomImporter/matRad_scanDicomImportFolder.m b/matRad/dicom/@matRad_DicomImporter/matRad_scanDicomImportFolder.m index ab4d3c376..b5874a40b 100644 --- a/matRad/dicom/@matRad_DicomImporter/matRad_scanDicomImportFolder.m +++ b/matRad/dicom/@matRad_DicomImporter/matRad_scanDicomImportFolder.m @@ -246,9 +246,9 @@ close(h) if ~isempty(obj.allfiles) - obj.patient = unique(obj.allfiles(:,3)); + obj.patients = unique(obj.allfiles(:,3)); - if isempty(obj.patient) + if isempty(obj.patients) matRad_cfg.dispError('No patient found with DICOM CT _and_ RT structure set in patient directory!'); end else diff --git a/matRad/doseCalc/+DoseEngines/@matRad_DoseEngineBase/getAvailableEngines.m b/matRad/doseCalc/+DoseEngines/@matRad_DoseEngineBase/getAvailableEngines.m index af42a7a51..44582f171 100644 --- a/matRad/doseCalc/+DoseEngines/@matRad_DoseEngineBase/getAvailableEngines.m +++ b/matRad/doseCalc/+DoseEngines/@matRad_DoseEngineBase/getAvailableEngines.m @@ -36,7 +36,21 @@ %Get available, valid classes through call to matRad helper function %for finding subclasses -availableDoseEngines = matRad_findSubclasses('DoseEngines.matRad_DoseEngineBase','packages',{'DoseEngines'},'folders',optionalPaths,'includeAbstract',false); +persistent allAvailableDoseEngines lastOptionalPaths + +%First we do a sanity check if persistently stored metaclasses are valid +if ~matRad_cfg.isOctave && ~isempty(allAvailableDoseEngines) && ~all(cellfun(@isvalid,allAvailableDoseEngines)) + matRad_cfg.dispWarning('Found invalid Dose Engines, updating engine cache.'); + allAvailableDoseEngines = []; +end + +%Check if we need to find the engines and if yes, do +if isempty(allAvailableDoseEngines) || (~isempty(lastOptionalPaths) && ~isequal(lastOptionalPaths, optionalPaths)) + lastOptionalPaths = optionalPaths; + allAvailableDoseEngines = matRad_findSubclasses('DoseEngines.matRad_DoseEngineBase','packages',{'DoseEngines'},'folders',optionalPaths,'includeAbstract',false); +end + +availableDoseEngines = allAvailableDoseEngines; %Now filter for pln ix = []; diff --git a/matRad/doseCalc/+DoseEngines/@matRad_DoseEngineBase/matRad_DoseEngineBase.m b/matRad/doseCalc/+DoseEngines/@matRad_DoseEngineBase/matRad_DoseEngineBase.m index b0fea5001..1fcd1da6c 100644 --- a/matRad/doseCalc/+DoseEngines/@matRad_DoseEngineBase/matRad_DoseEngineBase.m +++ b/matRad/doseCalc/+DoseEngines/@matRad_DoseEngineBase/matRad_DoseEngineBase.m @@ -114,6 +114,11 @@ function warnDeprecatedEngineProperty(this,oldProp,msg,newProp) function assignPropertiesFromPln(this,pln,warnWhenPropertyChanged) matRad_cfg = MatRad_Config.instance(); + %Check radiation mode + if ~isfield(pln,'radiationMode') || ~any(strcmp(pln.radiationMode,this.possibleRadiationModes)) + matRad_cfg.dispError('Invalid radiation mode for engine ''%s''!',this.name); + end + %Set Scenario Model if isfield(pln,'multScen') this.multScen = pln.multScen; @@ -285,17 +290,51 @@ function assignBioModelPropertiesFromPln(this, plnModel, warnWhenPropertyChanged if ~isa(this.multScen,'matRad_ScenarioModel') this.multScen = matRad_ScenarioModel.create(this.multScen,struct('numOfCtScen',ct.numOfCtScen)); end - + for i = 1:this.multScen.totNumScen scenSubIx = this.multScen.linearMask(i,:); resultGUItmp = matRad_calcCubes(ones(dij.numOfBeams,1),dij,this.multScen.sub2scenIx(scenSubIx(1),scenSubIx(2),scenSubIx(3))); if i == 1 resultGUI = resultGUItmp; end - resultGUI = matRad_appendResultGUI(resultGUI,resultGUItmp,false,sprintf('scen%d',i)); + if isvector(this.multScen.scenMask) && this.multScen.numOfCtScen>1%ctScen + resultGUI.phaseDose{i} = resultGUItmp.physicalDose; + for beamIx = 1:dij.numOfBeams + resultGUI.(['phaseDose_beam', num2str(beamIx)]){i} = resultGUItmp.(['physicalDose_beam', num2str(beamIx)]); + end + if isfield(resultGUItmp, 'alphaDoseCube') && isfield(resultGUItmp, 'SqrtBetaDoseCube') + resultGUI.phaseAlphaDose{i} = resultGUItmp.alpha .* resultGUItmp.physicalDose; + resultGUI.phaseSqrtBetaDose{i} = sqrt(resultGUItmp.beta) .* resultGUItmp.physicalDose; + resultGUI.phaseRBExDose{i} = resultGUItmp.RBExDose; + for beamIx = 1:dij.numOfBeams + resultGUI.(['phaseAlphaDose_beam', num2str(beamIx)]){i} = resultGUItmp.(['alpha_beam', num2str(beamIx)]).*resultGUItmp.(['physicalDose_beam', num2str(beamIx)]); + resultGUI.(['phaseSqrtBetaDose_beam', num2str(beamIx)]){i} = sqrt(resultGUItmp.(['beta_beam', num2str(beamIx)])).*resultGUItmp.(['physicalDose_beam', num2str(beamIx)]); + resultGUI.(['phaseRBExDose_beam', num2str(beamIx)]){i} = resultGUItmp.(['RBExDose_beam', num2str(beamIx)]); + end + elseif isfield(resultGUItmp,'RBExDose') + resultGUI.phaseRBExDose{i} = resultGUItmp.RBExDose; + for beamIx = 1:dij.numOfBeams + resultGUI.(['phaseRBExDose_beam', num2str(beamIx)]){i} = resultGUItmp.(['RBExDose_beam', num2str(beamIx)]); + end + end + else + if this.multScen.totNumScen > 1 + resultGUI = matRad_appendResultGUI(resultGUI,resultGUItmp,false,sprintf('scen%d',i)); + end + end + end + + if isfield(dij,'w') + resultGUI.w = dij.w; + else + resultGUI.w = w; + end + + if isfield(dij,'MU') + resultGUI.MU = dij.MU; end - resultGUI.w = w; + resultGUI = orderfields(resultGUI); end function dij = calcDoseInfluence(this,ct,cst,stf) diff --git a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/calcDose.m b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/calcDose.m new file mode 100644 index 000000000..39a188e23 --- /dev/null +++ b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/calcDose.m @@ -0,0 +1,441 @@ +function dij = calcDose(this,ct,cst,stf) +% Function to forward dose calculation to FRED and inport the results +% in matRad +% +% call +% dij = this.calcDose(ct,stf,pln,cst) +% +% input +% ct: matRad ct struct +% cst: matRad cst struct +% stf: atRad steering information struct +% +% output +% dij: matRad dij struct +% +% References +% +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2019 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSES.txt. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + matRad_cfg = MatRad_Config.instance(); + + currFolder = pwd; + cd(this.FREDrootFolder); + + %Now we can run initDoseCalc as usual + dij = this.initDoseCalc(ct,cst,stf); + + % Interpolate cube on dose grid + HUcube{1} = matRad_interp3(dij.ctGrid.x, dij.ctGrid.y', dij.ctGrid.z,ct.cubeHU{1}, ... + dij.doseGrid.x,dij.doseGrid.y',dij.doseGrid.z,'linear'); + + % Force HU clamping if values are found outside of available range + switch this.HUtable + case 'internal' + if any(HUcube{1}(:)>this.hLutLimits(2)) || any(HUcube{1}(:) Dose cube coordinate system places the first coordinate point (aka resolution) in + % the center of the first voxel. So the "zero" of the cube + % coordinate system is 0.5*resolution outside of the cube surface. + isoInDoseGridCoord = matRad_world2cubeCoords(stf(i).isoCenter,this.doseGrid); + + % Coordinate of the first voxel in cube system is in the center of the first voxel and is equal to resolution. Thus the zero + % of the cube coordinate system is 0.5*resolution before the surface of + % the phantom. The surface thus starts at 1/2 resolution wrt zero of that system + fredCubeSurfaceInDoseCubeCoords = 0.5*doseGridResolution; + + % Get coordinates of pivot point in FRED cube (center of + % geometrical cube) in dose cube coordinates. This is the distance + % between pivot point and the surface + the position of the + % surface in the cube coord. system. + fredPivotInCubeCoordinates = 0.5*this.doseGrid.dimensions([2 1 3]).*doseGridResolution + fredCubeSurfaceInDoseCubeCoords; + + + % Define the FRED isocenter as the distance between the pivot point + % and the matRad isocenter in the cube coordinate system. + stfFred(i).isoCenter = -(fredPivotInCubeCoordinates - isoInDoseGridCoord); + + % First coordinate is flipped + stfFred(i).isoCenter = stfFred(i).isoCenter.*[-1 1 1]; + + % NOTE on the coordinate system. + % FRED places the pivot point of the component at the center of the + % FRED coordinate system, then applies a translation s.t. the FRED + % isocenter (defined for each field) is in the center of the FRED + % coordinate system. Then applies rotations. This way everything + % is defined in BEV reference. + + nominalEnergies = unique([stf(i).ray.energy]); + [~,nominalEnergiesIdx] = intersect([this.machine.data.energy],nominalEnergies); + + energyIdxInEmittance = ismember(emittanceBaseData.energyIndex, nominalEnergiesIdx); + monteCarloBaseData = emittanceBaseData.monteCarloData(energyIdxInEmittance); + + stfFred(i).nominalEnergies = nominalEnergies; + stfFred(i).energies = [monteCarloBaseData.MeanEnergy].*this.numOfNucleons.*this.primaryMass; % Note for generic baseData: the kernels were simulated with equivalent of primaryMass = 1 + stfFred(i).energySpread = [monteCarloBaseData.EnergySpread]; + stfFred(i).energySpreadMeV = [monteCarloBaseData.EnergySpread].*[monteCarloBaseData.MeanEnergy]/100; + stfFred(i).FWHMs = 2.355*[monteCarloBaseData.SpotSize1x]; + + stfFred(i).energySpreadFWHMMev = 2.355*stfFred(i).energySpreadMeV; + stfFred(i).BAMStoIsoDist = emittanceBaseData.nozzleToIso; + + % Select the parametrs for source model + switch this.sourceModel + + case 'gaussian' + + case 'emittance' + stfFred(i).emittanceX = []; + stfFred(i).twissBetaX = []; + stfFred(i).twissAlphaX = []; + stfFred(i).emittanceRefPlaneDistance = []; + + % Need to get the parameters for the model from MCemittance + for eIdx=emittanceBaseData.energyIndex' + % Only using first focus index for now + tmpOpticsData = emittanceBaseData.fitBeamOpticsForEnergy(eIdx,1); + stfFred(i).emittanceX = [stfFred(i).emittanceX, tmpOpticsData.twissEpsilonX]; + stfFred(i).twissBetaX = [stfFred(i).twissBetaX, tmpOpticsData.twissBetaX]; + stfFred(i).twissAlphaX = [stfFred(i).twissAlphaX, tmpOpticsData.twissAlphaX]; + stfFred(i).emittanceRefPlaneDistance = [stfFred(i).emittanceRefPlaneDistance, this.machine.meta.BAMStoIsoDist]; + end + + case 'sigmaSqrModel' + stfFred(i).sSQr_a = []; + stfFred(i).sSQr_b = []; + stfFred(i).sSQr_c = []; + + for eIdx=emittanceBaseData.energyIndex' + tmpOpticsData = emittanceBaseData.fitBeamOpticsForEnergy(eIdx,1); + stfFred(i).sSQr_a = [stfFred(i).sSQr_a, tmpOpticsData.sSQ_a]; + stfFred(i).sSQr_b = [stfFred(i).sSQr_b, tmpOpticsData.sSQ_b]; + stfFred(i).sSQr_c = [stfFred(i).sSQr_c, tmpOpticsData.sSQ_c]; + end + otherwise + matRad_cfg.dispWarning('Unrecognized source model, setting gaussian'); + + end + + % Allocate empty layer container + % Rearrange info into separate energy layers + for j = 1:numel(stfFred(i).energies) + + %stfFred(i).energyLayer(j).targetPoints = []; + stfFred(i).energyLayer(j).numOfPrimaries = []; + stfFred(i).energyLayer(j).rayNum = []; + stfFred(i).energyLayer(j).bixelNum = []; + stfFred(i).energyLayer(j).rayDivX = []; + stfFred(i).energyLayer(j).rayDivY = []; + stfFred(i).energyLayer(j).rayPosX = []; + stfFred(i).energyLayer(j).rayPosY = []; + end + + for j = 1:stf(i).numOfRays + for k = 1:stf(i).numOfBixelsPerRay(j) + counter = counter + 1; + dij.beamNum(counter,1) = i; + dij.rayNum(counter,1) = j; + dij.bixelNum(counter,1) = k; + end + + for k = 1:numel(stfFred(i).energies) + + if any(stf(i).ray(j).energy == stfFred(i).nominalEnergies(k)) + stfFred(i).energyLayer(k).rayNum = [stfFred(i).energyLayer(k).rayNum j]; + + stfFred(i).energyLayer(k).bixelNum = [stfFred(i).energyLayer(k).bixelNum ... + find(stf(i).ray(j).energy == stfFred(i).nominalEnergies(k))]; + + % Get spot position and divergence + targetX = stf(i).ray(j).targetPoint_bev(1); + targetY = stf(i).ray(j).targetPoint_bev(3); + + % Stf.ray.rayPos_bev is position of ray at the + % IsoCenter plane. + sourceX = stf(i).ray(j).rayPos_bev(1); + sourceY = stf(i).ray(j).rayPos_bev(3); + + distance = stf(i).ray(j).targetPoint_bev(2) - stf(i).ray(j).rayPos_bev(2); + + divergenceX = (targetX - sourceX)/distance; + divergenceY = (targetY - sourceY)/distance; + + %stfFred(i).energyLayer(k).targetPoints = [stfFred(i).energyLayer(k).targetPoints; -targetX targetY]; + + % This is position of the spot at -BAMsToIso distance + % (zero is at IsoCenter depth). + stfFred(i).energyLayer(k).rayPosX = [stfFred(i).energyLayer(k).rayPosX, getPointAtBAMS(targetX,sourceX,distance,stfFred(i).BAMStoIsoDist)]; + stfFred(i).energyLayer(k).rayPosY = [stfFred(i).energyLayer(k).rayPosY, getPointAtBAMS(targetY,sourceY,distance,stfFred(i).BAMStoIsoDist)]; + + stfFred(i).energyLayer(k).rayDivX = [stfFred(i).energyLayer(k).rayDivX, divergenceX]; + stfFred(i).energyLayer(k).rayDivY = [stfFred(i).energyLayer(k).rayDivY, divergenceY]; + + + if this.calcDoseDirect + % Set the bixel weight + stfFred(i).energyLayer(k).numOfPrimaries = [stfFred(i).energyLayer(k).numOfPrimaries ... + stf(i).ray(j).weight(stf(i).ray(j).energy == stfFred(i).nominalEnergies(k))]; + else + stfFred(i).energyLayer(k).numOfPrimaries = [stfFred(i).energyLayer(k).numOfPrimaries, 1]; + end + + end + + end + end + + %FRED works in cm + stfFred(i).isoCenter = stfFred(i).isoCenter/10; + stfFred(i).BAMStoIsoDist = stfFred(i).BAMStoIsoDist/10; + + switch this.sourceModel + case 'gaussian' + stfFred(i).FWHMs = stfFred(i).FWHMs/10; + case 'emittance' + stfFred(i).emittanceRefPlaneDistance = stfFred(i).emittanceRefPlaneDistance/10; + case 'sigmaSqrModel' + + end + + stfFred(i).totalNumOfBixels = stf(i).totalNumOfBixels; + for j=1:numel(stfFred(i).nominalEnergies) + stfFred(i).energyLayer(j).rayPosX = stfFred(i).energyLayer(j).rayPosX/10; + stfFred(i).energyLayer(j).rayPosY = stfFred(i).energyLayer(j).rayPosY/10; + stfFred(i).energyLayer(j).nBixels = numel(stfFred(i).energyLayer(j).bixelNum); + + if this.calcDoseDirect + stfFred(i).energyLayer(j).numOfPrimaries = this.conversionFactor*stfFred(i).energyLayer(j).numOfPrimaries; + end + end + end + + counterFred = 0; + fredOrder = NaN * ones(dij.totalNumOfBixels,1); + for i = 1:length(stf) + for j = 1:numel(stfFred(i).nominalEnergies) + for k = 1:numel(stfFred(i).energyLayer(j).numOfPrimaries) + counterFred = counterFred + 1; + ix = find(i == dij.beamNum & ... + stfFred(i).energyLayer(j).rayNum(k) == dij.rayNum & ... + stfFred(i).energyLayer(j).bixelNum(k) == dij.bixelNum); + + fredOrder(ix) = counterFred; + end + end + end + + if any(isnan(fredOrder)) + matRad_cfg.dispError('Invalid ordering of Beamlets for FRED computation!'); + end + + % %% MC computation and dij filling + this.writeFredInputAllFiles(stfFred); + + switch this.externalCalculation + + case 'write' % Write simulation files for external calculation (no FRED installation required) + + matRad_cfg.dispInfo('All files have been generated\n'); + dijFieldsToOverride = {'numOfBeams','beamNum','bixelNum','rayNum','totalNumOfBixels','totalNumOfRays','numOfRaysPerBeam'}; + + for fieldName=dijFieldsToOverride + dij.(fieldName{1}) = this.numOfColumnsDij; + end + + doseCube = []; + + case 'off' % Run FRED simulation (requires installation) + + % Check consistency of installation + if this.checkExec() + + matRad_cfg.dispInfo('calling FRED'); + + cd(this.MCrunFolder); + + systemCall = [this.cmdCall, '-f fred.inp']; + if ~this.useGPU + systemCall = [this.cmdCall, ' -nogpu -f fred.inp']; + end + + % printOutput to matlab console + if this.printOutput + [status,~] = system(systemCall,'-echo'); + else + [status,~] = system(systemCall); + end + cd(this.FREDrootFolder); + else + matRad_cfg.dispError('FRED setup incorrect for this plan simulation'); + end + + if status==0 + matRad_cfg.dispInfo(' done\n'); + end + + % read simulation output + [doseCube, letdCube] = this.readSimulationOutput(this.MCrunFolder,this.calcDoseDirect, 'calcLET', logical(this.calcLET), 'readFunctionHandle', this.dijReaderHandle); + + otherwise % A path for loading has been provided + + matRad_cfg.dispInfo(['Reading simulation data from: ', strrep(this.MCrunFolder,'\','\\'), '\n']); + + % read simulation output + [doseCube, letdCube, loadFileName] = this.readSimulationOutput(this.MCrunFolder,this.calcDoseDirect, 'calcLET',logical(this.calcLET),'readFunctionHandle', this.dijReaderHandle); + + dij.externalCalculationLodPath = loadFileName; + + end + + if ~isempty(doseCube) + + % Fill dij + if this.calcDoseDirect + % Dose cube + if isequal(size(doseCube), this.doseGrid.dimensions) + dij.physicalDose{1} = sparse(this.VdoseGrid, ones(numel(this.VdoseGrid),1),doseCube(this.VdoseGrid), this.doseGrid.numOfVoxels,1); + end + + % LETd cube + if this.calcLET + if isequal(size(letdCube), this.doseGrid.dimensions) + dij.mLETd{1} = sparse(this.VdoseGrid, ones(numel(this.VdoseGrid),1),letdCube(this.VdoseGrid)./10, this.doseGrid.numOfVoxels,1); + + % We need LETd * dose as well + dij.mLETDose{1} = sparse(this.VdoseGrid, ones(numel(this.VdoseGrid),1),(letdCube(this.VdoseGrid)./10).*doseCube(this.VdoseGrid), this.doseGrid.numOfVoxels,1); + end + end + + % Needed for calcCubes + dijFieldsToOverride = {'numOfBeams','beamNum','bixelNum','rayNum','totalNumOfBixels','totalNumOfRays','numOfRaysPerBeam'}; + + for fieldName=dijFieldsToOverride + dij.(fieldName{1}) = 1; + end + + else + % Dose cube + if isequal(size(doseCube), [dij.doseGrid.numOfVoxels,dij.totalNumOfBixels]) + %When scoring dij, FRED internaly normalizes to 1 + dij.physicalDose{1}(this.VdoseGrid,:) = this.conversionFactor*doseCube(this.VdoseGrid,fredOrder); + end + + % LET cube + if this.calcLET + if isequal(size(letdCube), [dij.doseGrid.numOfVoxels,dij.totalNumOfBixels]) + % Need to divide by 10, FRED scores in MeV * cm^2 / g + dij.mLETd{1}(this.VdoseGrid,:) = letdCube(this.VdoseGrid,fredOrder)./10; + end + + % We need LETd * dose as well + dij.mLETDose{1} = sparse(dij.physicalDose{1}.*dij.mLETd{1}); + end + end + + + % Calc Biological quantities + if this.calcBioDose + % recover alpha and beta maps + tmpBixel.radDepths = zeros(size(this.VdoseGrid,1),1); + + tmpBixel.vAlphaX = dij.ax{1}(this.VdoseGrid); + tmpBixel.vBetaX = dij.bx{1}(this.VdoseGrid); + tmpBixel.vABratio = dij.ax{1}(this.VdoseGrid)./dij.bx{1}(this.VdoseGrid); + + if this.calcDoseDirect + tmpKernel.LET = dij.mLETd{1}(this.VdoseGrid); + + tmpBixel = this.bioModel.calcBiologicalQuantitiesForBixel(tmpBixel,tmpKernel); + + tmpBixel.alpha(isnan(tmpBixel.alpha)) = 0; + tmpBixel.beta(isnan(tmpBixel.beta)) = 0; + + dij.mAlphaDose{1} = sparse(this.VdoseGrid, ones(numel(this.VdoseGrid),1),tmpBixel.alpha.*dij.physicalDose{1}(this.VdoseGrid), this.doseGrid.numOfVoxels,1); + dij.mSqrtBetaDose{1} = sparse(this.VdoseGrid, ones(numel(this.VdoseGrid),1),sqrt(tmpBixel.beta).*dij.physicalDose{1}(this.VdoseGrid), this.doseGrid.numOfVoxels,1); + else + % Loop over all bixels + for bxlIdx = 1:dij.totalNumOfBixels + bixelLET = full(dij.mLETd{1}(:,bxlIdx)); + tmpKernel.LET = bixelLET(this.VdoseGrid); + + tmpBixel = this.bioModel.calcBiologicalQuantitiesForBixel(tmpBixel,tmpKernel); + + tmpBixel.alpha(isnan(tmpBixel.alpha)) = 0; + tmpBixel.beta(isnan(tmpBixel.beta)) = 0; + + dij.mAlphaDose{1}(:,bxlIdx) = sparse(this.VdoseGrid, ones(numel(this.VdoseGrid),1),tmpBixel.alpha.*dij.physicalDose{1}(this.VdoseGrid,bxlIdx), this.doseGrid.numOfVoxels,1); + dij.mSqrtBetaDose{1}(:,bxlIdx) = sparse(this.VdoseGrid, ones(numel(this.VdoseGrid),1),sqrt(tmpBixel.beta).*dij.physicalDose{1}(this.VdoseGrid,bxlIdx), this.doseGrid.numOfVoxels,1); + end + end + end + end + + dij = this.finalizeDose(dij); + + cd(currFolder); +end \ No newline at end of file diff --git a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/matRad_ParticleFREDEngine.m b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/matRad_ParticleFREDEngine.m new file mode 100644 index 000000000..ddc10e684 --- /dev/null +++ b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/matRad_ParticleFREDEngine.m @@ -0,0 +1,877 @@ +classdef matRad_ParticleFREDEngine < DoseEngines.matRad_MonteCarloEngineAbstract +% Engine for particle dose calculation using FRED Monte Carlo algorithm +% for more informations see superclass +% DoseEngines.matRad_MonteCarloEngineAbstract +% +% +% The following parameters for the FRED engine can be tuned by the user. In +% order to do so, specify the desired value in: pln.propDoseCalc. +% [s]: string/character array +% [b]: boolean +% [i]: integer +% [f]: float/double/any non strictly integer number +% +% +% HUclamping: [b] allows for clamping of HU table. Default: true +% HUtable: [s] HU table name. Example: 'internal', 'matRad_default_FRED' +% externalCalculation [b/s] off (default): run FRED +% t/'write' : Only write simulation paramter files +% 'path' : read simulation files from 'path' +% +% sourceModel [s] see AvailableSourceModels, {'gaussian', 'emittance', 'sigmaSqrModel'} +% useGPU [b] trigger use of GPU (if available) +% roomMaterial [s] material of the patient surroundings. Example: +% 'vacuum', 'Air' +% printOutput [b] 't: FRED output is mirrored to Matlab console, f: no output is printed' +% numHistoriesDirect [i] +% numHistoriesPerBeamlet [i] +% scorers [c] cell array with specified scorers. Example: +% 'Dose', 'LETd' +% primaryMass [f] mass of the primary ion (in Da). Default value for +% protons: 1.0727 +% numOfNucleons [i] number of nucleons. Default for protons: 1 + +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2023 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + properties (Constant) + possibleRadiationModes = {'protons'}; + name = 'FRED'; + shortName = 'FRED'; + end + + properties (SetAccess = protected, GetAccess = public) + + defaultHUtable = 'matRad_default_FredMaterialConverter'; + AvailableSourceModels = {'gaussian', 'emittance', 'sigmaSqrModel'}; + defaultDijFormatVersion = '20'; + + calcBioDose; + currentVersion; + availableVersions = {'3.70.0'}; % Or higher. + radiationMode; + + end + + properties + dijFormatVersion; + externalCalculation; + useGPU; + calcLET; + constantRBE; + HUclamping; + scorers; + HUtable; + sourceModel; + roomMaterial; + printOutput; + primaryMass; + numOfNucleons; + ignoreOutsideDensities; + end + + + properties (SetAccess = private, Hidden) + patientFilename = 'CTpatient.mhd'; + runInputFilename = 'fred.inp'; + regionsFilename = 'regions.inp'; + funcsFilename = 'funcs.inp'; + planFilename = 'plan.inp'; + fieldsFilename = 'fields.inp'; + layersFilename = 'layers.inp'; + beamletsFilename = 'beamlets.inp'; + planDeliveryFilename = 'planDelivery.inp'; + + hLutLimits = [-1000,1375]; % Default FRED values + + conversionFactor = 1e6; % Used to scale the FRED dose to matRad normalization + + FREDrootFolder; + + MCrunFolder; + inputFolder; + regionsFolder; + planFolder; + dijReaderHandle; + end + + methods + function this = matRad_ParticleFREDEngine(pln) + % Constructor + % + % call + % engine = DoseEngines.matRad_DoseEngineFRED(ct,stf,pln,cst) + % + + matRad_cfg = MatRad_Config.instance(); + if nargin < 1 + pln = []; + end + + % call superclass constructor + this = this@DoseEngines.matRad_MonteCarloEngineAbstract(pln); + + if nargin > 0 + if isfield(pln, 'radiationMode') + this.radiationMode = pln.radiationMode; + end + end + + if isempty(this.FREDrootFolder) + this.FREDrootFolder = fullfile(matRad_cfg.primaryUserFolder, 'FRED'); + end + + if ~exist(this.FREDrootFolder, 'dir') + mkdir(this.FREDrootFolder); + matRad_cfg.dispWarning('FRED root folder not found, this should not happen!'); + end + + end + + end + + methods(Access = protected) + + dij = calcDose(this,ct,cst,stf) + + function dij = initDoseCalc(this,ct,cst,stf) + + matRad_cfg = MatRad_Config.instance(); + + dij = initDoseCalc@DoseEngines.matRad_MonteCarloEngineAbstract(this,ct,cst,stf); + + dij = this.allocateQuantityMatrixContainers(dij, {'physicalDose'}); + + %Issue a warning when we have more than 1 scenario + if dij.numOfScenarios ~= 1 + matRad_cfg.dispWarning('FRED is only implemented for single scenario use at the moment. Will only use the first Scenario for Monte Carlo calculation!'); + end + + % Check for model consistency + if ~isempty(this.bioModel) && isa(this.bioModel, 'matRad_LQLETbasedModel') + this.calcBioDose = true; + else + this.calcBioDose = 0; + end + + % Limit RBE calculation to proton models for the time being + if this.calcBioDose + + switch this.radiationMode + + case 'protons' + + dij = this.loadBiologicalData(cst,dij); + dij = this.allocateQuantityMatrixContainers(dij,{'mAlphaDose', 'mSqrtBetaDose'}); + + % Only considering LET based models + this.calcLET = true; + otherwise + matRad_cfg.dispWarning('biological dose calculation not supported for radiation modality: %s', this.radiationMode); + this.calcBioDose = false; + end + end + + if isa(this.bioModel, 'matRad_ConstantRBE') + dij.RBE = this.bioModel.RBE; + end + + if this.calcLET + this.scorers = [this.scorers, {'LETd'}]; + % Allocate containers for both LET*Dose and dose weighted + % LET. This last is used for biological calculation as well + dij = this.allocateQuantityMatrixContainers(dij, {'mLETDose', 'mLETd'}); + end + + end + + function writeTreeDirectory(this) + + if ~exist(this.MCrunFolder, 'dir') + mkdir(this.MCrunFolder); + end + + % write input folder + if ~exist(this.inputFolder, 'dir') + mkdir(this.inputFolder); + end + + % build MCrun/inp/regions + if ~exist(this.regionsFolder, 'dir') + mkdir(this.regionsFolder); + end + + % build MCrun/inp/plan + if ~exist(this.planFolder, 'dir') + mkdir(this.planFolder); + end + end + + + %% Write files functions + + writeRunFile(~, fName) + + writeRegionsFile(this,fName, stf) + + writePlanDeliveryFile(this, fName, stf) + + writePlanFile(this,fName, stf) + + function writeFredInputAllFiles(this,stf) + + %write fred.inp file + runFilename = fullfile(this.MCrunFolder, this.runInputFilename); + this.writeRunFile(runFilename); + + %write region/region.inp file + regionFilename = fullfile(this.regionsFolder, this.regionsFilename); + this.writeRegionsFile(regionFilename); + + %write plan file + planFile = fullfile(this.planFolder, this.planFilename); + this.writePlanFile(planFile,stf); + + %write planDelivery file + + planDeliveryFile = fullfile(this.planFolder,this.planDeliveryFilename); + this.writePlanDeliveryFile(planDeliveryFile); + end + + function dij = loadBiologicalData(this,cst,dij) + matRad_cfg = MatRad_Config.instance(); + + matRad_cfg.dispInfo('Initializing biological dose calculation...\n'); + + dij.ax = zeros(dij.doseGrid.numOfVoxels,1); + dij.bx = zeros(dij.doseGrid.numOfVoxels,1); + + cstDownsampled = matRad_setOverlapPriorities(cst); + + % resizing cst to dose cube resolution + cstDownsampled = matRad_resizeCstToGrid(cstDownsampled,dij.ctGrid.x,dij.ctGrid.y,dij.ctGrid.z,... + dij.doseGrid.x,dij.doseGrid.y,dij.doseGrid.z); + + % retrieve photon LQM parameter for the current dose grid voxels + [dij.ax,dij.bx] = matRad_getPhotonLQMParameters(cstDownsampled,dij.doseGrid.numOfVoxels,this.VdoseGrid); + + end + + + function dij = allocateQuantityMatrixContainers(this,dij,names) + + % if this.calcDoseDirect + % numOfBixelsContainer = 1; + % else + % numOfBixelsContainer = dij.totalNumOfBixels; + % end + + %Loop over all requested quantities + for n = 1:numel(names) + + dij.(names{n}) = cell(size(this.multScen.scenMask)); + + %Now preallocate a matrix in each active scenario using the + %scenmask + if this.calcDoseDirect + dij.(names{n})(this.multScen.scenMask) = {zeros(dij.doseGrid.numOfVoxels,this.numOfColumnsDij)}; + else + %We preallocate a sparse matrix with sparsity of + %1e-3 to make the filling slightly faster + %TODO: the preallocation could probably + %have more accurate estimates + dij.(names{n})(this.multScen.scenMask) = {spalloc(dij.doseGrid.numOfVoxels,this.numOfColumnsDij,round(prod(dij.doseGrid.numOfVoxels,this.numOfColumnsDij)*1e-3))}; + end + end + + end + end + + methods + + function setDefaults(this) + setDefaults@DoseEngines.matRad_MonteCarloEngineAbstract(this); + + matRad_cfg = MatRad_Config.instance(); + + this.HUclamping = false; + this.HUtable = this.defaultHUtable; + this.externalCalculation = 'off'; + this.useGPU = false; + this.sourceModel = this.AvailableSourceModels{1}; + this.roomMaterial = 'Air'; + this.printOutput = true; + this.numHistoriesDirect = matRad_cfg.defaults.propDoseCalc.numHistoriesDirect; + this.numHistoriesPerBeamlet = matRad_cfg.defaults.propDoseCalc.numHistoriesPerBeamlet; + this.scorers = {'Dose'}; + this.primaryMass = 1.00727; + this.numOfNucleons = 1; + this.outputMCvariance = false; + this.constantRBE = NaN; + this.ignoreOutsideDensities = false; + this.dijFormatVersion = this.defaultDijFormatVersion; + + end + + function writeHlut(this,hLutFile) + + matRad_cfg = MatRad_Config.instance(); + fileName = fullfile(this.regionsFolder, 'hLut.inp'); + + mainFolder = fullfile(matRad_cfg.matRadSrcRoot,'hluts'); + userDefinedFolder = fullfile(matRad_cfg.primaryUserFolder, 'hluts'); + fredDefinedFolder = fullfile(matRad_cfg.matRadSrcRoot, 'doseCalc', 'FRED', 'hluts'); + + % Collect all the subfolders + + searchPath = [strsplit(genpath(mainFolder), pathsep)';... + strsplit(genpath(userDefinedFolder), pathsep)';... + strsplit(genpath(fredDefinedFolder),pathsep)']; + + searchPath(cellfun(@isempty, searchPath)) = []; + + % Check for existence of folder paths + searchPath = searchPath(cellfun(@isfolder, searchPath)); + + availableHLUTs = cellfun(@(x) dir([x, filesep, '*.txt']), searchPath, 'UniformOutput',false); + availableHLUTs = cell2mat(availableHLUTs); + + hLUTindex = find(strcmp([hLutFile,'.txt'], {availableHLUTs.name})); + + if ~isempty(hLUTindex) + selectedHlutfile = fullfile(availableHLUTs(hLUTindex).folder, availableHLUTs(hLUTindex).name); + + template = fileread(selectedHlutfile); + + newLut = fopen(fileName, 'w'); + fprintf(newLut, template); + fclose(newLut); + else + + errString = sprintf('Cannot open hLut: %s. Available hLut files are: ',hLutFile); + errString = [errString, sprintf('\ninternal')]; + for hLUTindex=1:numel(availableHLUTs) + errString = [errString, sprintf('\n%s', strrep(fullfile(availableHLUTs(hLUTindex).folder, availableHLUTs(hLUTindex).name), '\', '\\'))]; + end + matRad_cfg.dispError(errString); + end + + end + + end + + methods (Static) + function cmdString = cmdCall(newCmdString) + persistent fredCmdCall; + if nargin > 0 + fredCmdCall = newCmdString; + elseif isempty(fredCmdCall) + if ispc +% fredCmdCall = 'wsl if [ -f ~/.fredenv.sh ] ; then source ~/.fredenv.sh ; fi; fred'; + fredCmdCall = 'fred '; + elseif isunix + fredCmdCall = 'if [ -f ~/.fredenv.sh ] ; then source ~/.fredenv.sh ; fi; fred'; + else + matRad_cfg = MatRad_Config.instance(); + matRad_cfg.dispError('OS not supported for FRED!'); + end + end + cmdString = fredCmdCall; + end + + function availableVersions = getAvailableVersions() + % Function to get available FRED version + + matRad_cfg = MatRad_Config.instance(); + + currCmdCall = DoseEngines.matRad_ParticleFREDEngine.cmdCall; + + availableVersions = []; + + [status, cmdOut] = system([currCmdCall, ' -listVers']); + + if status == 0 + nLidx = regexp(cmdOut, '\n')+6; %6 because of tab + nVersions = numel(nLidx)-1; + + for versIdx=1:nVersions + availableVersions = [availableVersions,{cmdOut(nLidx(versIdx):nLidx(versIdx)+5)}]; + end + + else + matRad_cfg.dispError('Something wrong occured in checking FRED available version. Please check correct FRED installation'); + end + end + + function [available,msg] = isAvailable(pln,machine) + % see superclass for information + + msg = []; + available = false; + + if nargin < 2 + machine = matRad_loadMachine(pln); + end + + %checkBasic + try + checkBasic = isfield(machine,'meta') && isfield(machine,'data'); + + %check modality + checkModality = any(strcmp(DoseEngines.matRad_ParticleFREDEngine.possibleRadiationModes, machine.meta.radiationMode)); + + 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 + + function execCheck = checkExec() + + matRad_cfg = MatRad_Config.instance(); + + %Check if I can obtain FRED version + try + ver = DoseEngines.matRad_ParticleFREDEngine.getVersion(); + if ~isempty(ver) + execCheck = true; + else + execCheck = false; + msg = sprintf('Couldn''t call FRED executable. Please set the correct system call with DoseEngines.matRad_ParticleFREDEngine.cmdCall(''path/to/executable''). Current value is ''%s''',DoseEngines.matRad_ParticleFREDEngine.cmdCall); + matRad_cfg.dispError(msg); + end + catch + execCheck = false; + msg = sprintf('Couldn''t call FRED executable. Please set the correct system call with DoseEngines.matRad_ParticleFREDEngine.cmdCall(''path/to/executable''). Current value is ''%s''',DoseEngines.matRad_ParticleFREDEngine.cmdCall); + matRad_cfg.dispError(msg); + end + end + + function version = getVersion() + % Function to get current default FRED version + matRad_cfg = MatRad_Config.instance(); + + try + [status, cmdOut] = system([DoseEngines.matRad_ParticleFREDEngine.cmdCall, ' -vn']); + + if status == 0 + % Extract the version number using a regular expression + versionMatch = regexp(cmdOut, '\d+\.\d+\.\d+', 'match', 'once'); + if ~isempty(versionMatch) + version = versionMatch; + else + matRad_cfg.dispError('Unable to parse FRED version from output. Please check FRED installation.'); + version = []; + end + else + matRad_cfg.dispError('Error occurred while checking FRED installation. Please check FRED installation.'); + version = []; + end + catch + matRad_cfg.dispWarning('Something wrong occured in checking FRED installation. Please check correct FRED installation'); + version = []; + end + end + + % end + + function dijMatrix = readSparseDijBin(fName) + % FRED function to read sparseDij in .bin format + % call + % readSparseDijBin(fName) + % + % input + % fName: filename to read + % + % output + % dijMatrix: dij structure + + matRad_cfg = MatRad_Config.instance(); + + f = fopen(fName,'r','l'); + + try + %Header + fileFormatVerison = fread(f,1,"int32"); + dims = fread(f,3,"int32"); + res = fread(f,3,"float32"); + offset = fread(f,3,"float32"); + nComponents = fread(f,1,"int32"); + numberOfBixels = fread(f,1,"int32"); + + values = []; + valuesDen = []; + voxelIndices = []; + colIndices = []; + valuesNom = []; + + matRad_cfg.dispInfo("Reading %d number of beamlets in %d voxels (%dx%dx%d)\n",numberOfBixels,prod(dims),dims(1),dims(2),dims(3)); + + bixelCounter = 0; + for i = 1:numberOfBixels + %Read Beamlet + bixNum = fread(f,1,"int32"); + numVox = fread(f,1,"int32"); + + bixelCounter = bixelCounter +1; + + colIndices(end+1:end+numVox) = bixelCounter; + currVoxelIndices = fread(f,numVox,"uint32") + 1; + tmpValues = fread(f,numVox*nComponents,"float32"); + valuesNom = tmpValues(1:nComponents:end); + + if nComponents == 2 + valuesDen = tmpValues(nComponents:nComponents:end); + values(end+1:end+numVox) = valuesNom./valuesDen; + else + values(end+1:end+numVox) = valuesNom; + end + + % x and y components have been permuted in CT + [indY, indX, indZ] = ind2sub(dims, currVoxelIndices); + + voxelIndices(end+1:end+numVox) = sub2ind(dims([2,1,3]), indX, indY, indZ); + matRad_cfg.dispInfo("\tRead beamlet %d, %d voxels...\n",bixNum,numVox); + end + dijMatrix = sparse(voxelIndices,colIndices,values,prod(dims),numberOfBixels); + + fclose(f); + catch + fclose(f); + matRad_cfg.dispError('unable to load file: %s',fName); + end + end + + function dijMatrix = readSparseDijBin_v21(fName) + + matRad_cfg = MatRad_Config.instance(); + + f = fopen(fName,'r','l'); + + try + %Header + fileFormatVerison = fread(f,1,"int32"); + dims = fread(f,3,"int32"); + res = fread(f,3,"float32"); + offset = fread(f,3,"float32"); + orientation = fread(f,9,"float32"); + nComponents = fread(f,1,"int32"); + numberOfBixels = fread(f,1,"int32"); + + values = []; + valuesDen = []; + voxelIndices = []; + colIndices = []; + valuesNom = []; + + matRad_cfg.dispInfo("Reading %d number of beamlets in %d voxels (%dx%dx%d)\n",numberOfBixels,prod(dims),dims(1),dims(2),dims(3)); + + bixelCounter = 0; + for i = 1:numberOfBixels + %Read Beamlet + bixNum = fread(f,1,"uint32"); + numVox = fread(f,1,"int32"); + + bixelCounter = bixelCounter +1; + + colIndices(end+1:end+numVox) = bixelCounter; + currVoxelIndices = fread(f,numVox,"uint32") + 1; + tmpValues = fread(f,numVox*nComponents,"float32"); + valuesNom = tmpValues(1:nComponents:end); + + if nComponents == 2 + valuesDen = tmpValues(nComponents:nComponents:end); + values(end+1:end+numVox) = valuesNom./valuesDen; + else + values(end+1:end+numVox) = valuesNom; + end + + % x and y components have been permuted in CT + [indY, indX, indZ] = ind2sub(dims, currVoxelIndices); + + voxelIndices(end+1:end+numVox) = sub2ind(dims([2,1,3]), indX, indY, indZ); + matRad_cfg.dispInfo("\tRead beamlet %d, %d voxels...\n",bixNum,numVox); + end + dijMatrix = sparse(voxelIndices,colIndices,values,prod(dims),numberOfBixels); + + fclose(f); + catch + fclose(f); + matRad_cfg.dispError('unable to load file: %s',fName); + end + end + + function dijMatrix = readSparseDijBin_v31(fName) + + matRad_cfg = MatRad_Config.instance(); + + f = fopen(fName,'r','l'); + + try + fileFormatVerison = fread(f,1,"int32"); + dims = fread(f,3,"int32"); + res = fread(f,3,"float32"); + offset = fread(f,3,"float32"); + orientation = fread(f,9,"float32"); + nComponents = fread(f,1,"uint32"); + numberOfBixels = fread(f,1,"uint32"); + + values = []; + valuesDen = []; + voxelIndices = []; + colIndices = []; + valuesNom = []; + + matRad_cfg.dispInfo("Reading %d number of beamlets in %d voxels (%dx%dx%d)\n",numberOfBixels,prod(dims),dims(1),dims(2),dims(3)); + + allBixelMeta = fread(f, 3*numberOfBixels, "uint32"); + allBixelMeta = reshape(allBixelMeta, 3,numberOfBixels)'; % (#bixels, PBidx, FID, PBID) + + % if nComponents>1 + % matRad_cfg.dispWarning('!! Only last component will be read!!') + % end + % Components header + for i = 1:nComponents + componentDataSize(i) = fread(f,1,"uint32"); + end + + % Data + for compIdx=1:nComponents + % PBidx and voxelIndices should be always the same for + % each component? + PBidxs = fread(f, componentDataSize(compIdx), "uint32")+1; + voxelIndices = fread(f, componentDataSize(compIdx), "uint32")+1; + tmpValues{compIdx} = fread(f, componentDataSize(compIdx), "float32"); + + end + + % For now we only have Dose and LET scorers, if nComponents + % == 2, then it's LET + + if nComponents>1 + values = tmpValues{1}./tmpValues{2}; + else + values = tmpValues{1}; + end + + % x and y components have been permuted in CT + [indY, indX, indZ] = ind2sub(dims, voxelIndices); + + voxelIndices = sub2ind(dims([2,1,3]), indX, indY, indZ); % + (nComponents-1)*prod(dims); + % This will probably not work for multiple components + dijMatrix = sparse(voxelIndices,PBidxs,values,prod(dims), numberOfBixels); + + fclose(f); + + catch + fclose(f); + matRad_cfg.dispError('unable to load file: %s',fName); + + end + end + + [doseCubeV, letdCubeV, fileName] = readSimulationOutput(runFolder,calcDoseDirect, varargin); + + end + + + methods (Access = private) + + function updatePaths(obj, rootFolder) + + if ~strcmp(rootFolder, obj.FREDrootFolder) + obj.FREDrootFolder = rootFolder; + end + + obj.MCrunFolder = fullfile(obj.FREDrootFolder, 'MCrun'); + obj.inputFolder = fullfile(obj.MCrunFolder, 'inp'); + obj.regionsFolder = fullfile(obj.inputFolder, 'regions'); + obj.planFolder = fullfile(obj.inputFolder, 'plan'); + + end + + function [radiationMode] = updateRadiationMode(this,value) + % This function also resets the values for primary mass and numebr + % of nucleons. Used for possible future extension to multiple + % ion species + matRad_cfg = MatRad_Config.instance(); + + if any(strcmp(value, this.possibleRadiationModes)) + radiationMode = value; + else + matRad_cfg.dispError('Invalid radiation modality: %s', value); + end + + switch radiationMode + case 'protons' + this.primaryMass = 1.00727; % Da + this.numOfNucleons = 1; + matRad_cfg.dispInfo('Default values for priamry mass and number of nucleons set.'); + otherwise + matRad_cfg.dispError('Only proton dose calculation available with this version of FRED'); + + end + + matRad_cfg.dispWarning('Selected radiation modality: %s with primary mass: %2.3f', radiationMode, this.primaryMass); + end + + function isHigher = isVersionHigher(this,version) + isHigher = false; + + % This function directly looks at FRED installation, not at + % the current FRED version stored in the class property. + fredVersion = this.getVersion(); + + if ~isempty(fredVersion) + % Decompose the current version for comparison + v1 = sscanf(fredVersion, '%d.%d.%d')'; + v2 = sscanf(version, '%d.%d.%d')'; + + if (v1(1) >= v2(1)) && (v1(2) >= v2(2)) && (v1(3) > v2(3)) + isHigher = true; + end + end + + end + + end + + + methods + + function set.sourceModel(this, value) + matRad_cfg = MatRad_Config.instance(); + + valid = ischar(value) && any(strcmp(value, this.AvailableSourceModels)); + + if valid + this.sourceModel = value; + else + matRad_cfg.dispWarning('Unable to set source model:%s, setting default:%s', value, this.AvailableSourceModels{1}) + this.sourceModel = this.AvailableSourceModels{1}; + end + + end + + function version = get.currentVersion(this) + + if isempty(this.currentVersion) + version = this.getVersion(); + this.currentVersion = version; + else + version = this.currentVersion; + end + + end + + function set.dijFormatVersion(this,value) + + matRad_cfg = MatRad_Config.instance(); + + if ~this.isVersionHigher('3.70.0') + % FRED version < 3.70.0 does not allow dij version + % selection and only works with ifFormatVersion < 21 + + this.dijFormatVersion = this.defaultDijFormatVersion; + + if ~strcmp(value, this.defaultDijFormatVersion) + matRad_cfg.dispWarning(sprintf('ijFormat: %s not available for FRED version < 3.70.0', value)); + end + + else + %if this.useGPU + if strcmp(value, '20') + this.dijFormatVersion = '21'; + else + this.dijFormatVersion = value; + end + %else + % this.dijFormatVersion = '20'; + %end + end + end + + function readerHandle = get.dijReaderHandle(this) + + matRad_cfg = MatRad_Config.instance(); + + switch this.dijFormatVersion + + case '20' + readerHandle = @(lFile) DoseEngines.matRad_ParticleFREDEngine.readSparseDijBin(lFile); + case '21' + readerHandle = @(lFile) DoseEngines.matRad_ParticleFREDEngine.readSparseDijBin_v21(lFile); + case '31' + readerHandle = @(lFile) DoseEngines.matRad_ParticleFREDEngine.readSparseDijBin_v31(lFile); + otherwise + matRad_cfg.dispWarning(sprintf('Unable to read dij format version: %s, using default: %s', this.dijFormatVersion, this.defaultDijFormatVersion)); + readerHandle = @(lFile) DoseEngines.matRad_ParticleFREDEngine.readSparseDijBin(lFile); + end + + + end + + function set.radiationMode(this,value) + if ischar(value) + if ~isempty(this.radiationMode) && ~strcmp(this.radiationMode, value) + this.radiationMode = value; + this.updateRadiationMode(this.radiationMode); + elseif isempty(this.radiationMode) + this.radiationMode = value; + end + end + + end + + function set.FREDrootFolder(obj, pathValue) + obj.FREDrootFolder = pathValue; + obj.updatePaths(pathValue); + end + + + function set.externalCalculation(this, value) + % Set exportCalculation value, available options are: + % - false: (default) runs the FRED simulation (requires FRED installation) + % - write/1: triggers the file export + % - 'path': simulation data will be loaded from the specified + % path. Full simulation directory path should be provided. + % Example: 'matRadRoot/userdata/FRED/' + + if isnumeric(value) || islogical(value) + switch value + case 1 + this.externalCalculation = 'write'; + case 0 + this.externalCalculation = 'off'; + end + elseif ischar(value) + + if any(strcmp(value, {'write', 'off'})) + this.externalCalculation = value; + elseif isfolder(value) + this.externalCalculation = value; + + this.updatePaths(value); + end + end + end + + end +end + diff --git a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/readSimulationOutput.m b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/readSimulationOutput.m new file mode 100644 index 000000000..0ae5da683 --- /dev/null +++ b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/readSimulationOutput.m @@ -0,0 +1,100 @@ +function [doseCube, letCube, loadFileName] = readSimulationOutput(runFolder,calcDoseDirect,varargin) +% FRED helper to read simulation output +% call +% readSimulationOutput(runFolder,calcDoseDirect, varargin) +% +% input +% runFolder: path to folder containing the simulation files +% calcDoseDirect: boolean to trigger dij or .mhd reading +% +% optional: +% calLET: addirional boolean to trigger loading of LETd +% readFunctionHandle: handle to readout function for ij-scorer +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2023 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +matRad_cfg = MatRad_Config.instance(); + +p = inputParser(); +addRequired(p, 'runFolder', @ischar); +addRequired(p, 'calcDoseDirect', @islogical); +addParameter(p, 'calcLET',0,@islogical); +addParameter(p, 'readFunctionHandle', @(lFile) DoseEngines.matRad_ParticleFREDEngine.readSparseDijBin(lFile)) + +parse(p, runFolder,calcDoseDirect, varargin{:}); + +runFolder = p.Results.runFolder; +calcDoseDirect = p.Results.calcDoseDirect; +calcLET = p.Results.calcLET; + +doseCube = []; +letCube = []; +loadFileName = []; + +if ~calcDoseDirect + + doseDijFolder = fullfile(runFolder, 'out', 'scoreij'); + doseDijFile = 'Phantom.Dose.bin'; + loadFileName = fullfile(doseDijFolder,doseDijFile); + + matRad_cfg.dispInfo(sprintf('Looking for scorer-ij output in sub folder: %s\n', strrep(doseDijFolder, '\', '\\'))); + + % read dij matrix + if isfile(loadFileName) +% doseCube = DoseEngines.matRad_ParticleFREDEngine.readSparseDijBin(loadFileName); + doseCube = p.Results.readFunctionHandle(loadFileName); + else + matRad_cfg.dispError(sprintf('Unable to find file: %s', strrep(loadFileName, '\', '\\'))); + end + + if calcLET + + letdDijFile = 'Phantom.LETd.bin'; + letdDijFileName = fullfile(doseDijFolder,letdDijFile); + + try +% letCube = DoseEngines.matRad_ParticleFREDEngine.readSparseDijBin(letdDijFileName); + letCube = p.Results.readFunctionHandle(letdDijFileName); + catch + matRad_cfg.dispError('unable to load file: %s',letdDijFileName); + end + end +else + + doseCubeFolder = fullfile(runFolder, 'out', 'score'); + doseCubeFileName = 'Phantom.Dose.mhd'; + loadFileName = fullfile(doseCubeFolder, doseCubeFileName); + + matRad_cfg.dispInfo(sprintf('Looking for scorer file in sub folder: %s\n', strrep(doseCubeFolder, '\', '\\'))); + + if isfile(loadFileName) + doseCube = matRad_readMHD(loadFileName); + else + matRad_cfg.dispError(sprintf('Unable to find file: %s', strrep(loadFileName, '\', '\\'))); + end + + if calcLET + + letdDijFolder = doseCubeFolder; + letdCubeFileName = 'Phantom.LETd.mhd'; + + try + letCube = matRad_readMHD(fullfile(letdDijFolder, letdCubeFileName)); + catch + matRad_cfg.dispError('Unable to load file: %s',fullfile(letdDijFolder, letdCubeFileName)); + end + end + +end + +matRad_cfg.dispInfo('Loading succesful!\n'); +end \ No newline at end of file diff --git a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writePlanDeliveryFile.m b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writePlanDeliveryFile.m new file mode 100644 index 000000000..6cca35019 --- /dev/null +++ b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writePlanDeliveryFile.m @@ -0,0 +1,162 @@ +function writePlanDeliveryFile(this, fName) +% FRED helper to write file for plan delivery routine +% call +% writePlanDeliveryFile(fName) +% +% input +% fName: tring specifying the file path and name for saving the data. +% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2023 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +matRad_cfg = MatRad_Config.instance(); + +fID = fopen(fName, 'w'); +try + fprintf(fID, '#Include file defining fields and layers geometry\n'); + fprintf(fID, 'include: inp/plan/plan.inp\n'); + fprintf(fID, '\n'); + + + fprintf(fID, '#Define the fields\n'); + fprintf(fID, 'for(currField in plan.get(''Fields''))<\n'); + fprintf(fID, '\tfield<\n'); + fprintf(fID, '\t\tID = ${currField.get(''fieldNumber'')}\n'); + fprintf(fID, '\t\tO = [0,${plan.get(''SAD'')},0]\n'); + fprintf(fID, '\t\tL = ${currField.get(''dim'')}\n'); + fprintf(fID, '\t\tpivot = [0.5,0.5,0.5]\n'); + + fprintf(fID, '\t\tl = [0, 0, -1]\n'); + fprintf(fID, '\t\tu = [1, 0 ,0]\n'); + + fprintf(fID, '\tfield>\n'); + + fprintf(fID, '\n'); + fprintf(fID, '\t#Deactivate the fields to avoid geometrical overlap\n'); + fprintf(fID, '\tdeactivate: field_${currField.get(''fieldNumber'')}\n'); + fprintf(fID, 'for>\n\n'); + + %loop over fields + fprintf(fID, 'for(currField in plan.get(''Fields''))<\n'); + fprintf(fID,'\n'); + fprintf(fID, '\tdef: fieldIdx = currField.get(''fieldNumber'')\n'); + fprintf(fID,'\n'); + + % activate current filed + fprintf(fID, '\t#Activate current field\n'); + fprintf(fID, '\tactivate: field_$fieldIdx\n'); + fprintf(fID,'\n'); + + % Gantry/COunch angles + fprintf(fID,'\t#Collect Gantry and Couch angles\n'); + fprintf(fID, '\tdef: GA = currField.get(''GA'')\n'); + fprintf(fID, '\tdef: CA = currField.get(''CA'')\n'); + fprintf(fID,'\n'); + + % Isocenter + fprintf(fID, '\t#Collect Isocenter\n'); + fprintf(fID, '\tdef: ISO = currField.get(''ISO'')\n'); + fprintf(fID, '\n'); + + fprintf(fID, '\t#First move the patient so that the Isocenter is now in the center of the Room coordinate system\n'); + fprintf(fID, '\ttransform: Phantom move_to ${ISO.item(0)} ${ISO.item(1)} ${ISO.item(2)} Room\n'); + fprintf(fID, '\n'); + + fprintf(fID, '\t#Second rotate the patient according to the gantry and couch angles.\n'); + fprintf(fID, '\t#In this configuration the fileds are always fixed in +SAD in y direction and the patient is rotated accordingly\n'); + fprintf(fID, '\ttransform: Phantom rotate y ${CA} Room\n'); + fprintf(fID, '\ttransform: Phantom rotate z ${GA} Room\n'); + fprintf(fID, '\n'); + + fprintf(fID, '\tfor(layer in currField.get(''Layers''))<\n'); + fprintf(fID, '\n'); + + fprintf(fID, '\t\t#Recover parameters of the current energy layer\n'); + fprintf(fID, '\t\tdef: currEnergy = layer.get(''Energy'')\n'); + fprintf(fID, '\t\tdef: currEspread = layer.get(''Espread'')\n'); + + switch this.sourceModel + case 'gaussian' + fprintf(fID, '\t\tdef: currFWHM = layer.get(''FWHM'')\n'); + + case 'emittance' + fprintf(fID, '\t\tdef: currEmittanceX = layer.get(''emittanceX'')\n'); + + fprintf(fID, '\t\tdef: currTwissAlphaX = layer.get(''twissAlphaX'')\n'); + fprintf(fID, '\t\tdef: currTwissBetaX = layer.get(''twissBetaX'')\n'); + fprintf(fID, '\t\tdef: currReferencePlane = layer.get(''emittanceRefPlaneDistance'')\n'); + case 'sigmaSqrModel' + fprintf(fID, '\t\tdef: currSQr_a = layer.get(''sSQr_a'')\n'); + fprintf(fID, '\t\tdef: currSQr_b = layer.get(''sSQr_b'')\n'); + fprintf(fID, '\t\tdef: currSQr_c = layer.get(''sSQr_c'')\n'); + end + + fprintf(fID, '\n'); + fprintf(fID, '\t\tfor(beamlet in layer.get(''beamlets''))<\n'); + fprintf(fID, '\t\t\tpb<\n'); + fprintf(fID, '\t\t\t\tID = ${beamlet.get(''beamletID'')}\n'); + fprintf(fID, '\t\t\t\tfieldID = $fieldIdx\n'); + switch this.machine.meta.radiationMode + case 'protons' + fprintf(fID, '\t\t\t\tparticle = proton\n'); + case 'carbon' + fprintf(fID, '\t\t\t\tparticle = C12\n'); + end + fprintf(fID, '\t\t\t\tT = $currEnergy\n'); + fprintf(fID, '\t\t\t\tEFWHM = $currEspread\n'); + + switch this.sourceModel + case 'gaussian' + + fprintf(fID, '\t\t\t\tXsec = gauss\n'); + fprintf(fID, '\t\t\t\tFWHM = $currFWHM\n'); + case 'emittance' + fprintf(fID, '\t\t\t\tXsec = emittance\n'); + fprintf(fID, '\t\t\t\temittanceX = $currEmittanceX\n'); + fprintf(fID, '\t\t\t\ttwissAlphaX = $currTwissAlphaX\n'); + fprintf(fID, '\t\t\t\ttwissBetaX = $currTwissBetaX\n'); + fprintf(fID, '\t\t\t\temittanceRefPlaneDistance = 100\n'); + + case 'sigmaSqrModel' + fprintf(fID, '\t\t\t\tXsec = emittance\n'); + fprintf(fID, '\t\t\t\tsigmaSqrModel = [${plan.get(''SAD'')},${currSQr_a},${currSQr_b}, ${currSQr_c}]\n'); + end + + fprintf(fID, '\n'); + fprintf(fID, '\t\t\t\tP = ${beamlet.get(''P'')}\n'); + fprintf(fID, '\t\t\t\tv = ${beamlet.get(''v'')}\n'); + fprintf(fID, '\t\t\t\tN = ${beamlet.get(''w'')}\n'); + fprintf(fID, '\t\t\tpb>\n'); + fprintf(fID, '\t\tfor>\n'); + fprintf(fID, '\tfor>\n'); + fprintf(fID, '\n'); + + fprintf(fID, '\t#Deliver all the pecil beams in this field\n'); + fprintf(fID, '\tdeliver: field_$fieldIdx\n'); + fprintf(fID, '\n'); + + fprintf(fID, '\t#Deactivate the current field\n'); + fprintf(fID, '\tdeactivate: field_$fieldIdx\n'); + fprintf(fID, '\n'); + + fprintf(fID, '\t#Restore the patient to original position\n'); + fprintf(fID, '\ttransform: Phantom rotate z ${-1*GA} Room\n'); + fprintf(fID, '\ttransform: Phantom rotate y ${-1*CA} Room\n'); + fprintf(fID, '\ttransform: Phantom move_to 0 0 0 Room\n'); + + fprintf(fID, 'for>\n\n'); +catch + matRad_cfg.dispError('Failed to write planDelivery file'); +end + +fclose(fID); +end \ No newline at end of file diff --git a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writePlanFile.m b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writePlanFile.m new file mode 100644 index 000000000..76059d814 --- /dev/null +++ b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writePlanFile.m @@ -0,0 +1,183 @@ +function writePlanFile(this, fName, stf) +% FRED helper to write data to plan.inp file +% call +% writePlanFile(fName, stf) +% +% input +% fName: string specifying the file path and name for saving the data. +% stf: Fred stf struct +% +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2023 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +matRad_cfg = MatRad_Config.instance(); + +fID = fopen(fName, 'w'); + +try + totalNumOfBixels = sum([stf.totalNumOfBixels]); + if this.calcDoseDirect + simulatedPrimariesPerBixel = max([1, floor(this.numHistoriesDirect/totalNumOfBixels)]); + else + simulatedPrimariesPerBixel = this.numHistoriesPerBeamlet; + end + + fprintf(fID, 'nprim = %i\n', simulatedPrimariesPerBixel); + + layerCounter = 0; + bixelCounter = 0; + + % loop oever the fields + for i=1:numel(stf) + + %Loop over energy layers + for j=1:numel(stf(i).energies) + + fprintf(fID, '#Bixels Field%i, Layer%i\n', i-1,layerCounter+j-1); + + % Print bixel info (ID, Position, Direction, Weight) + for k=1:stf(i).energyLayer(j).nBixels + currBixel.beamletID = num2str(bixelCounter+k-1); + currBixel.P = arrayfun(@(idx) num2str(idx, '%2.3f'), [stf(i).energyLayer(j).rayPosY(k),stf(i).energyLayer(j).rayPosX(k),0], 'UniformOutput', false); + currBixel.v = arrayfun(@(idx) num2str(idx, '%2.5f'), [stf(i).energyLayer(j).rayDivY(k),stf(i).energyLayer(j).rayDivX(k),1], 'UniformOutput', false); + currBixel.w = num2str(stf(i).energyLayer(j).numOfPrimaries(k), '%2.7f'); + + printStructToDictionary(fID, currBixel, ['S', num2str(bixelCounter+k-1)],2); + + end + + currLayer.Energy = num2str(stf(i).energies(j)); + currLayer.Espread = num2str(stf(i).energySpreadFWHMMev(j)); + + % Select source model + switch this.sourceModel + case 'gaussian' + currLayer.FWHM = num2str(stf(i).FWHMs(j)); + case 'emittance' + currLayer.emittanceX = num2str(stf(i).emittanceX(j), '%1.10f'); + currLayer.twissAlphaX = num2str(stf(i).twissAlphaX(j),'%1.10f'); + currLayer.twissBetaX = num2str(stf(i).twissBetaX(j), '%1.10f'); + case 'sigmaSqrModel' + currLayer.sSQr_a = num2str(stf(i).sSQr_a(j)); + currLayer.sSQr_b = num2str(stf(i).sSQr_b(j)); + currLayer.sSQr_c = num2str(stf(i).sSQr_c(j)); + end + + % Specify the beamlets in current layer + currLayer.beamlets = arrayfun(@(idx) ['S', num2str(idx)], bixelCounter:stf(i).energyLayer(j).nBixels+bixelCounter-1, 'UniformOutput', false); + + %Print layer + printStructToDictionary(fID, currLayer, ['L', num2str(layerCounter+j-1)],1); + fprintf(fID, '\n'); + bixelCounter = bixelCounter + stf(i).energyLayer(j).nBixels; + end + + % Estimate field dimension + fieldLim = max(abs([stf(i).energyLayer.rayPosX,stf(i).energyLayer.rayPosY])) + 10*max([stf(i).FWHMs]); + + %Write field parameters + currF.fieldNumber = i-1; + currF.GA = num2str(stf(i).gantryAngle); + currF.CA = num2str(stf(i).couchAngle); + currF.ISO = arrayfun(@num2str, stf(i).isoCenter, 'UniformOutput', false); + currF.dim = arrayfun(@num2str, [fieldLim, fieldLim, 0.1], 'UniformOutput', false); + currF.Layers = arrayfun(@(idx) ['L', num2str(idx)], layerCounter:numel(stf(i).energies)+layerCounter-1, 'UniformOutput', false); + + layerCounter = layerCounter + numel(stf(i).energies); + + printStructToDictionary(fID, currF, ['F', num2str(i-1)]); + fprintf(fID, '\n'); + end + + %% Build plan + + % BAMsToIso is the same for all fields + plan.SAD = stf(1).BAMStoIsoDist; + plan.Fields = arrayfun(@(i) ['F', num2str(i)], 0:numel(stf)-1, 'UniformOutput', false); + + printStructToDictionary(fID, plan, 'plan'); + +catch + matRad_cfg.dispError('Failed to write plan file'); +end + +fclose(fID); +end + +function printStructToDictionary(fID, S, sName, indentTabs) +% Helper function to convert struct fields into FRED specific python dictionary +% call +% printStructToDictionary(fID, S, sName, indentTabs) +% +% input +% fID: ID of file to write +% S: struct to convert +% sName: variable name of the printed dictionary +% indentTabs: optional, adds indentation to the left of pruinted code +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2023 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +if ~exist('indentTabs', 'var') || isempty(indentTabs) + indentTabs = 0; +end + +indentString = repmat('\t',1,indentTabs); + + +fprintf(fID, indentString); + +% variable defintion +fprintf(fID, 'def: %s = {', sName); + +% Get all fields to print +sFields = fieldnames(S); + + +for sFieldIdx =1:numel(sFields) + + currField = sFields{sFieldIdx}; + + % write field name + fprintf(fID, '''%s'': ',currField); + + % write one or multiple values + if ~iscell(S.(currField)) + fprintf(fID, '%s', num2str(S.(currField))); + else + fprintf(fID, '['); + for elementIdx=1:numel(S.(currField))-1 + fprintf(fID, '%s, ', S.(currField){elementIdx}); + end + fprintf(fID, '%s]', S.(currField){end}); + + end + + if sFieldIdx ~= numel(sFields) + fprintf(fID, ', '); + end +end + +% Close dictionary defintion +fprintf(fID, '}\n'); +end \ No newline at end of file diff --git a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writeRegionsFile.m b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writeRegionsFile.m new file mode 100644 index 000000000..849304e9e --- /dev/null +++ b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writeRegionsFile.m @@ -0,0 +1,85 @@ +function writeRegionsFile(this,fName) +% FRED helper to write file for geometrical components +% call +% writeRegionsFile(fName) +% +% input +% fName: string specifying the file path and name for saving the data. +% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2023 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +matRad_cfg = MatRad_Config.instance(); + +fID = fopen(fName, 'w'); +try + % Write patient component + fprintf(fID,'region<\n'); + fprintf(fID,'\tID=Phantom\n'); + fprintf(fID,'\tCTscan=inp/regions/%s\n', this.patientFilename); + fprintf(fID,'\tO=[%i,%i,%i]\n', 0,0,0); + fprintf(fID,'\tpivot=[0.5,0.5,0.5]\n'); + + % l=e1; u=e2; + % x in Room coordinates is x in patient frame + % y in Romm coordinates is -y in patient frame + % Voxels in y-direection in matRad grow in -y direction in FRED Room reference + fprintf(fID, '\tl=[%1.1f,%1.1f,%1.1f]\n', 1,0,0); + fprintf(fID, '\tu=[%1.1f,%1.1f,%1.1f]\n', 0,-1,0); + + % Syntax changes for scorers according to direct or ij calculation + if this.calcDoseDirect + fprintf(fID,'\tscore=['); + else + fprintf(fID,'\tscoreij=['); + end + + if numel(this.scorers)>1 + for k=1:size(this.scorers,2)-1 + fprintf(fID,'%s,', this.scorers{k}); + end + end + fprintf(fID,'%s]\n', this.scorers{end}); + + fprintf(fID,'region>\n'); + + % Write Room parameters + fprintf(fID, 'region<\n'); + fprintf(fID, '\tID=Room\n'); + fprintf(fID, '\tmaterial=%s\n', this.roomMaterial); + fprintf(fID, 'region>\n'); + + % Write HU table if needed + switch this.HUtable + case 'internal' + fprintf(fID, 'lUseInternalHU2Mat=t\n'); + otherwise + fprintf(fID, 'include: inp/regions/hLut.inp\n'); + this.writeHlut(this.HUtable); + end + + % Toogle HU clamping if requested + if this.HUclamping + fprintf(fID, 'lAllowHUClamping=t\n'); + end + + if ~isempty(this.dijFormatVersion) && this.isVersionHigher('3.70.0') + fprintf(fID, 'ijFormatVersion = %s\n', this.dijFormatVersion); + end + +catch ME + matRad_cfg.dispError(['Failed to write regions file. Exit with error: ', ME.message]); + +end + +fclose(fID); +end \ No newline at end of file diff --git a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writeRunFile.m b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writeRunFile.m new file mode 100644 index 000000000..b3f0d3814 --- /dev/null +++ b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writeRunFile.m @@ -0,0 +1,34 @@ +function writeRunFile(~, fName) +% FRED helper to write file for simulation +% call +% writeRunFile(fName) +% +% input +% fName: string specifying the file path and name for saving the data. +% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2023 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +matRad_cfg = MatRad_Config.instance(); + +fID = fopen(fName, 'w'); + +try + % Include regions and plan delivery routine + fprintf(fID, 'include: inp/regions/regions.inp\n'); + fprintf(fID, 'include: inp/plan/planDelivery.inp\n'); +catch + matRad_cfg.dispError('Failed to write run file'); +end + +fclose(fID); +end \ No newline at end of file diff --git a/matRad/doseCalc/+DoseEngines/matRad_ParticleAnalyticalBortfeldEngine.m b/matRad/doseCalc/+DoseEngines/matRad_ParticleAnalyticalBortfeldEngine.m index 2ec514177..df2bcf3aa 100644 --- a/matRad/doseCalc/+DoseEngines/matRad_ParticleAnalyticalBortfeldEngine.m +++ b/matRad/doseCalc/+DoseEngines/matRad_ParticleAnalyticalBortfeldEngine.m @@ -72,107 +72,8 @@ end end - methods (Access = protected) - function dij = initDoseCalc(this,ct,cst,stf) - - matRad_cfg = MatRad_Config.instance(); - - if this.calcLET == true - matRad_cfg.dispWarning('Engine does not support LET calculation! Disabling!'); - this.calcLET = false; - end - - if this.calcBioDose == true - matRad_cfg.dispWarning('Engine does not support BioDose calculation! Disabling!'); - this.calcBioDose = false; - end - - dij = this.initDoseCalc@DoseEngines.matRad_ParticlePencilBeamEngineAbstract(ct,cst,stf); - end - - function chooseLateralModel(this) - %Now check if we need tho chose the lateral model because it - %was set to auto - matRad_cfg = MatRad_Config.instance(); - if strcmp(this.lateralModel,'auto') - this.lateralModel = 'single'; - elseif ~strcmp(this.lateralModel,'single') - matRad_cfg.dispWarning('Engine only supports analytically computed singleGaussian lateral Model!'); - this.lateralModel = 'single'; - end - matRad_cfg.dispInfo('Using an analytically computed %s Gaussian pencil-beam kernel model!\n'); - end - - function [currBixel] = getBixelIndicesOnRay(this,currBixel,currRay) - - % create offset vector to account for additional offsets modelled in the base data and a potential - % range shifter. In the following, we only perform dose calculation for voxels having a radiological depth - % that is within the limits of the base data set (-> machine.data(i).dephts). By this means, we only allow - % interpolations in this.calcParticleDoseBixel() and avoid extrapolations. - %urrBixel.offsetRadDepth = currBixel.baseData.offset + currBixel.radDepthOffset; - tmpOffset = currBixel.baseData.offset - currBixel.radDepthOffset; - - maxDepth = 1.15 * currBixel.baseData.range; - - % find depth depended lateral cut off - if this.dosimetricLateralCutOff == 1 - currIx = currRay.radDepths <= maxDepth + tmpOffset; - elseif this.dosimetricLateralCutOff < 1 && this.dosimetricLateralCutOff > 0 - currIx = currRay.radDepths <= maxDepth + tmpOffset; - sigmaSq = this.calcSigmaLatMCS(currRay.radDepths(currIx) - tmpOffset, currBixel.baseData.energy).^2 + currBixel.sigmaIniSq; - currIx(currIx) = currRay.radialDist_sq(currIx) < currBixel.baseData.LatCutOff.numSig.^2*sigmaSq; - else - matRad_cfg = MatRad_Config.instance(); - matRad_cfg.dispError('Cutoff must be a value between 0 and 1!') - end - - currBixel.subRayIx = currIx; - currBixel.ix = currRay.ix(currIx); - end - - function X = interpolateKernelsInDepth(this,bixel) - baseData = bixel.baseData; - - % calculate particle dose for bixel k on ray j of beam i - % convert from MeV cm^2/g per primary to Gy mm^2 per 1e6 primaries - conversionFactor = 1.6021766208e-02; - - radDepthOffset = bixel.radDepthOffset; - - if isfield(baseData,'energySpectrum') - energyMean = baseData.energySpectrum.mean; - energySpread = baseData.energySpectrum.sigma/100 * baseData.energySpectrum.mean; - else - energyMean = baseData.energy; - energySpread = baseData.energy * this.sigmaEnergy; - end - - if isfield(baseData,'offset') && ~isfield(baseData,'energySpectrum') - radDepthOffset = radDepthOffset - baseData.offset; - end - - X.Z = conversionFactor * this.calcAnalyticalBragg(energyMean, bixel.radDepths + radDepthOffset, energySpread); - X.sigma = this.calcSigmaLatMCS(bixel.radDepths + radDepthOffset, baseData.energy); - end - - function bixel = calcParticleBixel(this,bixel) - - kernel = this.interpolateKernelsInDepth(bixel); - - %compute lateral sigma - sigmaSq = kernel.sigma.^2 + bixel.sigmaIniSq; - - % calculate dose - bixel.physicalDose = bixel.baseData.LatCutOff.CompFac * exp( -bixel.radialDist_sq ./ (2*sigmaSq)) .* kernel.Z ./(2*pi*sigmaSq); - - % check if we have valid dose values - if any(isnan(bixel.physicalDose)) || any(bixel.physicalDose<0) - matRad_cfg = MatRad_Config.instance(); - matRad_cfg.dispError('Error in particle dose calculation.'); - end - end - - function doseVector = calcAnalyticalBragg(this, primaryEnergy, depthZ, energySpread) + methods (Access = public) + function [doseVector,hatD] = calcAnalyticalBragg(this, primaryEnergy, depthZ, energySpread) %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % call % this.calcAnalyticalBragg(PrimaryEnergy, depthz, WidthMod) @@ -319,6 +220,107 @@ function chooseLateralModel(this) sigmaMCS(depthZ==0) = 0; end + end + + methods (Access = protected) + function dij = initDoseCalc(this,ct,cst,stf) + + matRad_cfg = MatRad_Config.instance(); + + if this.calcLET == true + matRad_cfg.dispWarning('Engine does not support LET calculation! Disabling!'); + this.calcLET = false; + end + + if this.calcBioDose == true + matRad_cfg.dispWarning('Engine does not support BioDose calculation! Disabling!'); + this.calcBioDose = false; + end + + dij = this.initDoseCalc@DoseEngines.matRad_ParticlePencilBeamEngineAbstract(ct,cst,stf); + end + + function chooseLateralModel(this) + %Now check if we need tho chose the lateral model because it + %was set to auto + matRad_cfg = MatRad_Config.instance(); + if strcmp(this.lateralModel,'auto') + this.lateralModel = 'single'; + elseif ~strcmp(this.lateralModel,'single') + matRad_cfg.dispWarning('Engine only supports analytically computed singleGaussian lateral Model!'); + this.lateralModel = 'single'; + end + matRad_cfg.dispInfo('Using an analytically computed %s Gaussian pencil-beam kernel model!\n'); + end + + function [currBixel] = getBixelIndicesOnRay(this,currBixel,currRay) + + % create offset vector to account for additional offsets modelled in the base data and a potential + % range shifter. In the following, we only perform dose calculation for voxels having a radiological depth + % that is within the limits of the base data set (-> machine.data(i).dephts). By this means, we only allow + % interpolations in this.calcParticleDoseBixel() and avoid extrapolations. + %urrBixel.offsetRadDepth = currBixel.baseData.offset + currBixel.radDepthOffset; + tmpOffset = currBixel.baseData.offset - currBixel.radDepthOffset; + + maxDepth = 1.15 * currBixel.baseData.range; + + % find depth depended lateral cut off + if this.dosimetricLateralCutOff == 1 + currIx = currRay.radDepths <= maxDepth + tmpOffset; + elseif this.dosimetricLateralCutOff < 1 && this.dosimetricLateralCutOff > 0 + currIx = currRay.radDepths <= maxDepth + tmpOffset; + sigmaSq = this.calcSigmaLatMCS(currRay.radDepths(currIx) - tmpOffset, currBixel.baseData.energy).^2 + currBixel.sigmaIniSq; + currIx(currIx) = currRay.radialDist_sq(currIx) < currBixel.baseData.LatCutOff.numSig.^2*sigmaSq; + else + matRad_cfg = MatRad_Config.instance(); + matRad_cfg.dispError('Cutoff must be a value between 0 and 1!') + end + + currBixel.subRayIx = currIx; + currBixel.ix = currRay.ix(currIx); + end + + function X = interpolateKernelsInDepth(this,bixel) + baseData = bixel.baseData; + + % calculate particle dose for bixel k on ray j of beam i + % convert from MeV cm^2/g per primary to Gy mm^2 per 1e6 primaries + conversionFactor = 1.6021766208e-02; + + radDepthOffset = bixel.radDepthOffset; + + if isfield(baseData,'energySpectrum') + energyMean = baseData.energySpectrum.mean; + energySpread = baseData.energySpectrum.sigma/100 * baseData.energySpectrum.mean; + else + energyMean = baseData.energy; + energySpread = baseData.energy * this.sigmaEnergy; + end + + if isfield(baseData,'offset') && ~isfield(baseData,'energySpectrum') + radDepthOffset = radDepthOffset - baseData.offset; + end + + X.Z = conversionFactor * this.calcAnalyticalBragg(energyMean, bixel.radDepths + radDepthOffset, energySpread); + X.sigma = this.calcSigmaLatMCS(bixel.radDepths + radDepthOffset, baseData.energy); + end + + function bixel = calcParticleBixel(this,bixel) + + kernel = this.interpolateKernelsInDepth(bixel); + + %compute lateral sigma + sigmaSq = kernel.sigma.^2 + bixel.sigmaIniSq; + + % calculate dose + bixel.physicalDose = bixel.baseData.LatCutOff.CompFac * exp( -bixel.radialDist_sq ./ (2*sigmaSq)) .* kernel.Z ./(2*pi*sigmaSq); + + % check if we have valid dose values + if any(isnan(bixel.physicalDose)) || any(bixel.physicalDose<0) + matRad_cfg = MatRad_Config.instance(); + matRad_cfg.dispError('Error in particle dose calculation.'); + end + end function calcLateralParticleCutOff(this,cutOffLevel,~) calcRange = false; diff --git a/matRad/doseCalc/+DoseEngines/matRad_ParticleFineSamplingPencilBeamEngine.m b/matRad/doseCalc/+DoseEngines/matRad_ParticleFineSamplingPencilBeamEngine.m index a3cfc87dd..fbe852d5a 100644 --- a/matRad/doseCalc/+DoseEngines/matRad_ParticleFineSamplingPencilBeamEngine.m +++ b/matRad/doseCalc/+DoseEngines/matRad_ParticleFineSamplingPencilBeamEngine.m @@ -45,6 +45,8 @@ end this = this@DoseEngines.matRad_ParticlePencilBeamEngineAbstract(pln); + + this.restrictBeamRadDepthsByMaxEnergy = false; end function setDefaults(this) @@ -66,6 +68,14 @@ function setDefaults(this) ray.rotMat_system_T = beam.rotMat_system_T; end + function [currBixel] = getBixelIndicesOnRay(this,currBixel,currRay) + % In Finesampling we can not reduce the number of points to be + % computed that easily based on the rad depth, so we overload + % this method to just give all indices stored in the ray + currBixel.subRayIx = true(size(currRay.ix)); + currBixel.ix = currRay.ix; + end + % We override this function to get full lateral distances function ray = getRayGeometryFromBeam(this,ray,currBeam) lateralRayCutOff = this.getLateralDistanceFromDoseCutOffOnRay(ray); diff --git a/matRad/doseCalc/+DoseEngines/matRad_ParticleHongPencilBeamEngine.m b/matRad/doseCalc/+DoseEngines/matRad_ParticleHongPencilBeamEngine.m index 11d8454e5..4ccb4a904 100644 --- a/matRad/doseCalc/+DoseEngines/matRad_ParticleHongPencilBeamEngine.m +++ b/matRad/doseCalc/+DoseEngines/matRad_ParticleHongPencilBeamEngine.m @@ -1,7 +1,6 @@ classdef matRad_ParticleHongPencilBeamEngine < DoseEngines.matRad_ParticlePencilBeamEngineAbstract -% matRad_ParticlePencilBeamEngineAbstractGaussian: -% Implements an engine for particle based dose calculation -% For detailed information see superclass matRad_DoseEngine +% matRad_ParticleHongPencilBeamEngine: +% Implements the Hong pencil-beam engine % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -20,7 +19,7 @@ % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% properties (Constant) - possibleRadiationModes = {'protons', 'helium','carbon'} + possibleRadiationModes = {'protons', 'helium','carbon', 'VHEE'} name = 'Hong Particle Pencil-Beam'; shortName = 'HongPB'; end @@ -68,6 +67,13 @@ case 'multi' sigmaSq = kernels.sigmaMulti.^2 + bixel.sigmaIniSq; L = sum([1 - sum(kernels.weightMulti,2), kernels.weightMulti] .* exp(-bixel.radialDist_sq ./ (2*sigmaSq))./(2*pi*sigmaSq),2); + case 'singleXY' + %compute lateral sigma in both directions + sigmaSq_x = kernels.sigmaX.^2 + bixel.sigmaIniSq; + sigmaSq_y = kernels.sigmaY.^2 + bixel.sigmaIniSq; + sigma_x = sqrt(sigmaSq_x); + sigma_y = sqrt(sigmaSq_y); + L = exp( - (bixel.latDists(:,1).^2)./(2*sigmaSq_x) - (bixel.latDists(:,2).^2)./(2*sigmaSq_y) ) ./(2*pi*sigma_x.*sigma_y); otherwise %Sanity check matRad_cfg = MatRad_Config.instance(); @@ -138,12 +144,16 @@ dataType = machine.meta.dataType; if strcmp(dataType,'singleGauss') - checkData = all(isfield(machine.data,{'energy','depths','Z','peakPos','sigma','offset','initFocus'})); + checkData = all(isfield(machine.data,{'energy','depths','Z','sigma','offset','initFocus'})); elseif strcmp(dataType,'doubleGauss') - checkData = all(isfield(machine.data,{'energy','depths','Z','peakPos','weight','sigma1','sigma2','offset','initFocus'})); + checkData = all(isfield(machine.data,{'energy','depths','Z','weight','sigma1','sigma2','offset','initFocus'})); elseif strcmp(dataType,'multipleGauss') - checkData = all(isfield(machine.data,{'energy','depths','Z','peakPos','weightMulti','sigmaMulti','offset','initFocus'})); + checkData = all(isfield(machine.data,{'energy','depths','Z','weightMulti','sigmaMulti','offset','initFocus'})); + elseif strcmp(dataType,'singleGaussXY') + checkData = all(isfield(machine.data,{'energy','depths','Z','offset','initFocus','sigmaXY'})); else + matRad_cfg = MatRad_Config.instance(); + matRad_cfg.dispWarning('Machine does not contain a valid ''dataType'' field!'); checkData = false; end diff --git a/matRad/doseCalc/+DoseEngines/matRad_ParticlePencilBeamEngineAbstract.m b/matRad/doseCalc/+DoseEngines/matRad_ParticlePencilBeamEngineAbstract.m index a4d2530c9..64749b5f2 100644 --- a/matRad/doseCalc/+DoseEngines/matRad_ParticlePencilBeamEngineAbstract.m +++ b/matRad/doseCalc/+DoseEngines/matRad_ParticlePencilBeamEngineAbstract.m @@ -1,23 +1,23 @@ classdef (Abstract) matRad_ParticlePencilBeamEngineAbstract < DoseEngines.matRad_PencilBeamEngineAbstract - % matRad_DoseEngineParticlePB: - % Implements an engine for particle based dose calculation - % For detailed information see superclass matRad_DoseEngine - % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - - % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - % - % Copyright 2022 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 - % help edit - - % LICENSE file. - % - % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% matRad_DoseEngineParticlePB: +% Implements an engine for particle based dose calculation +% For detailed information see superclass matRad_DoseEngine +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2022 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 +% help edit + +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% properties (SetAccess = public, GetAccess = public) @@ -38,6 +38,8 @@ vBetaX; % Stores Photon Beta bioKernelQuantities; % Kernel quantites to request from the machine data for biological dose calculation + + restrictBeamRadDepthsByMaxEnergy = true; end methods @@ -48,8 +50,6 @@ this = this@DoseEngines.matRad_PencilBeamEngineAbstract(pln); end - - end % Should be abstract methods but in order to satisfy the compatibility @@ -66,13 +66,16 @@ function chooseLateralModel(this) fValidateMulti = @(bd) isfield(bd,'sigmaMulti') && isfield(bd,'weightMulti') && ~isempty(bd.sigmaMulti) && ~isempty(bd.weightMulti); fValidateDouble = @(bd) isfield(bd,'sigma1') && isfield(bd,'sigma2') && isfield(bd,'weight') && ~isempty(bd.sigma1) && ~isempty(bd.sigma2) && ~isempty(bd.weight); fValidateSingle = @(bd) isfield(bd,'sigma') && ~isempty(bd.sigma); - + fValidateAsymFocused = @(bd) isfield(bd,'sigmaXY') && ~isempty(bd.sigmaXY); + matRad_cfg = MatRad_Config.instance(); singleAvailable = all(arrayfun(fValidateSingle,this.machine.data)); doubleAvailable = all(arrayfun(fValidateDouble,this.machine.data)); - multiAvailable = all(arrayfun(fValidateMulti,this.machine.data)); - + multiAvailable = all(arrayfun(fValidateMulti,this.machine.data)); + focusedAvailable = all(arrayfun(fValidateAsymFocused,this.machine.data)); + + matRad_cfg.dispInfo('''%s'' selected for lateral beam model, checking machine...\n',this.lateralModel); switch this.lateralModel @@ -86,6 +89,11 @@ function chooseLateralModel(this) matRad_cfg.dispWarning('Chosen Machine does not support a doubleGaussian Pencil-Beam model!'); this.lateralModel = 'auto'; end + case 'singleXY' + if ~multiAvailable + matRad_cfg.dispWarning('Chosen Machine does not support an asymmetrically focused Pencil-Beam model!'); + this.lateralModel = 'auto'; + end case 'multi' if ~multiAvailable matRad_cfg.dispWarning('Chosen Machine does not support a multiGaussian Pencil-Beam model!'); @@ -106,6 +114,8 @@ function chooseLateralModel(this) this.lateralModel = 'double'; elseif singleAvailable this.lateralModel = 'single'; + elseif focusedAvailable + this.lateralModel = 'singleXY'; else matRad_cfg.dispError('Invalid kernel model!'); end @@ -118,6 +128,8 @@ function chooseLateralModel(this) this.lateralModel = 'double'; elseif multiAvailable this.lateralModel = 'multi'; + elseif focusedAvailable + this.lateralModel = 'singleXY'; else matRad_cfg.dispError('Invalid kernel model!'); end @@ -162,6 +174,15 @@ function chooseLateralModel(this) bixel.numParticlesPerMU = currRay.numParticlesPerMU(k); end + if this.calcDoseDirect + if ~isfield(currRay,'weight') || numel(currRay.weight) < k + matRad_cfg = MatRad_Config.instance(); + matRad_cfg.dispError('No weight available for beam %d, ray %d, bixel %d',bixel.beamIndex,bixel.rayIndex,bixel.bixelIndex); + end + bixel.weight = currRay.weight(k); + bixel.MU = (bixel.weight.*1e6) ./ bixel.numParticlesPerMU; + end + % find energy index in base data energyIx = find(this.round2(currRay.energy(k),4) == this.round2([this.machine.data.energy],4)); bixel.energyIx = energyIx; @@ -193,6 +214,9 @@ function chooseLateralModel(this) % This allows us to efficiently access them by indexing in the % bixel computation bixel.radialDist_sq = currRay.radialDist_sq(bixel.subRayIx); + if isfield(currRay,'latDists') + bixel.latDists = currRay.latDists(bixel.subRayIx,:); + end bixel.radDepths = currRay.radDepths(bixel.subRayIx); if this.calcBioDose bixel.vTissueIndex = currRay.vTissueIndex(bixel.subRayIx); @@ -227,6 +251,9 @@ function chooseLateralModel(this) X.sigma1 = baseData.sigma1; X.sigma2 = baseData.sigma2; X.weight = baseData.weight; + case 'singleXY' + X.sigmaX = baseData.sigmaXY(:,1); + X.sigmaY = baseData.sigmaXY(:,2); case 'multi' X.weightMulti = baseData.weightMulti; X.sigmaMulti = baseData.sigmaMulti; @@ -247,7 +274,7 @@ function chooseLateralModel(this) X.LET = baseData.LET; end - X = structfun(@(v) matRad_interp1(depths,v,bixel.radDepths(:),'nearest'),X,'UniformOutput',false); %Extrapolate to zero? + X = structfun(@(v) matRad_interp1(depths,v,bixel.radDepths(:),'linear'),X,'UniformOutput',false); %Extrapolate to zero? end % We override this function to boost efficiency a bit (latDistX & Z @@ -256,12 +283,22 @@ function chooseLateralModel(this) lateralRayCutOff = this.getLateralDistanceFromDoseCutOffOnRay(ray); % Ray tracing for beam i and ray j - [ix,radialDist_sq] = this.calcGeoDists(currBeam.bevCoords, ... - ray.sourcePoint_bev, ... - ray.targetPoint_bev, ... - ray.SAD, ... - currBeam.validCoordsAll, ... - lateralRayCutOff); + if isequal(this.lateralModel,'singleXY') + [ix,radialDist_sq,latDists] = this.calcGeoDists(currBeam.bevCoords, ... + ray.sourcePoint_bev, ... + ray.targetPoint_bev, ... + ray.SAD, ... + currBeam.validCoordsAll, ... + lateralRayCutOff); + else + [ix,radialDist_sq] = this.calcGeoDists(currBeam.bevCoords, ... + ray.sourcePoint_bev, ... + ray.targetPoint_bev, ... + ray.SAD, ... + currBeam.validCoordsAll, ... + lateralRayCutOff); + latDists = []; + end ray.validCoords = cellfun(@(beamIx) beamIx & ix,currBeam.validCoords,'UniformOutput',false); ray.ix = cellfun(@(ixInGrid) this.VdoseGrid(ixInGrid),ray.validCoords,'UniformOutput',false); @@ -269,6 +306,9 @@ function chooseLateralModel(this) %subCoords = cellfun(@(beamIx) beamIx(ix),currBeam.validCoords,'UniformOutput',false); %ray.radialDist_sq = cellfun(@(subix) radialDist_sq(subix),radialDist_sq,subCoords); ray.radialDist_sq = cellfun(@(beamIx) radialDist_sq(beamIx(ix)),currBeam.validCoords,'UniformOutput',false); + if ~isempty(latDists) + ray.latDists = cellfun(@(beamIx) latDists(beamIx(ix),:),currBeam.validCoords,'UniformOutput',false); + end ray.validCoordsAll = any(cell2mat(ray.validCoords),2); @@ -418,12 +458,12 @@ function chooseLateralModel(this) radDepthOffset = this.machine.data(maxEnergyIx).offset + minRaShi; % apply limit in depth - %subSelectIx = currBeam.radDepths{1} < (this.machine.data(maxEnergyIx).depths(end) - radDepthOffset); - - subSelectIx = cellfun(@(rD) rD < (this.machine.data(maxEnergyIx).depths(end) - radDepthOffset),currBeam.radDepths,'UniformOutput',false); - currBeam.validCoords = cellfun(@and,subSelectIx,currBeam.validCoords,'UniformOutput',false); - currBeam.validCoordsAll = any(cell2mat(currBeam.validCoords),2); - + if this.restrictBeamRadDepthsByMaxEnergy + subSelectIx = cellfun(@(rD) rD < (this.machine.data(maxEnergyIx).depths(end) - radDepthOffset),currBeam.radDepths,'UniformOutput',false); + currBeam.validCoords = cellfun(@and,subSelectIx,currBeam.validCoords,'UniformOutput',false); + currBeam.validCoordsAll = any(cell2mat(currBeam.validCoords),2); + end + %currBeam.ixRadDepths = currBeam.ixRadDepths(subSelectIx); %currBeam.subIxVdoseGrid = currBeam.subIxVdoseGrid(subSelectIx); %currBeam.radDepths = cellfun(@(rd) rd(subSelectIx),currBeam.radDepths,'UniformOutput',false); @@ -543,13 +583,15 @@ function calcLateralParticleCutOff(this,cutOffLevel,stfElement) % function handle for calculating depth dose for APM sumGauss = @(x,mu,SqSigma,w) ((1./sqrt(2*pi*ones(numel(x),1) * SqSigma') .* ... exp(-bsxfun(@minus,x,mu').^2 ./ (2* ones(numel(x),1) * SqSigma' ))) * w); + + % define some variables needed for the cutoff calculation - vX = [0 logspace(-1,3,1200)]; % [mm] + vR = [0 logspace(-1,3,120)]; % [mm] % integration steps - r_mid = 0.5*(vX(1:end-1) + vX(2:end))'; % [mm] - dr = (vX(2:end) - vX(1:end-1))'; + r_mid = 0.5*(vR(1:end-1) + vR(2:end))'; % [mm] + dr = (vR(2:end) - vR(1:end-1))'; radialDist_sq = r_mid.^2; % number of depth points for which a lateral cutoff is determined @@ -612,6 +654,24 @@ function calcLateralParticleCutOff(this,cutOffLevel,stfElement) vEnergiesIx = find(ismember([this.machine.data(:).energy],uniqueEnergies(:,1))); cnt = 0; + if isequal(this.lateralModel,'singleXY') + vTheta = linspace(0,2*pi,361); + vTheta = vTheta(1:end-1); + dTheta = (vTheta(2)-vTheta(1))*ones(size(vTheta)); + [r_mid,theta] = meshgrid(r_mid,vTheta); + r_mid = r_mid(:); + radialDist_sq = r_mid.^2; + dr = repmat(dr,1,numel(vTheta))'; + dr = dr(:); + theta = theta(:); + dTheta = repmat(dTheta,1,numel(vR)-1); + dTheta = dTheta(:); + latDists = sqrt(radialDist_sq).* [cos(theta), sin(theta)]; + else + latDists = repmat(sqrt(radialDist_sq ./ 2),1,2); + dTheta = 2*pi; + end + % loop over all entries in the machine.data struct for energyIx = vEnergiesIx @@ -669,6 +729,7 @@ function calcLateralParticleCutOff(this,cutOffLevel,stfElement) bixel.energyIx = energyIx; bixel.baseData = baseData; bixel.radialDist_sq = radialDist_sq; + bixel.latDists = latDists; bixel.sigmaIniSq = largestSigmaSq4uniqueEnergies(cnt); bixel.radDepths = (depthValues(j) + baseData.offset) * ones(size(radialDist_sq)); bixel.vTissueIndex = ones(size(bixel.radDepths)); @@ -683,7 +744,7 @@ function calcLateralParticleCutOff(this,cutOffLevel,stfElement) bixel = this.calcParticleBixel(bixel); dose_r = bixel.physicalDose; - cumArea = cumsum(2*pi.*r_mid.*dose_r.*dr); + cumArea = cumsum(dTheta.*r_mid.*dose_r.*dr); relativeTolerance = 0.5; %in [%] if abs((cumArea(end)./(idd(j)))-1)*100 > relativeTolerance matRad_cfg.dispWarning('LateralParticleCutOff: shell integration is wrong !') @@ -742,6 +803,7 @@ function calcLateralParticleCutOff(this,cutOffLevel,stfElement) radDepths = [0:sStep:this.machine.data(energyIx).depths(end)] + this.machine.data(energyIx).offset; radialDist_sq = (X.^2 + Y.^2); radialDist_sq = radialDist_sq(:); + latDists = [X(:)', Y(:)']; mDose = zeros(dimX,dimX,numel(radDepths)); vDoseInt = zeros(numel(radDepths),1); @@ -766,6 +828,7 @@ function calcLateralParticleCutOff(this,cutOffLevel,stfElement) bixel.energyIx = energyIx; bixel.baseData = baseData; bixel.radialDist_sq = radialDist_sq; + bixel.latDists = latDists; bixel.sigmaIniSq = sigmaIni_sq; bixel.radDepths = radDepths(kk)*ones(size(bixel.radialDist_sq)); bixel.vTissueIndex = ones(size(bixel.radDepths)); @@ -780,7 +843,7 @@ function calcLateralParticleCutOff(this,cutOffLevel,stfElement) [~,IX] = min(abs((this.machine.data(energyIx).LatCutOff.depths + this.machine.data(energyIx).offset) - radDepths(kk))); TmpCutOff = this.machine.data(energyIx).LatCutOff.CutOff(IX); - vXCut = vX(vX<=TmpCutOff); + vXCut = vR(vR<=TmpCutOff); % integration steps r_mid_Cut = (0.5*(vXCut(1:end-1) + vXCut(2:end)))'; % [mm] @@ -940,7 +1003,9 @@ function calcLateralParticleCutOff(this,cutOffLevel,stfElement) dij = this.fillDij@DoseEngines.matRad_PencilBeamEngineAbstract(bixel,dij,stf,scenIdx,currBeamIdx,currRayIdx,currBixelIdx,bixelCounter); % Add MU information - if ~this.calcDoseDirect + if this.calcDoseDirect + dij.MU(bixelCounter,1) = bixel.MU; + else dij.minMU(bixelCounter,1) = bixel.minMU; dij.maxMU(bixelCounter,1) = bixel.maxMU; dij.numParticlesPerMU(bixelCounter,1) = bixel.numParticlesPerMU; @@ -973,8 +1038,7 @@ function calcLateralParticleCutOff(this,cutOffLevel,stfElement) q{end+1} = 'beta'; end end - - if ~isempty(q{1}) && isfield(machine.data,'LET') + if ~isempty(q) && ~isempty(q{1}) && isfield(machine.data,'LET') q{end+1} = 'LET'; end end diff --git a/matRad/doseCalc/+DoseEngines/matRad_PencilBeamEngineAbstract.m b/matRad/doseCalc/+DoseEngines/matRad_PencilBeamEngineAbstract.m index be64d33e9..6d647af0e 100644 --- a/matRad/doseCalc/+DoseEngines/matRad_PencilBeamEngineAbstract.m +++ b/matRad/doseCalc/+DoseEngines/matRad_PencilBeamEngineAbstract.m @@ -442,16 +442,9 @@ function setDefaults(this) dijColIx = (ceil(counter/this.numOfBixelsContainer)-1)*this.numOfBixelsContainer+1:counter; containerIx = 1:bixelContainerColIx; - weight = 1; else dijColIx = currBeamIdx; containerIx = 1; - if isfield(stf(currBeamIdx).ray(currRayIdx),'weight') && numel(stf(currBeamIdx).ray(currRayIdx).weight) >= currBixelIdx - weight = stf(currBeamIdx).ray(currRayIdx).weight(currBixelIdx); - else - matRad_cfg = MatRad_Config.instance(); - matRad_cfg.dispError('No weight available for beam %d, ray %d, bixel %d',currBeamIdx,currRayIdx,currBixelIdx); - end end % Iterate through all quantities @@ -463,7 +456,7 @@ function setDefaults(this) this.tmpMatrixContainers.(qName)(containerIx,subScenIdx{:}) = cell(numel(containerIx,subScenIdx{:})); else %dij.(qName){1}(this.VdoseGrid(bixel.ix),dijColIx) = dij.(qName){1}(this.VdoseGrid(bixel.ix),dijColIx) + weight * this.tmpMatrixContainers.(qName){containerIx,1}(this.VdoseGrid(bixel.ix)); - dij.(qName){scenIdx}(bixel.ix,dijColIx) = dij.(qName){scenIdx}(bixel.ix,dijColIx) + weight * bixel.(qName); + dij.(qName){scenIdx}(bixel.ix,dijColIx) = dij.(qName){scenIdx}(bixel.ix,dijColIx) + bixel.weight * bixel.(qName); end end end @@ -475,35 +468,13 @@ function setDefaults(this) dij.beamNum(currBeamIdx) = currBeamIdx; dij.rayNum(currBeamIdx) = currBeamIdx; dij.bixelNum(currBeamIdx) = currBeamIdx; + dij.w(counter,1) = bixel.weight; else dij.beamNum(counter) = currBeamIdx; dij.rayNum(counter) = currRayIdx; dij.bixelNum(counter) = currBixelIdx; end end - - %{ - function ray = computeRaySSD(this,ray) - [alpha,~,rho,d12,~] = matRad_siddonRayTracer(ray.isoCenter, ... - ct.resolution, ... - ray.sourcePoint, ... - ray.targetPoint, ... - this.cubeWED(1)); - ixSSD = find(rho{1} > this.ssdDensityThreshold,1,'first'); - - - if isempty(ixSSD) - matRad_cfg.dispError('ray does not hit patient. Trying to fix afterwards...'); - boolShowWarning = false; - elseif ixSSD(1) == 1 - matRad_cfg.dispWarning('Surface for SSD calculation starts directly in first voxel of CT!'); - boolShowWarning = false; - end - - % calculate SSD - ray.SSD = double(d12* alpha(ixSSD)); - end - %} function dij = finalizeDose(this,dij) %TODO: We could also do this by default for all engines, but diff --git a/matRad/doseCalc/+DoseEngines/matRad_PhotonPencilBeamSVDEngine.m b/matRad/doseCalc/+DoseEngines/matRad_PhotonPencilBeamSVDEngine.m index 5dcea9066..dd273c235 100644 --- a/matRad/doseCalc/+DoseEngines/matRad_PhotonPencilBeamSVDEngine.m +++ b/matRad/doseCalc/+DoseEngines/matRad_PhotonPencilBeamSVDEngine.m @@ -295,6 +295,10 @@ function setDefaults(this) bixel = struct(); + if this.calcDoseDirect + bixel.weight = currRay.weight; + end + if isfield(this.tmpMatrixContainers,'physicalDose') bixel.physicalDose = this.calcSingleBixel(currRay.SAD,... this.machine.data.m,... diff --git a/matRad/doseCalc/+DoseEngines/matRad_TG43BrachyEngine.m b/matRad/doseCalc/+DoseEngines/matRad_TG43BrachyEngine.m index 11c8e0cbd..13d44deca 100644 --- a/matRad/doseCalc/+DoseEngines/matRad_TG43BrachyEngine.m +++ b/matRad/doseCalc/+DoseEngines/matRad_TG43BrachyEngine.m @@ -322,7 +322,7 @@ function setDefaults(this) % radii in cm, angles in degree, values unitless FTab{1} = machine.data.AnisotropyRadialDistances; FTab{2} =machine.data.AnisotropyPolarAngles; - FTab{3} = machine.data.AnisotropyFunctionValue; + FTab{3} = reshape(machine.data.AnisotropyFunctionValue, [numel(FTab{2}),numel(FTab{1})]); % 2D formalism % according to Rivard et al.: AAPM TG-43 update p. 637 eq. (1) @@ -545,23 +545,23 @@ function setDefaults(this) % F: array of the same shape as r and thet containing the % interpolated and extrapolated values [DataRGrid,DataThetGrid] = meshgrid(FTab{1},FTab{2}); - Data(:,1) = reshape(DataRGrid,[],1); - Data(:,2) = reshape(DataThetGrid,[],1); - Value = reshape(FTab{3},[],1); - F = interp2(DataRGrid,DataThetGrid,FTab{3}, r, thet, 'linear'); + + F = interp2(DataRGrid,DataThetGrid,FTab{3}, r, thet, 'linear',1.0); % extrapolate for large and small values of r by taking the % interpolation of the maximal tabulated value at this angle % theta should be tabulated from 0?? to 180?? - rmin = FTab{1}(1); - rmax = FTab{1}(end); - IndLarge = r > rmax; - IndSmall = r < rmin; - rmaxGrid = rmax*ones(sum(IndLarge(:)),1); - rminGrid = rmin*ones(sum(IndSmall(:)),1); - F(IndLarge) = interp2(DataRGrid,DataThetGrid,FTab{3},rmaxGrid,double(thet(IndLarge))); - F(IndSmall) = interp2(DataRGrid,DataThetGrid,FTab{3},rminGrid,double(thet(IndSmall))); + + % rmin = FTab{1}(1); + % rmax = FTab{1}(end); + % + % IndLarge = r > rmax; + % IndSmall = r < rmin; + % rmaxGrid = rmax*ones(sum(IndLarge(:)),1); + % rminGrid = rmin*ones(sum(IndSmall(:)),1); + % F(IndLarge) = interp2(DataRGrid,DataThetGrid,FTab{3},rmaxGrid,double(thet(IndLarge))); + % F(IndSmall) = interp2(DataRGrid,DataThetGrid,FTab{3},rminGrid,double(thet(IndSmall))); end end diff --git a/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m b/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m index 8d4038ddf..58ab20995 100644 --- a/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m +++ b/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m @@ -1,5 +1,5 @@ classdef matRad_TopasMCEngine < DoseEngines.matRad_MonteCarloEngineAbstract - % matRad_TopasMCEngine + % matRad_TopasMCEngine % Implementation of the TOPAS interface for Monte Carlo dose % calculation % @@ -18,8 +18,8 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - properties (Constant) - possibleRadiationModes = {'photons','protons','helium','carbon'}; + properties (Constant) + possibleRadiationModes = {'photons','protons','helium','carbon','VHEE'}; name = 'TOPAS'; shortName = 'TOPAS'; @@ -28,6 +28,8 @@ end properties + hlut; + useGivenEqDensityCube; % Use the given density cube ct.cube and omit conversion from cubeHU. calcLET = false; calcBioDose = false; prescribedDose = []; @@ -36,7 +38,7 @@ topasExecCommand; %Defaults will be set during construction according to TOPAS installation instructions and used system parallelRuns = false; %Starts runs in parallel - + externalCalculation = 'off'; %Generates folder for external TOPAS calculation (e.g. on a server) workingDir; %working directory for the simulation @@ -51,7 +53,7 @@ modeHistories = 'num'; %'frac'; fracHistories = 1e-4; %Fraction of histories to compute - numParticlesPerHistory = 1e6; + numParticlesPerWeight = 1e6; verbosity = struct( 'timefeatures',0,... 'cputime',true,... 'run',0,... @@ -73,6 +75,10 @@ pencilBeamScanning = true; %This should be always true except when using photons (enables deflection) + %4D Calculation + calc4DInterplay = false; % switch CT phases according to SS order time sequence + calcTimeSequence = []; + %Image materialConverter = struct('mode','HUToWaterSchneider',... %'RSP','HUToWaterSchneider'; 'densityCorrection','Schneider_TOPAS',... %'rspHLUT','Schneider_TOPAS','Schneider_matRad' @@ -111,7 +117,8 @@ radiationMode; modules_protons = {'g4em-standard_opt4','g4h-phy_QGSP_BIC_HP','g4decay','g4h-elastic_HP','g4stopping','g4ion-QMD','g4radioactivedecay'}; modules_GenericIon = {'g4em-standard_opt4','g4h-phy_QGSP_BIC_HP','g4decay','g4h-elastic_HP','g4stopping','g4ion-QMD','g4radioactivedecay'}; - modules_photons = {'g4em-standard_opt4','g4h-phy_QGSP_BIC_HP','g4decay'}; + modules_photons = {'g4em-standard_opt4','g4h-phy_QGSP_BIC_HP','g4decay'}; + modules_VHEE = {'g4em-standard_opt4','g4h-phy_QGSP_BIC_HP','g4decay','g4ion-binarycascade','g4h-elastic_HP','g4stopping'}; %From 10.1002/mp.16697 %Geometry / World worldMaterial = 'G4_AIR'; @@ -151,7 +158,7 @@ 'Scorer_RBE_MCN','TOPAS_scorer_doseRBE_McNamara.txt.in', ... ... %PhaseSpace Source 'phaseSpaceSourcePhotons' ,'VarianClinaciX_6MV_20x20_aboveMLC_w2' ); - + end @@ -177,6 +184,8 @@ function setDefaults(this) this.setDefaults@DoseEngines.matRad_MonteCarloEngineAbstract(); matRad_cfg = MatRad_Config.instance(); %Instance of matRad configuration class + this.useGivenEqDensityCube = matRad_cfg.defaults.propDoseCalc.useGivenEqDensityCube; + % Default execution paths are set here this.topasFolder = [matRad_cfg.matRadSrcRoot filesep 'doseCalc' filesep 'topas' filesep]; this.workingDir = [matRad_cfg.primaryUserFolder filesep 'TOPAS' filesep]; @@ -185,7 +194,7 @@ function setDefaults(this) mkdir(this.workingDir); matRad_cfg.dispInfo('Created TOPAS working directory in userfolder %s\n',this.workingDir); end - + %Let's set some default commands taken from topas installation %instructions for mac & debain/ubuntu @@ -238,13 +247,13 @@ function writeAllFiles(obj,ct,cst,stf,machine,w) end % Get alpha beta parameters from bioParam struct - for i = 1:length(obj.bioParameters.AvailableAlphaXBetaX) - if ~isempty(strfind(lower(obj.bioParameters.AvailableAlphaXBetaX{i,2}),'default')) - break - end + if isfield(obj.bioParameters, 'tissuseAlphaX') + obj.bioParameters.AlphaX = obj.bioModel.tissueAlphaX(1); + obj.bioParameters.BetaX = obj.bioModel.tissueBetaX(1); + end + if numel(obj.bioParameters.AlphaX)>1 + matRad_cfg.dispWarning('!!! Only a unique alpha/beta ratio supported at the moment. Found multiple, only the first one will be used !!!!'); end - obj.bioParameters.AlphaX = obj.bioParameters.AvailableAlphaXBetaX{5,1}(1); - obj.bioParameters.BetaX = obj.bioParameters.AvailableAlphaXBetaX{5,1}(2); end if obj.scorer.LET @@ -286,8 +295,11 @@ function writeAllFiles(obj,ct,cst,stf,machine,w) % Save used RBE models if obj.scorer.RBE obj.MCparam.RBE_models = obj.scorer.RBE_model; - [obj.MCparam.ax,obj.MCparam.bx] = matRad_getPhotonLQMParameters(cst,prod(ct.cubeDim),obj.MCparam.numOfCtScen); - obj.MCparam.abx(obj.MCparam.bx>0) = obj.MCparam.ax(obj.MCparam.bx>0)./obj.MCparam.bx(obj.MCparam.bx>0); + [obj.MCparam.ax,obj.MCparam.bx] = matRad_getPhotonLQMParameters(obj.cstDoseGrid,prod(ct.cubeDim),obj.VdoseGrid); + obj.MCparam.abx = arrayfun(@(scen) zeros(size(obj.MCparam.bx{scen})), 1:obj.MCparam.numOfCtScen, 'UniformOutput',false); + for scen=1:obj.MCparam.numOfCtScen + obj.MCparam.abx{scen}(obj.MCparam.bx{scen}>0) = obj.MCparam.ax{scen}(obj.MCparam.bx{scen}>0)./obj.MCparam.bx{scen}(obj.MCparam.bx{scen}>0); + end end % fill in bixels, rays and beams in case of dij calculation or external calculation @@ -366,6 +378,29 @@ function writeAllFiles(obj,ct,cst,stf,machine,w) function resultGUI = getResultGUI(obj,dij) if obj.scorer.calcDij resultGUI = matRad_calcCubes(ones(dij.totalNumOfBixels,1),dij,1); + elseif obj.calc4DInterplay || obj.MCparam.numOfCtScen > 1 + for ctScen = 1:dij.numOfScenarios + tmpResultGUI = matRad_calcCubes(ones(dij.numOfBeams,1),dij,ctScen); + resultGUI.phaseDose{ctScen} = tmpResultGUI.physicalDose; + for beamIx = 1:dij.numOfBeams + resultGUI.(['phaseDose_beam', num2str(beamIx)]){ctScen} = tmpResultGUI.(['physicalDose_beam', num2str(beamIx)]); + end + if isfield(tmpResultGUI, 'alphaDoseCube') && isfield(tmpResultGUI, 'SqrtBetaDoseCube') + resultGUI.phaseAlphaDose{ctScen} = tmpResultGUI.alpha .* tmpResultGUI.physicalDose; + resultGUI.phaseSqrtBetaDose{ctScen} = sqrt(tmpResultGUI.beta) .* tmpResultGUI.physicalDose; + resultGUI.phaseRBExD{ctScen} = tmpResultGUI.RBExD; + for beamIx = 1:dij.numOfBeams + resultGUI.(['phaseAlphaDose_beam', num2str(beamIx)]){ctScen} = tmpResultGUI.(['alpha_beam', num2str(beamIx)]).*tmpResultGUI.(['physicalDose_beam', num2str(beamIx)]); + resultGUI.(['phaseSqrtBetaDose_beam', num2str(beamIx)]){ctScen} = sqrt(tmpResultGUI.(['beta_beam', num2str(beamIx)])).*tmpResultGUI.(['physicalDose_beam', num2str(beamIx)]); + resultGUI.(['phaseRBExD_beam', num2str(beamIx)]){ctScen} = tmpResultGUI.(['RBExD_beam', num2str(beamIx)]); + end + elseif isfield(tmpResultGUI,'RBExD') + resultGUI.phaseRBExD{ctScen} = tmpResultGUI.RBExD; + for beamIx = 1:dij.numOfBeams + resultGUI.(['phaseRBExD_beam', num2str(beamIx)]){ctScen} = tmpResultGUI.(['RBExD_beam', num2str(beamIx)]); + end + end + end else resultGUI = matRad_calcCubes(ones(dij.numOfBeams,1),dij,1); end @@ -434,7 +469,7 @@ function writeAllFiles(obj,ct,cst,stf,machine,w) return; else end - + %% Initialize dose grid and dij @@ -465,12 +500,12 @@ function writeAllFiles(obj,ct,cst,stf,machine,w) end end end - + for i = 1:numel(stf) if strcmp(stf(i).radiationMode,'photons') stf(i).ray.energy = stf(i).ray.energy.*ones(size(w)); end - end + end % Get photon parameters for RBExDose calculation if this.calcBioDose @@ -505,12 +540,16 @@ function writeAllFiles(obj,ct,cst,stf,machine,w) end % Run simulations for each scenario - for ctScen = 1:this.multScen.numOfCtScen + numCTScen = this.multScen.numOfCtScen; + if this.calc4DInterplay + numCTScen = 1; + end + for ctScen = 1:numCTScen for rangeShiftScen = 1:this.multScen.totNumRangeScen if this.multScen.scenMask(ctScen,shiftScen,rangeShiftScen) % Save ctScen and rangeShiftScen for file constructor - if ct.numOfCtScen > 1 + if ct.numOfCtScen > 1 && ~this.calc4DInterplay this.ctR.currCtScen = ctScen; this.ctR.currRangeShiftScen = rangeShiftScen; end @@ -610,7 +649,7 @@ function writeAllFiles(obj,ct,cst,stf,machine,w) dij.beamNum = 1; dij.bixelNum = 1; dij.ctGrid = this.ctR.ctGrid; - dij.doseGrid = this.doseGrid; + dij.doseGrid = this.doseGrid; dij.numOfBeams = 1; dij.numOfRaysPerBeam = 1; dij.numOfScenarios = this.multScen.totNumScen; @@ -636,11 +675,30 @@ function writeAllFiles(obj,ct,cst,stf,machine,w) end - + function dij = initDoseCalc(this,ct,cst,stf) dij = this.initDoseCalc@DoseEngines.matRad_MonteCarloEngineAbstract(ct,cst,stf); matRad_cfg = MatRad_Config.instance(); + % calculate rED or rSP from HU or take provided wedCube + if this.useGivenEqDensityCube && ~isfield(ct,'cube') + matRad_cfg.dispWarning('HU Conversion requested to be omitted but no ct.cube exists! Will override and do the conversion anyway!'); + this.useGivenEqDensityCube = false; + end + + if this.useGivenEqDensityCube + matRad_cfg.dispInfo('Omitting HU to rED/rSP conversion and using existing ct.cube!\n'); + else + ct = matRad_calcWaterEqD(ct, stf); % Maybe we can avoid duplicating the CT here? + end + + if isfield(ct,'hlut') + this.hlut = ct.hlut; + else + this.hlut = matRad_loadHLUT(ct,stf); + end + + % % for TOPAS we explicitly downsample the ct to the dose grid (might not be necessary in future versions with separated grids) % Check if CT has already been resampled matRad_cfg.dispInfo('Resampling cst... '); @@ -648,7 +706,7 @@ function writeAllFiles(obj,ct,cst,stf,machine,w) % Allpcate resampled cubes cubeHUresampled = cell(1,ct.numOfCtScen); cubeResampled = cell(1,ct.numOfCtScen); - + % Perform resampling to dose grid for s = 1:ct.numOfCtScen cubeHUresampled{s} = matRad_interp3(dij.ctGrid.x, dij.ctGrid.y', dij.ctGrid.z,ct.cubeHU{s}, ... @@ -656,27 +714,27 @@ function writeAllFiles(obj,ct,cst,stf,machine,w) cubeResampled{s} = matRad_interp3(dij.ctGrid.x, dij.ctGrid.y', dij.ctGrid.z,ct.cube{s}, ... dij.doseGrid.x,dij.doseGrid.y',dij.doseGrid.z,'linear'); end - + % Allocate temporary resampled CT this.ctR = ct; this.ctR.cube = cell(1); this.ctR.cubeHU = cell(1); - + % Set CT resolution to doseGrid resolution this.ctR.resolution = dij.doseGrid.resolution; this.ctR.cubeDim = dij.doseGrid.dimensions; this.ctR.x = dij.doseGrid.x; this.ctR.y = dij.doseGrid.y; this.ctR.z = dij.doseGrid.z; - + % Write resampled cubes this.ctR.cubeHU = cubeHUresampled; this.ctR.cube = cubeResampled; - + % Set flag for complete resampling this.ctR.resampled = 1; this.ctR.ctGrid = dij.doseGrid; - + % Save original grid this.ctR.originalGrid = dij.ctGrid; matRad_cfg.dispInfo('done!\n'); @@ -733,19 +791,27 @@ function writeAllFiles(obj,ct,cst,stf,machine,w) end % Normalize with histories and particles/weight - correctionFactor = obj.numParticlesPerHistory / double(obj.MCparam.nbHistoriesTotal); + correctionFactor = obj.numParticlesPerWeight / double(obj.MCparam.nbHistoriesTotal); % Get all saved quantities % Make sure that the filename always ends on 'run1_tally' switch obj.MCparam.outputType case 'csv' - searchstr = 'score_matRad_plan_field1_run1_*.csv'; + if obj.MCparam.numOfCtScen > 1 && ~obj.calc4DInterplay + searchstr = 'score_matRad_plan_field1_ct1_run1_*.bin'; + else + searchstr = 'score_matRad_plan_field1_run1_*.bin'; + end files = dir([folder filesep searchstr]); %obj.MCparam.tallies = cellfun(@(x) extractBetween(x,'run1_','.csv') ,{files(:).name}); %Not Octave compatible nameBegin = strfind(searchstr,'*'); obj.MCparam.tallies = cellfun(@(s) s(nameBegin:end-4),{files(:).name},'UniformOutput',false); case 'binary' - searchstr = 'score_matRad_plan_field1_run1_*.bin'; + if obj.MCparam.numOfCtScen > 1 && ~obj.calc4DInterplay + searchstr = 'score_matRad_plan_field1_ct1_run1_*.bin'; + else + searchstr = 'score_matRad_plan_field1_run1_*.bin'; + end files = dir([folder filesep searchstr]); %obj.MCparam.tallies = cellfun(@(x) extractBetween(x,'run1_','.bin') ,{files(:).name}); %Not Octave compatible nameBegin = strfind(searchstr,'*'); @@ -753,6 +819,10 @@ function writeAllFiles(obj,ct,cst,stf,machine,w) end obj.MCparam.tallies = unique(obj.MCparam.tallies); + if obj.calc4DInterplay + obj.MCparam.tallies = unique(cellfun(@(x) extractBefore(x, '-'), obj.MCparam.tallies, 'UniformOutput',false)); + obj.MCparam.tallies(1) =[]; + end talliesCut = strrep(obj.MCparam.tallies,'-','_'); % Load data for each tally individually @@ -766,8 +836,10 @@ function writeAllFiles(obj,ct,cst,stf,machine,w) % Loop over all batches/runs for k = 1:obj.MCparam.nbRuns % Get file name of current field, run and tally (and ct, if applicable) - if obj.MCparam.numOfCtScen > 1 + if obj.MCparam.numOfCtScen > 1 && ~obj.calc4DInterplay genFileName = sprintf('score_%s_field%d_ct%d_run%d_%s',obj.MCparam.simLabel,f,ctScen,k,tnameFile); + elseif obj.calc4DInterplay + genFileName = sprintf('score_%s_field%d_run%d_%s-matRad_cube%d',obj.MCparam.simLabel,f,k,tnameFile,ctScen); else genFileName = sprintf('score_%s_field%d_run%d_%s',obj.MCparam.simLabel,f,k,tnameFile); end @@ -838,7 +910,15 @@ function writeAllFiles(obj,ct,cst,stf,machine,w) % Tally per field if isfield(topasSum,'Sum') - topasCube.([tname '_beam' num2str(f)]){ctScen} = topasSum.Sum; + if obj.calc4DInterplay || obj.MCparam.numOfCtScen > 1 + if strcmp(tname, 'physicalDose') + topasCube.(['phaseDose_beam' num2str(f)]){ctScen} = topasSum.Sum; + else + topasCube.(['phase' tname '_beam' num2str(f)]){ctScen} = topasSum.Sum; + end + else + topasCube.([tname '_beam' num2str(f)]){ctScen} = topasSum.Sum; + end end if isfield(topasSum,'Standard_Deviation') topasCube.([tname '_std_beam' num2str(f)]){ctScen} = topasSum.Standard_Deviation; @@ -995,7 +1075,7 @@ function writeAllFiles(obj,ct,cst,stf,machine,w) % Allocate possible scored quantities processedQuantities = {'','_std','_batchStd'}; topasCubesTallies = unique(erase(topasCubesTallies,processedQuantities(2:end))); - + % Loop through 4D scenarios for ctScen = 1:dij.numOfScenarios @@ -1084,6 +1164,9 @@ function writeAllFiles(obj,ct,cst,stf,machine,w) % Check if current quantity is available and write to dij if isfield(topasCubes,[topasCubesTallies{j} processedQuantities{p} '_beam' num2str(d)]) && iscell(topasCubes.([topasCubesTallies{j} processedQuantities{p} '_beam' num2str(d)])) dij.([topasCubesTallies{j} processedQuantities{p}]){ctScen}(:,d) = sum(w)*reshape(topasCubes.([topasCubesTallies{j} processedQuantities{p} '_beam',num2str(d)]){ctScen},[],1); + if strcmp(topasCubesTallies{j}, 'phaseDose') + dij.(['physicalDose' processedQuantities{p}]){ctScen}(:,d) = dij.([topasCubesTallies{j} processedQuantities{p}]){ctScen}(:,d); + end end end % Handle RBE-related quantities (not multiplied by sum(w)!) @@ -1153,13 +1236,13 @@ function writeRunHeader(obj,fID,fieldIx,runIx,ctScen) fprintf(fID,'\n'); fprintf(fID,['i:Ts/Seed = ',num2str(runIx),'\n']); - %TODO: remove or document + %TODO: remove or document %fprintf(fID,'includeFile = %s/TOPAS_Simulation_Setup.txt\n',obj.thisFolder); %fprintf(fID,'includeFile = %s/TOPAS_matRad_geometry.txt\n',obj.thisFolder); %fprintf(fID,'includeFile = %s/TOPAS_scorer_surfaceIC.txt\n',obj.thisFolder); end - function writeFieldHeader(obj,fID,ctScen) + function writeFieldHeader(obj,fID,ctScen,beamIx) %TODO: Insert documentation matRad_cfg = MatRad_Config.instance(); %Instance of matRad configuration class @@ -1180,14 +1263,21 @@ function writeFieldHeader(obj,fID,ctScen) fprintf(fID,'\n'); % Add ctScen number to filenames - if exist('ctScen','var') + if exist('ctScen','var') && ~isempty(ctScen) paramFile = strsplit(obj.outfilenames.patientParam,'.'); paramFile = strjoin(paramFile,[num2str(ctScen) '.']); else paramFile = obj.outfilenames.patientParam; end - fprintf(fID,'includeFile = %s\n',paramFile); + if obj.calc4DInterplay + paramFile = strsplit(paramFile,'.'); + paramFile{1} = [paramFile{1} '_field']; + paramFile = strjoin(paramFile,[num2str(beamIx) '.']); + fprintf(fID, 'includeFile = %s \n', paramFile); + else + fprintf(fID,'includeFile = %s\n',paramFile); + end fprintf(fID,'\n'); fname = fullfile(obj.topasFolder,obj.infilenames.geometry); @@ -1197,7 +1287,7 @@ function writeFieldHeader(obj,fID,ctScen) end - function writeScorers(obj,fID) + function writeScorers(obj,fID,beamIx) %TODO: Insert documentation matRad_cfg = MatRad_Config.instance(); %Instance of matRad configuration class @@ -1210,6 +1300,22 @@ function writeScorers(obj,fID) scorerName = fileread(fname); fprintf(fID,'\n%s\n\n',scorerName); + if obj.calc4DInterplay + for PhaseNum = obj.MCparam.Phases{beamIx}' + scorerTxt = fileread(fname); + scorerTxt = strrep(scorerTxt, '/Patient', ['/Patient' num2str(PhaseNum)]); + scorerTxt = strrep(scorerTxt, '_physicalDose', ['_physicalDose-matRad_cube' num2str(PhaseNum)]); + fprintf(fID,'\n%s\n\n',scorerTxt); + fprintf(fID, 'b:Sc/Patient%i/Tally_DoseToMedium/Active = Tf/Patient%i/Tally_DoseToMedium/Active/Value\n', PhaseNum, PhaseNum); + fprintf(fID,'s:Tf/Patient%i/Tally_DoseToMedium/Active/Function = "Step"\n', PhaseNum); + fprintf(fID,'dv:Tf/Patient%i/Tally_DoseToMedium/Active/Times= %i ', PhaseNum, obj.MCparam.cutNumOfBixel(beamIx)); + fprintf(fID,'%i ',linspace(10,obj.MCparam.cutNumOfBixel(beamIx)*10,obj.MCparam.cutNumOfBixel(beamIx))); + fprintf(fID,' ms\n'); + fprintf(fID,'bv:Tf/Patient%i/Tally_DoseToMedium/Active/Values = %d %s \n ', PhaseNum, obj.MCparam.numPhasesTimeFeature(beamIx), strjoin(obj.MCparam.isInPhase{PhaseNum,beamIx})); + fprintf(fID, '\n'); + end + end + % Update MCparam.tallies with processed scorer obj.MCparam.tallies = [obj.MCparam.tallies,{'physicalDose'}]; end @@ -1223,8 +1329,10 @@ function writeScorers(obj,fID) if ~isempty(strfind(lower(obj.scorer.RBE_model{i}),'mcn')) fname = fullfile(obj.topasFolder,filesep,obj.scorerFolder,filesep,obj.infilenames.Scorer_RBE_MCN); + ixToWrite4D = [4,5]; elseif ~isempty(strfind(lower(obj.scorer.RBE_model{i}),'wed')) fname = fullfile(obj.topasFolder,filesep,obj.scorerFolder,filesep,obj.infilenames.Scorer_RBE_WED); + ixToWrite4D = [6,7]; else matRad_cfg.dispError(['Model ',obj.scorer.RBE_model{i},' not implemented for ',obj.radiationMode]); end @@ -1232,8 +1340,10 @@ function writeScorers(obj,fID) % Process available varRBE models for carbon and helium if ~isempty(strfind(lower(obj.scorer.RBE_model{i}),'libamtrack')) fname = fullfile(obj.topasFolder,filesep,obj.scorerFolder,filesep,obj.infilenames.Scorer_RBE_libamtrack); + ixToWrite4D = [1,2,3]; elseif ~isempty(strfind(lower(obj.scorer.RBE_model{i}),'lem')) fname = fullfile(obj.topasFolder,filesep,obj.scorerFolder,filesep,obj.infilenames.Scorer_RBE_LEM1); + ixToWrite4D = [1,2,3]; else matRad_cfg.dispError(['Model ',obj.scorer.RBE_model{i},' not implemented for ',obj.radiationMode]); end @@ -1246,6 +1356,47 @@ function writeScorers(obj,fID) matRad_cfg.dispDebug('Reading RBE Scorer from %s\n',fname); scorerName = fileread(fname); fprintf(fID,'\n%s\n\n',scorerName); + + if obj.calc4DInterplay + for PhaseNum = obj.MCparam.Phases{beamIx}' + scorerTxt = fileread(fname); + scorerTxt = strrep(scorerTxt, 'Alpha/', ['Alpha' num2str(PhaseNum) '/']); + scorerTxt = strrep(scorerTxt, 'Beta/', ['Beta' num2str(PhaseNum) '/']); + scorerTxt = strrep(scorerTxt, '/RBE', ['/RBE' num2str(PhaseNum)]); + if ~isempty(regexp(obj.scorer.RBE_model{i}, 'mcn', 'ignorecase')) + scorerTxt = strrep(scorerTxt, '_alpha_MCN', ['_alpha_MCN-matRad_cube' num2str(PhaseNum)]); + scorerTxt = strrep(scorerTxt, '_beta_MCN', ['_beta_MCN-matRad_cube' num2str(PhaseNum)]); + elseif ~isempty(regexp(obj.scorer.RBE_model{i}, 'wed', 'ignorecase')) + scorerTxt = strrep(scorerTxt, '_alpha_WED', ['_alpha_WED-matRad_cube' num2str(PhaseNum)]); + scorerTxt = strrep(scorerTxt, '_beta_WED', ['_beta_WED-matRad_cube' num2str(PhaseNum)]); + elseif ~isempty(regexp(obj.scorer.RBE_model{i}, 'lem', 'ignorecase')) + scorerTxt = strrep(scorerTxt, '_alpha_LEM', ['_alpha_LEM-matRad_cube' num2str(PhaseNum)]); + scorerTxt = strrep(scorerTxt, '_beta_LEM', ['_beta_LEM-matRad_cube' num2str(PhaseNum)]); + scorerTxt = strrep(scorerTxt, '_RBE_LEM', ['_RBE_LEM-matRad_cube' num2str(PhaseNum)]); + elseif ~isempty(regexp(obj.scorer.RBE_model{i}, 'libamtrack', 'ignorecase')) + scorerTxt = strrep(scorerTxt, '_alpha_libamtrack', ['_alpha_libamtrack-matRad_cube' num2str(PhaseNum)]); + scorerTxt = strrep(scorerTxt, '_beta_libamtrack', ['_beta_libamtrack-matRad_cube' num2str(PhaseNum)]); + scorerTxt = strrep(scorerTxt, '_RBE_libamtrack', ['_RBE_libamtrack-matRad_cube' num2str(PhaseNum)]); + end + %dont allways write cell lines + idx = strfind(scorerTxt, '### HCP Tabulated ###'); + if ~isempty(idx) + scorerTxt = scorerTxt(1:idx(1)-1); + end + fprintf(fID,'\n%s\n\n',scorerTxt); + scorerInString = {'tabulatedAlpha', 'tabulatedBeta', 'RBE', 'McNamaraAlpha', 'McNamaraBeta', 'WedenbergAlpha', 'WedenbergBeta'}; + for ixWrite = ixToWrite4D + fprintf(fID,'b:Sc/%s%i/Active = Tf/%s%i/Active/Value\n', scorerInString{ixWrite},PhaseNum,scorerInString{ixWrite},PhaseNum); + fprintf(fID,'s:Tf/%s%i/Active/Function = "Step"\n', scorerInString{ixWrite}, PhaseNum); + fprintf(fID,'dv:Tf/%s%i/Active/Times= %i ',scorerInString{ixWrite},PhaseNum, obj.MCparam.cutNumOfBixel(beamIx)); + fprintf(fID,'%i ',linspace(10,obj.MCparam.cutNumOfBixel(beamIx)*10,obj.MCparam.cutNumOfBixel(beamIx))); + fprintf(fID,' ms\n'); + fprintf(fID,'bv:Tf/%s%i/Active/Values = %d %s \n ',scorerInString{ixWrite}, PhaseNum, obj.MCparam.numPhasesTimeFeature(beamIx), strjoin(obj.MCparam.isInPhase{PhaseNum,beamIx})); + fprintf(fID,'\n'); + end + end + end + end % Begin writing biological scorer components: cell lines @@ -1303,6 +1454,14 @@ function writeScorers(obj,fID) fprintf(fID,'s:Sc/%s%s/ReferencedSubScorer_LET = "ProtonLET"\n',scorerPrefix,scorerNames{s}); end fprintf(fID,'s:Sc/%s%s/ReferencedSubScorer_Dose = "Tally_DoseToWater"\n',scorerPrefix,scorerNames{s}); + if obj.calc4DInterplay + for PhaseNum = obj.MCparam.Phases{beamIx}' + if strcmp(obj.radiationMode,'protons') + fprintf(fID,'s:Sc/%s%s%i/ReferencedSubScorer_LET = "ProtonLET%i"\n',scorerPrefix,scorerNames{s},PhaseNum,PhaseNum); + end + fprintf(fID,'s:Sc/%s%s%i/ReferencedSubScorer_Dose = "Tally_DoseToWater%i"\n',scorerPrefix,scorerNames{s},PhaseNum,PhaseNum); + end + end end end @@ -1315,6 +1474,23 @@ function writeScorers(obj,fID) % Update MCparam.tallies with processed scorer obj.MCparam.tallies = [obj.MCparam.tallies,{'doseToWater'}]; + + if obj.calc4DInterplay + for PhaseNum = obj.MCparam.Phases{beamIx}' + scorerTxt = fileread(fname); + scorerTxt = strrep(scorerTxt, 'Tally_DoseToWater', ['Tally_DoseToWater' num2str(PhaseNum)]); + scorerTxt = strrep(scorerTxt, '_doseToWater', ['_doseToWater-matRad_cube' num2str(PhaseNum)]); + fprintf(fID,'\n%s\n\n',scorerTxt); + fprintf(fID, 'b:Sc/Tally_DoseToWater%i/Active = Tf/Tally_DoseToWater%i/Active/Value\n', PhaseNum, PhaseNum); + fprintf(fID,'s:Tf/Tally_DoseToWater%i/Active/Function = "Step"\n', PhaseNum); + fprintf(fID,'dv:Tf/Tally_DoseToWater%i/Active/Times= %i ', PhaseNum, obj.MCparam.cutNumOfBixel(beamIx)); + fprintf(fID,'%i ',linspace(10,obj.MCparam.cutNumOfBixel(beamIx)*10,obj.MCparam.cutNumOfBixel(beamIx))); + fprintf(fID,' ms\n'); + fprintf(fID,'bv:Tf/Tally_DoseToWater%i/Active/Values = %d %s \n ', PhaseNum, obj.MCparam.numPhasesTimeFeature(beamIx), strjoin(obj.MCparam.isInPhase{PhaseNum,beamIx})); + fprintf(fID, '\n'); + end + end + end % write LET scorer from file @@ -1327,6 +1503,22 @@ function writeScorers(obj,fID) % Update MCparam.tallies with processed scorer obj.MCparam.tallies = [obj.MCparam.tallies,{'LET'}]; + + if obj.calc4DInterplay + for PhaseNum = obj.MCparam.Phases{beamIx}' + scorerTxt = fileread(fname); + scorerTxt = strrep(scorerTxt, '/ProtonLET', ['/ProtonLET' num2str(PhaseNum)]); + scorerTxt = strrep(scorerTxt, '_LET', ['_LET-matRad_cube' num2str(PhaseNum)]); + fprintf(fID,'\n%s\n\n',scorerTxt); + fprintf(fID, 'b:Sc/ProtonLET%i/Active = Tf/ProtonLET%i/Active/Value\n', PhaseNum, PhaseNum); + fprintf(fID,'s:Tf/ProtonLET%i/Active/Function = "Step"\n', PhaseNum); + fprintf(fID,'dv:Tf/ProtonLET%i/Active/Times= %i ', PhaseNum, obj.MCparam.cutNumOfBixel(beamIx)); + fprintf(fID,'%i ',linspace(10,obj.MCparam.cutNumOfBixel(beamIx)*10,obj.MCparam.cutNumOfBixel(beamIx))); + fprintf(fID,' ms\n'); + fprintf(fID,'bv:Tf/ProtonLET%i/Active/Values = %d %s \n ', PhaseNum, obj.MCparam.numPhasesTimeFeature(beamIx), strjoin(obj.MCparam.isInPhase{PhaseNum,beamIx})); + fprintf(fID, '\n'); + end + end else matRad_cfg.dispError('LET in TOPAS only for protons!\n'); end @@ -1437,9 +1629,9 @@ function writeStfFields(obj,ct,stf,w,baseData) matRad_cfg.dispError('Given number of weights (#%d) doesn''t match bixel count in stf (#%d)',numel(w), sum([stf(:).totalNumOfBixels])); end - nParticlesTotalBixel = round(obj.numParticlesPerHistory * w); + nParticlesTotalBixel = round(obj.numParticlesPerWeight * w); nParticlesTotal = sum(nParticlesTotalBixel); - maxParticlesBixel = obj.numParticlesPerHistory * max(w(:)); + maxParticlesBixel = obj.numParticlesPerWeight * max(w(:)); minParticlesBixel = round(max([obj.minRelWeight*maxParticlesBixel,1])); switch obj.modeHistories @@ -1489,7 +1681,6 @@ function writeStfFields(obj,ct,stf,w,baseData) % Set variables for loop over beams nBeamParticlesTotal = zeros(1,length(stf)); currentBixel = 1; - bixelNotMeetingParticleQuota = 0; historyCount = zeros(1,length(stf)); for beamIx = 1:length(stf) @@ -1514,8 +1705,8 @@ function writeStfFields(obj,ct,stf,w,baseData) selectedData = []; focusIndex = baseData.selectedFocus(baseData.energyIndex); - scalarFields = {'NominalEnergy','EnergySpread','MeanEnergy'}; - + scalarFields = {'NominalEnergy','EnergySpread','MeanEnergy'}; + for i = 1:numel(focusIndex) for field = scalarFields baseData.monteCarloData(i).(field{1}) = ones(1,max(focusIndex))*baseData.monteCarloData(i).(field{1}); @@ -1550,96 +1741,101 @@ function writeStfFields(obj,ct,stf,w,baseData) % Clear dataTOPAS from the previous beam dataTOPAS = []; + totNumBixel = [0,[stf(:).totalNumOfBixels]]; + %Loop over rays and then over spots on ray for rayIx = 1:stf(beamIx).numOfRays for bixelIx = 1:stf(beamIx).numOfBixelsPerRay(rayIx) nCurrentParticles = nParticlesTotalBixel(currentBixel); - % check whether there are (enough) particles for beam delivery - if (nCurrentParticles>minParticlesBixel) - - % collectBixelIdx(end+1) = bixelIx; - cutNumOfBixel = cutNumOfBixel + 1; - bixelEnergy = stf(beamIx).ray(rayIx).energy(bixelIx); + cutNumOfBixel = cutNumOfBixel + 1; + bixelEnergy = stf(beamIx).ray(rayIx).energy(bixelIx); - dataTOPAS(cutNumOfBixel).posX = stf(beamIx).ray(rayIx).rayPos_bev(3); - dataTOPAS(cutNumOfBixel).posY = stf(beamIx).ray(rayIx).rayPos_bev(1); + dataTOPAS(cutNumOfBixel).posX = stf(beamIx).ray(rayIx).rayPos_bev(3); + dataTOPAS(cutNumOfBixel).posY = stf(beamIx).ray(rayIx).rayPos_bev(1); + % check whether there are (enough) particles for beam delivery + if (nCurrentParticles<=minParticlesBixel) + dataTOPAS(cutNumOfBixel).current = 0; + else dataTOPAS(cutNumOfBixel).current = uint32(obj.fracHistories * nCurrentParticles / obj.numOfRuns); + end + + obj.MCparam.order{beamIx,1}(cutNumOfBixel) = currentBixel; + if obj.calc4DInterplay + dataTOPAS(cutNumOfBixel).order = obj.calcTimeSequence(beamIx).orderToSTF(currentBixel - totNumBixel(beamIx)); + end - if obj.pencilBeamScanning - % angleX corresponds to the rotation around the X axis necessary to move the spot in the Y direction - % angleY corresponds to the rotation around the Y' axis necessary to move the spot in the X direction - % note that Y' corresponds to the Y axis after the rotation of angleX around X axis - % note that Y translates to -Y for TOPAS - dataTOPAS(cutNumOfBixel).angleX = atan(dataTOPAS(cutNumOfBixel).posY / SAD); - dataTOPAS(cutNumOfBixel).angleY = atan(-dataTOPAS(cutNumOfBixel).posX ./ (SAD ./ cos(dataTOPAS(cutNumOfBixel).angleX))); - % Translate posX and posY to patient coordinates - dataTOPAS(cutNumOfBixel).posX = (dataTOPAS(cutNumOfBixel).posX / SAD)*(SAD-nozzleToAxisDistance); - dataTOPAS(cutNumOfBixel).posY = (dataTOPAS(cutNumOfBixel).posY / SAD)*(SAD-nozzleToAxisDistance); - end + if obj.pencilBeamScanning + % angleX corresponds to the rotation around the X axis necessary to move the spot in the Y direction + % angleY corresponds to the rotation around the Y' axis necessary to move the spot in the X direction + % note that Y' corresponds to the Y axis after the rotation of angleX around X axis + % note that Y translates to -Y for TOPAS + dataTOPAS(cutNumOfBixel).angleX = atan(dataTOPAS(cutNumOfBixel).posY / SAD); + dataTOPAS(cutNumOfBixel).angleY = atan(-dataTOPAS(cutNumOfBixel).posX ./ (SAD ./ cos(dataTOPAS(cutNumOfBixel).angleX))); + % Translate posX and posY to patient coordinates + dataTOPAS(cutNumOfBixel).posX = (dataTOPAS(cutNumOfBixel).posX / SAD)*(SAD-nozzleToAxisDistance); + dataTOPAS(cutNumOfBixel).posY = (dataTOPAS(cutNumOfBixel).posY / SAD)*(SAD-nozzleToAxisDistance); + end - switch obj.radiationMode - case {'protons','carbon','helium'} - [~,ixTmp,~] = intersect(energies, bixelEnergy); - if obj.useOrigBaseData - dataTOPAS(cutNumOfBixel).energy = selectedData(ixTmp).energy; - dataTOPAS(cutNumOfBixel).focusFWHM = selectedData(ixTmp).initFocus.SisFWHMAtIso(stf(beamIx).ray(rayIx).focusIx(bixelIx)); + switch obj.radiationMode + case {'protons','carbon','helium','VHEE'} + [~,ixTmp,~] = intersect(energies, bixelEnergy); + if obj.useOrigBaseData + dataTOPAS(cutNumOfBixel).energy = selectedData(ixTmp).energy; + dataTOPAS(cutNumOfBixel).focusFWHM = selectedData(ixTmp).initFocus.SisFWHMAtIso(stf(beamIx).ray(rayIx).focusIx(bixelIx)); - else - dataTOPAS(cutNumOfBixel).energy = selectedData(ixTmp).MeanEnergy; - dataTOPAS(cutNumOfBixel).nominalEnergy = selectedData(ixTmp).NominalEnergy; - dataTOPAS(cutNumOfBixel).energySpread = selectedData(ixTmp).EnergySpread; - dataTOPAS(cutNumOfBixel).spotSizeX = selectedData(ixTmp).SpotSize1x; - dataTOPAS(cutNumOfBixel).divergenceX = selectedData(ixTmp).Divergence1x; - dataTOPAS(cutNumOfBixel).correlationX = selectedData(ixTmp).Correlation1x; - dataTOPAS(cutNumOfBixel).spotSizeY = selectedData(ixTmp).SpotSize1y; - dataTOPAS(cutNumOfBixel).divergenceY = selectedData(ixTmp).Divergence1y; - dataTOPAS(cutNumOfBixel).correlationY = selectedData(ixTmp).Correlation1y; - dataTOPAS(cutNumOfBixel).focusFWHM = selectedData(ixTmp).FWHMatIso; - end - case 'photons' - dataTOPAS(cutNumOfBixel).energy = bixelEnergy; - dataTOPAS(cutNumOfBixel).energySpread = 0; - end + else + dataTOPAS(cutNumOfBixel).energy = selectedData(ixTmp).MeanEnergy; + dataTOPAS(cutNumOfBixel).nominalEnergy = selectedData(ixTmp).NominalEnergy; + dataTOPAS(cutNumOfBixel).energySpread = selectedData(ixTmp).EnergySpread; + dataTOPAS(cutNumOfBixel).spotSizeX = selectedData(ixTmp).SpotSize1x; + dataTOPAS(cutNumOfBixel).divergenceX = selectedData(ixTmp).Divergence1x; + dataTOPAS(cutNumOfBixel).correlationX = selectedData(ixTmp).Correlation1x; + dataTOPAS(cutNumOfBixel).spotSizeY = selectedData(ixTmp).SpotSize1y; + dataTOPAS(cutNumOfBixel).divergenceY = selectedData(ixTmp).Divergence1y; + dataTOPAS(cutNumOfBixel).correlationY = selectedData(ixTmp).Correlation1y; + dataTOPAS(cutNumOfBixel).focusFWHM = selectedData(ixTmp).FWHMatIso; + end + case 'photons' + dataTOPAS(cutNumOfBixel).energy = bixelEnergy; + dataTOPAS(cutNumOfBixel).energySpread = 0; + end - if obj.scorer.calcDij - % remember beam and bixel number - dataTOPAS(cutNumOfBixel).beam = beamIx; - dataTOPAS(cutNumOfBixel).ray = rayIx; - dataTOPAS(cutNumOfBixel).bixel = bixelIx; - dataTOPAS(cutNumOfBixel).totalBixel = currentBixel; - end + if obj.scorer.calcDij + % remember beam and bixel number + dataTOPAS(cutNumOfBixel).beam = beamIx; + dataTOPAS(cutNumOfBixel).ray = rayIx; + dataTOPAS(cutNumOfBixel).bixel = bixelIx; + dataTOPAS(cutNumOfBixel).totalBixel = currentBixel; + end - %Add RangeShifterState - if exist('raShis','var') && ~isempty(raShis) - raShiOut = zeros(1,length(raShis)); - for r = 1:length(raShis) - if stf(beamIx).ray(rayIx).rangeShifter(bixelIx).ID == raShis(r).ID - raShiOut(r) = 0; %Range shifter is in beam path - else - raShiOut(r) = 1; %Range shifter is out of beam path / not used - end + %Add RangeShifterState + if exist('raShis','var') && ~isempty(raShis) + raShiOut = zeros(1,length(raShis)); + for r = 1:length(raShis) + if stf(beamIx).ray(rayIx).rangeShifter(bixelIx).ID == raShis(r).ID + raShiOut(r) = 0; %Range shifter is in beam path + else + raShiOut(r) = 1; %Range shifter is out of beam path / not used end - dataTOPAS(cutNumOfBixel).raShiOut = raShiOut; end - - nBeamParticlesTotal(beamIx) = nBeamParticlesTotal(beamIx) + nCurrentParticles; - - + dataTOPAS(cutNumOfBixel).raShiOut = raShiOut; end + nBeamParticlesTotal(beamIx) = nBeamParticlesTotal(beamIx) + nCurrentParticles; currentBixel = currentBixel + 1; + end end - bixelNotMeetingParticleQuota = bixelNotMeetingParticleQuota + (stf(beamIx).totalNumOfBixels-cutNumOfBixel); - % discard data if the current has unphysical values idx = find([dataTOPAS.current] < 1); - dataTOPAS(idx) = []; + if ~isempty(idx) + [dataTOPAS(idx).current] = deal(0); + end % Safety check for empty beam (not allowed) if isempty(dataTOPAS) @@ -1649,8 +1845,13 @@ function writeStfFields(obj,ct,stf,w,baseData) end % Sort dataTOPAS according to energy - if length(dataTOPAS)>1 && ~issorted([dataTOPAS(:).energy]) - [~,ixSorted] = sort([dataTOPAS(:).energy]); + if length(dataTOPAS)>1 + if obj.calc4DInterplay + [~,ixSorted] = sort([dataTOPAS(:).order]); + obj.MCparam.orderToSS{beamIx,1} = obj.MCparam.order{beamIx,1}(ixSorted); + else + [~,ixSorted] = sort([dataTOPAS(:).energy]); + end dataTOPAS = dataTOPAS(ixSorted); end @@ -1664,19 +1865,21 @@ function writeStfFields(obj,ct,stf,w,baseData) % Check if current has the set amount of histories % If needed, adjust current to actual histories (by adding/subtracting from random rays) while sum([dataTOPAS(:).current]) ~= historyCount(beamIx) + idxLargerZero = find([dataTOPAS.current] >0); diff = sum([dataTOPAS.current]) - sum(historyCount(beamIx)); if matRad_cfg.isMatlab - [~,~,R] = histcounts(rand(abs(diff),1),cumsum([0;double(transpose([dataTOPAS(:).current]))./double(sum([dataTOPAS(:).current]))])); + [~,~,R] = histcounts(rand(abs(diff),1),cumsum([0;double(transpose([dataTOPAS(idxLargerZero).current]))./double(sum([dataTOPAS(idxLargerZero).current]))])); else - [~,R] = histc(rand(abs(diff),1),cumsum([0;double(transpose([dataTOPAS(:).current]))./double(sum([dataTOPAS(:).current]))])); + [~,R] = histc(rand(abs(diff),1),cumsum([0;double(transpose([dataTOPAS(idxLargerZero).current]))./double(sum([dataTOPAS(idxLargerZero).current]))])); end - idx = 1:length(dataTOPAS); + idx = 1:length(dataTOPAS(idxLargerZero)); randIx = idx(R); - newCurr = num2cell(arrayfun(@plus,double([dataTOPAS(randIx).current]),-1*sign(diff)*ones(1,abs(diff))),1); - [dataTOPAS(randIx).current] = newCurr{:}; + newCurr = num2cell(arrayfun(@plus,double([dataTOPAS(idxLargerZero(randIx)).current]),-1*sign(diff)*ones(1,abs(diff))),1); + [dataTOPAS(idxLargerZero(randIx)).current] = newCurr{:}; end + % Previous histories were set per run historyCount(beamIx) = historyCount(beamIx) * obj.numOfRuns; @@ -1685,15 +1888,15 @@ function writeStfFields(obj,ct,stf,w,baseData) % 4D case fieldSetupFileName = sprintf('beamSetup_%s_field%d_ct%d.txt',obj.label,beamIx,ct.currCtScen); fileID = fopen(fullfile(obj.workingDir,fieldSetupFileName),'w'); - obj.writeFieldHeader(fileID,ct.currCtScen); + obj.writeFieldHeader(fileID,ct.currCtScen,beamIx); else fieldSetupFileName = sprintf('beamSetup_%s_field%d.txt',obj.label,beamIx); fileID = fopen(fullfile(obj.workingDir,fieldSetupFileName),'w'); - obj.writeFieldHeader(fileID); + obj.writeFieldHeader(fileID,[],beamIx); end % NozzleAxialDistance - if isPhoton + if isPhoton fprintf(fileID,'d:Ge/Nozzle/TransZ = -%f mm\n', stf(beamIx).SCD+40); %Phasespace hardcorded infront of MLC at SSD 46 cm else fprintf(fileID,'d:Ge/Nozzle/TransZ = -%f mm\n', nozzleToAxisDistance); @@ -1743,6 +1946,15 @@ function writeStfFields(obj,ct,stf,w,baseData) modules = obj.modules_photons; + case 'VHEE' + fprintf(fileID,'s:Sim/ParticleName = "e-"\n'); + fprintf(fileID,'u:Sim/ParticleMass = 5.4462e-04\n'); + + particleA = 1; + % particleZ = 0; + + modules = obj.modules_VHEE; + otherwise matRad_cfg.dispError('Invalid radiation mode %s!',stf.radiationMode) end @@ -1885,7 +2097,7 @@ function writeStfFields(obj,ct,stf,w,baseData) fprintf(fileID,'d:Sim/GantryAngle = %f deg\n',stf(beamIx).gantryAngle); %just one beam angle for now fprintf(fileID,'d:Sim/CouchAngle = %f deg\n',stf(beamIx).couchAngle); - % Here the phasespace file is loaded and referenced in the beamSetup file + % Here the phasespace file is loaded and referenced in the beamSetup file if strcmp(obj.externalCalculation,'write') matRad_cfg.dispWarning(['External calculation and phaseSpace selected, manually place ' obj.infilenames.phaseSpaceSourcePhotons '.header and ' obj.infilenames.phaseSpaceSourcePhotons '.phsp into your simulation directory.']); else @@ -1894,7 +2106,7 @@ function writeStfFields(obj,ct,stf,w,baseData) end end %phasespaceStr = ['..' filesep 'beamSetup' filesep 'phasespace' filesep phaseSpaceFileName]; - %&phasespaceStr = replace(phasespaceStr, '\', '/'); + %&phasespaceStr = strrep(phasespaceStr, '\', '/'); fprintf(fileID,'s:So/Phasespace/PhaseSpaceFileName = "%s"\n', obj.infilenames.phaseSpaceSourcePhotons ); end @@ -2035,6 +2247,53 @@ function writeStfFields(obj,ct,stf,w,baseData) end + % Input 4DCT data + if obj.calc4DInterplay + + if isempty(obj.calcTimeSequence) + matRad_cfg.dispError('Time Sequence Data required for 4D calculation \n'); + end + + % Write changing CT phases in matRad_cube file + outfilePatient = obj.outfilenames.patientParam; + outfilePatient = strsplit(outfilePatient,'.'); + outfilePatient{1} = [outfilePatient{1} '_field']; + outfilePatient = strjoin(outfilePatient,[num2str(beamIx) '.']); + outfilePatient = fullfile(obj.workingDir, outfilePatient); + fIDPatient = fopen(outfilePatient,'a'); + PhaseNum = obj.calcTimeSequence(beamIx).phaseNum(obj.MCparam.orderToSS{beamIx,1}-totNumBixel(beamIx)); + fileNamesPhases = cell(1); + for i4D = 1:cutNumOfBixel + fileNamesPhases{1,i4D} = ['matRad_cube', num2str(PhaseNum(i4D)), '.dat']; + end + fileNamesPhases = cellfun(@(s) sprintf('"%s"',s),fileNamesPhases,'UniformOutput',false); + fprintf(fIDPatient, '\n'); + fprintf(fIDPatient, 'b:Ge/Patient/PreLoadAllMaterials = "True"\n'); + fprintf(fIDPatient,'s:Tf/InputFile/Function = "Step"\n'); + fprintf(fIDPatient,'dv:Tf/InputFile/Times= %i ', cutNumOfBixel); + fprintf(fIDPatient,'%i ',linspace(10,cutNumOfBixel*10,cutNumOfBixel)); + fprintf(fIDPatient,' ms\n'); + fprintf(fIDPatient,'sv:Tf/InputFile/Values = %d %s \n ', numel(fileNamesPhases),strjoin(fileNamesPhases,' ')); + fprintf(fIDPatient, '\n'); + fclose(fIDPatient); + + %write On of Feature into MCparam to create scorers + %later + uniquePhaseNum = unique(PhaseNum); + for iPhase = 1:numel(uniquePhaseNum) + isPhaseBool = PhaseNum == uniquePhaseNum(iPhase); + isPhaseBool = num2cell(isPhaseBool); + isPhaseBool = cellfun(@(s) num2str(s),isPhaseBool,'UniformOutput',false); + isPhaseBool = cellfun(@(s) sprintf('"%s"',s),isPhaseBool,'UniformOutput',false); + obj.MCparam.isInPhase{uniquePhaseNum(iPhase),beamIx} = isPhaseBool; + end + obj.MCparam.numPhases(beamIx) = numel(uniquePhaseNum); + obj.MCparam.Phases{beamIx} = uniquePhaseNum; + obj.MCparam.numPhasesTimeFeature(beamIx) = numel(fileNamesPhases); + obj.MCparam.cutNumOfBixel(beamIx) = cutNumOfBixel; + obj.MCparam.calc4DInterplay = true; + end + % Translate patient according to beam isocenter fprintf(fileID,'d:Ge/Patient/TransX = %f mm\n',0.5*ct.resolution.x*(ct.cubeDim(2)+1)-stf(beamIx).isoCenter(1)); fprintf(fileID,'d:Ge/Patient/TransY = %f mm\n',0.5*ct.resolution.y*(ct.cubeDim(1)+1)-stf(beamIx).isoCenter(2)); @@ -2069,7 +2328,7 @@ function writeStfFields(obj,ct,stf,w,baseData) fprintf(fileID,'includeFile = ./%s\n',fieldSetupFileName); % Write lines from scorer files - obj.writeScorers(fileID); + obj.writeScorers(fileID,beamIx); % Write dij-related config lines % TODO: move this to github issue/todo -> We should discuss here if that's something that has to be available for photons as well @@ -2089,8 +2348,8 @@ function writeStfFields(obj,ct,stf,w,baseData) end end - if bixelNotMeetingParticleQuota ~= 0 - matRad_cfg.dispWarning([num2str(bixelNotMeetingParticleQuota) ' bixels were discarded due to particle threshold.']) + if sum([dataTOPAS(:).current]==0) ~= 0 + matRad_cfg.dispWarning([num2str(sum([dataTOPAS(:).current]==0)),' bixels have 0 weight probably, due to particle threshold.']) end % Bookkeeping @@ -2147,239 +2406,266 @@ function writePatient(obj,ct,pln) ctScen = ct.currCtScen; paramFile = strsplit(paramFile,'.'); paramFile = strjoin(paramFile,[num2str(ct.currCtScen) '.']); - dataFile = strsplit(dataFile,'.'); dataFile = strjoin(dataFile,[num2str(ct.currCtScen) '.']); + ctScens = ctScen:ctScen; + fields = 1; + elseif obj.calc4DInterplay + ctScens = 1:ct.numOfCtScen; + fields = 1:size(obj.calcTimeSequence,2); + dataFileBasic = dataFile; else ctScen = 1; + ctScens = ctScen:ctScen; + fields = 1; end - % Open file to write in data - outfile = fullfile(obj.workingDir, paramFile); - matRad_cfg.dispInfo('Writing data to %s\n',outfile) - fID = fopen(outfile,'w+'); - % Write material converter - switch obj.materialConverter.mode - case 'RSP' % Relative stopping power converter - rspHlut = matRad_loadHLUT(ct,obj.radiationMode); - min_HU = rspHlut(1,1); - max_HU = rspHlut(end,1); - - huCube = int32(permute(ct.cubeHU{ctScen},permutation)); % X,Y,Z ordering - huCube(huCube < min_HU) = min_HU; - huCube(huCube > max_HU) = max_HU; - - unique_hu = unique(huCube(:)); - unique_rsp = matRad_interp1(rspHlut(:,1),rspHlut(:,2),double(unique_hu)); - fbase = fopen(['materialConverter/definedMaterials/' medium '.txt'],'r'); - while ~feof(fbase) - strLine = fgets(fbase); %# read line by line - fprintf(fID,'%s',strLine); - end - fclose(fbase); - - unique_materials = cell(1,length(unique_hu)); - for ix=1:length(unique_hu) - unique_materials{ix} = strrep(['Material_HU_',num2str(unique_hu(ix))],'-','m'); - fprintf(fID,'s:Ma/%s/BaseMaterial = "%s"\n',unique_materials{ix},medium); - fprintf(fID,'d:Ma/%s/Density = %f g/cm3\n',unique_materials{ix},unique_rsp(ix)); + for ctScen = ctScens + for field = fields + % Open file to write in data + if obj.calc4DInterplay + outfile = strsplit(paramFile,'.'); + outfile{1} = [outfile{1} '_field']; + outfile = strjoin(outfile,[num2str(field) '.']); + outfile = fullfile(obj.workingDir, outfile); + dataFile = strsplit(dataFileBasic,'.'); + dataFile = strjoin(dataFile,[num2str(ctScen) '.']); + else + outfile = fullfile(obj.workingDir, paramFile); end - - fprintf(fID,'s:Ge/Patient/Parent="Isocenter"\n'); - fprintf(fID,'s:Ge/Patient/Type = "TsImageCube"\n'); - fprintf(fID,'s:Ge/Patient/InputDirectory = "./"\n'); - fprintf(fID,'s:Ge/Patient/InputFile = "%s"\n',dataFile); - fprintf(fID,'s:Ge/Patient/ImagingtoMaterialConverter = "MaterialTagNumber"\n'); - fprintf(fID,'i:Ge/Patient/NumberOfVoxelsX = %d\n',ct.cubeDim(2)); - fprintf(fID,'i:Ge/Patient/NumberOfVoxelsY = %d\n',ct.cubeDim(1)); - fprintf(fID,'iv:Ge/Patient/NumberOfVoxelsZ = 1 %d\n',ct.cubeDim(3)); - fprintf(fID,'d:Ge/Patient/VoxelSizeX = %.3f mm\n',ct.resolution.x); - fprintf(fID,'d:Ge/Patient/VoxelSizeY = %.3f mm\n',ct.resolution.y); - fprintf(fID,'dv:Ge/Patient/VoxelSizeZ = 1 %.3f mm\n',ct.resolution.z); - fprintf(fID,'s:Ge/Patient/DataType = "SHORT"\n'); - fprintf(fID,'iv:Ge/Patient/MaterialTagNumbers = %d ',length(unique_hu)); - fprintf(fID,num2str(unique_hu','%d ')); - fprintf(fID,'\n'); - fprintf(fID,'sv:Ge/Patient/MaterialNames = %d ',length(unique_hu)); - fprintf(fID,'"%s"',strjoin(unique_materials,'" "')); - fprintf(fID,'\n'); - fclose(fID); - - % write data - fID = fopen(fullfile(obj.workingDir, dataFile),'w'); - fwrite(fID,huCube,'short'); - fclose(fID); - cube = huCube; - - - case 'HUToWaterSchneider' % Schneider converter - rspHlut = matRad_loadHLUT(ct,obj.radiationMode); - - try - % Write Schneider Converter - if ~obj.materialConverter.loadConverterFromFile - % define density correction - matRad_cfg.dispInfo('TOPAS: Writing density correction\n'); - switch obj.materialConverter.densityCorrection - case 'rspHLUT' - densityCorrection.density = []; - for i = 1:size(rspHlut,1)-1 - startVal = rspHlut(i,1); - endVal = rspHlut(i+1,1); - range = startVal:1:endVal-1; - densityCorrection.density(end+1:end+numel(range)) = matRad_interp1(rspHlut(:,1),rspHlut(:,2),range); - end - densityCorrection.density(end+1) = rspHlut(end,2); %add last missing value - densityCorrection.boundaries = [rspHlut(1,1) numel(densityCorrection.density)-abs(rspHlut(1,1))]; - - case {'Schneider_TOPAS','Schneider_matRad'} - fname = fullfile(obj.topasFolder,filesep,obj.converterFolder,filesep,obj.infilenames.(['matConv_Schneider_densityCorr_',obj.materialConverter.densityCorrection])); - densityFile = fopen(fname); - densityCorrection.density = fscanf(densityFile,'%f'); - fclose(densityFile); - densityCorrection.boundaries = [-1000 numel(densityCorrection.density)-1000]; - + matRad_cfg.dispInfo('Writing data to %s\n',outfile) + fID = fopen(outfile,'w+'); + switch obj.materialConverter.mode + case 'RSP' % Relative stopping power converter + rspHlut = obj.hlut; + min_HU = rspHlut(1,1); + max_HU = rspHlut(end,1); + + huCube = int32(permute(ct.cubeHU{ctScen},permutation)); % X,Y,Z ordering + huCube(huCube < min_HU) = min_HU; + huCube(huCube > max_HU) = max_HU; + + unique_hu = unique(huCube(:)); + unique_rsp = matRad_interp1(rspHlut(:,1),rspHlut(:,2),double(unique_hu)); + fbase = fopen(['materialConverter/definedMaterials/' medium '.txt'],'r'); + while ~feof(fbase) + strLine = fgets(fbase); %# read line by line + fprintf(fID,'%s',strLine); end + fclose(fbase); - % define additional density sections - switch obj.materialConverter.addSection - case 'lung' - addSection = [0.00012 1.05]; - otherwise - addSection = []; + unique_materials = cell(1,length(unique_hu)); + for ix=1:length(unique_hu) + unique_materials{ix} = strrep(['Material_HU_',num2str(unique_hu(ix))],'-','m'); + fprintf(fID,'s:Ma/%s/BaseMaterial = "%s"\n',unique_materials{ix},medium); + fprintf(fID,'d:Ma/%s/Density = %f g/cm3\n',unique_materials{ix},unique_rsp(ix)); end - if exist('addSection','var') && ~isempty(addSection) - densityCorrection.density(end+1:end+numel(addSection)) = addSection; - densityCorrection.boundaries(end+1) = densityCorrection.boundaries(end)+numel(addSection); + + fprintf(fID,'s:Ge/Patient/Parent="Isocenter"\n'); + fprintf(fID,'s:Ge/Patient/Type = "TsImageCube"\n'); + fprintf(fID,'s:Ge/Patient/InputDirectory = "./"\n'); + if obj.calc4DInterplay + fprintf(fID,'s:Ge/Patient/InputFile = Tf/InputFile/Value \n'); + else + fprintf(fID,'s:Ge/Patient/InputFile = "%s"\n',dataFile); end - % define Hounsfield Unit Sections - switch obj.materialConverter.HUSection - case 'default' - densityCorrection.unitSections = [densityCorrection.boundaries]; - densityCorrection.offset = 1; - densityCorrection.factor = 0; - densityCorrection.factorOffset = -rspHlut(1,1); - - case 'advanced' - densityCorrection.offset = [0.00121 1.018 1.03 1.003 1.017 2.201]; - densityCorrection.factor = [0.001029700665188 0.000893 0 0.001169 0.000592 0.0005]; - densityCorrection.factorOffset = [1000 0 1000 0 0 -2000]; - - if isfield(obj.materialConverter,'addTitanium') && obj.materialConverter.addTitanium %Titanium independent of set hounsfield unit! - densityCorrection.density(end+1) = 1.00275; - densityCorrection.boundaries(end+1) = densityCorrection.boundaries(end)+1; - densityCorrection.offset(end+1) = 4.54; - densityCorrection.factor(end+1) = 0; - densityCorrection.factorOffset(end+1) = 0; + fprintf(fID,'s:Ge/Patient/ImagingtoMaterialConverter = "MaterialTagNumber"\n'); + fprintf(fID,'i:Ge/Patient/NumberOfVoxelsX = %d\n',ct.cubeDim(2)); + fprintf(fID,'i:Ge/Patient/NumberOfVoxelsY = %d\n',ct.cubeDim(1)); + fprintf(fID,'iv:Ge/Patient/NumberOfVoxelsZ = 1 %d\n',ct.cubeDim(3)); + fprintf(fID,'d:Ge/Patient/VoxelSizeX = %.3f mm\n',ct.resolution.x); + fprintf(fID,'d:Ge/Patient/VoxelSizeY = %.3f mm\n',ct.resolution.y); + fprintf(fID,'dv:Ge/Patient/VoxelSizeZ = 1 %.3f mm\n',ct.resolution.z); + fprintf(fID,'s:Ge/Patient/DataType = "SHORT"\n'); + fprintf(fID,'iv:Ge/Patient/MaterialTagNumbers = %d ',length(unique_hu)); + fprintf(fID,num2str(unique_hu','%d ')); + fprintf(fID,'\n'); + fprintf(fID,'sv:Ge/Patient/MaterialNames = %d ',length(unique_hu)); + fprintf(fID,'"%s"',strjoin(unique_materials,'" "')); + fprintf(fID,'\n'); + fclose(fID); + + % write data + fID = fopen(fullfile(obj.workingDir, dataFile),'w'); + fwrite(fID,huCube,'short'); + fclose(fID); + cube = huCube; + + + case 'HUToWaterSchneider' % Schneider converter + rspHlut = obj.hlut; + + try + % Write Schneider Converter + if ~obj.materialConverter.loadConverterFromFile + % define density correction + matRad_cfg.dispInfo('TOPAS: Writing density correction\n'); + switch obj.materialConverter.densityCorrection + case 'rspHLUT' + densityCorrection.density = []; + for i = 1:size(rspHlut,1)-1 + startVal = rspHlut(i,1); + endVal = rspHlut(i+1,1); + range = startVal:1:endVal-1; + densityCorrection.density(end+1:end+numel(range)) = matRad_interp1(rspHlut(:,1),rspHlut(:,2),range); + end + densityCorrection.density(end+1) = rspHlut(end,2); %add last missing value + densityCorrection.boundaries = [rspHlut(1,1) numel(densityCorrection.density)-abs(rspHlut(1,1))]; + + case {'Schneider_TOPAS','Schneider_matRad'} + fname = fullfile(obj.topasFolder,filesep,obj.converterFolder,filesep,obj.infilenames.(['matConv_Schneider_densityCorr_',obj.materialConverter.densityCorrection])); + densityFile = fopen(fname); + densityCorrection.density = fscanf(densityFile,'%f'); + fclose(densityFile); + densityCorrection.boundaries = [-1000 numel(densityCorrection.density)-1000]; + end - densityCorrection.unitSections = [densityCorrection.boundaries(1) -98 15 23 101 2001 densityCorrection.boundaries(2:end)]; - end - for i = numel(densityCorrection.offset)+1:numel(densityCorrection.unitSections)-1 - densityCorrection.offset(i) = 1; - densityCorrection.factor(i) = 0; - densityCorrection.factorOffset(i) = 0; - end + % define additional density sections + switch obj.materialConverter.addSection + case 'lung' + addSection = [0.00012 1.05]; + otherwise + addSection = []; + end + if exist('addSection','var') && ~isempty(addSection) + densityCorrection.density(end+1:end+numel(addSection)) = addSection; + densityCorrection.boundaries(end+1) = densityCorrection.boundaries(end)+numel(addSection); + end + % define Hounsfield Unit Sections + switch obj.materialConverter.HUSection + case 'default' + densityCorrection.unitSections = [densityCorrection.boundaries]; + densityCorrection.offset = 1; + densityCorrection.factor = 0; + densityCorrection.factorOffset = -rspHlut(1,1); + + case 'advanced' + densityCorrection.offset = [0.00121 1.018 1.03 1.003 1.017 2.201]; + densityCorrection.factor = [0.001029700665188 0.000893 0 0.001169 0.000592 0.0005]; + densityCorrection.factorOffset = [1000 0 1000 0 0 -2000]; + + if isfield(obj.materialConverter,'addTitanium') && obj.materialConverter.addTitanium %Titanium independent of set hounsfield unit! + densityCorrection.density(end+1) = 1.00275; + densityCorrection.boundaries(end+1) = densityCorrection.boundaries(end)+1; + densityCorrection.offset(end+1) = 4.54; + densityCorrection.factor(end+1) = 0; + densityCorrection.factorOffset(end+1) = 0; + end + + densityCorrection.unitSections = [densityCorrection.boundaries(1) -98 15 23 101 2001 densityCorrection.boundaries(2:end)]; + end + for i = numel(densityCorrection.offset)+1:numel(densityCorrection.unitSections)-1 + densityCorrection.offset(i) = 1; + densityCorrection.factor(i) = 0; + densityCorrection.factorOffset(i) = 0; + end - % write density correction - fprintf(fID,'# -- Density correction\n'); - fprintf(fID,['dv:Ge/Patient/DensityCorrection = %i',repmat(' %.6g',1,numel(densityCorrection.density)),' g/cm3\n'],numel(densityCorrection.density),densityCorrection.density); - fprintf(fID,['iv:Ge/Patient/SchneiderHounsfieldUnitSections = %i',repmat(' %g',1,numel(densityCorrection.unitSections)),'\n'],numel(densityCorrection.unitSections),densityCorrection.unitSections); - fprintf(fID,['uv:Ge/Patient/SchneiderDensityOffset = %i',repmat(' %g',1,numel(densityCorrection.offset)),'\n'],numel(densityCorrection.offset),densityCorrection.offset); - % this is needed for a custom fprintf format which formats integers i to 'i.' and floats without trailing zeros - % TODO: check whether this can be removed -> this is potentially not necessary but was done to mimick the original TOPAS Schneider converter file - TOPASisFloat = mod(densityCorrection.factor,1)==0; - fprintf(fID,['uv:Ge/Patient/SchneiderDensityFactor = %i ',strjoin(cellstr(char('%1.01f '.*TOPASisFloat' + '%1.15g '.*~TOPASisFloat'))),'\n'],numel(densityCorrection.factor),densityCorrection.factor); - TOPASisFloat = mod(densityCorrection.factorOffset,1)==0; - fprintf(fID,['uv:Ge/Patient/SchneiderDensityFactorOffset = %i ',strjoin(cellstr(char('%1.01f '.*TOPASisFloat' + '%1.15g '.*~TOPASisFloat'))),'\n'],numel(densityCorrection.factorOffset),densityCorrection.factorOffset); - % fprintf(fID,'uv:Ge/Patient/SchneiderDensityFactor = 8 0.001029700665188 0.000893 0.0 0.001169 0.000592 0.0005 0.0 0.0\n'); - % fprintf(fID,'uv:Ge/Patient/SchneiderDensityFactorOffset = 8 1000. 0. 1000. 0. 0. -2000. 0. 0.0\n\n'); - - % define HU to material sections - matRad_cfg.dispInfo('TOPAS: Writing HU to material sections\n'); - switch obj.materialConverter.HUToMaterial - case 'default' - HUToMaterial.sections = rspHlut(2,1); - case 'MCsquare' - HUToMaterial.sections = [-1000 -950 -120 -82 -52 -22 8 19 80 120 200 300 400 500 600 700 800 900 1000 1100 1200 1300 1400 1500]; - case 'advanced' - HUToMaterial.sections = [-950 -120 -83 -53 -23 7 18 80 120 200 300 400 500 600 700 800 900 1000 1100 1200 1300 1400 1500]; - end - HUToMaterial.sections = [densityCorrection.boundaries(1) HUToMaterial.sections densityCorrection.boundaries(2:end)]; - % write HU to material sections - % fprintf(fID,'i:Ge/Patient/MinImagingValue = %d\n',densityCorrection.boundaries(1)); - fprintf(fID,['iv:Ge/Patient/SchneiderHUToMaterialSections = %i ',repmat('%d ',1,numel(HUToMaterial.sections)),'\n\n'],numel(HUToMaterial.sections),HUToMaterial.sections); - % load defined material based on materialConverter.HUToMaterial - - fname = fullfile(obj.topasFolder,filesep,obj.converterFolder,filesep,obj.infilenames.matConv_Schneider_definedMaterials.(obj.materialConverter.HUToMaterial)); - materials = strsplit(fileread(fname),'\n')'; - switch obj.materialConverter.HUToMaterial - case 'default' - fprintf(fID,'%s \n',materials{1:end-1}); - ExcitationEnergies = str2double(strsplit(materials{end}(strfind(materials{end},'=')+4:end-3))); - if ~isempty(strfind(lower(obj.materialConverter.addSection),lower('lung'))) - fprintf(fID,'uv:Ge/Patient/SchneiderMaterialsWeight%i = 5 0.10404040 0.75656566 0.03131313 0.10606061 0.00202020\n',length(materials)-2); - ExcitationEnergies = [ExcitationEnergies' 75.3]; + % write density correction + fprintf(fID,'# -- Density correction\n'); + fprintf(fID,['dv:Ge/Patient/DensityCorrection = %i',repmat(' %.6g',1,numel(densityCorrection.density)),' g/cm3\n'],numel(densityCorrection.density),densityCorrection.density); + fprintf(fID,['iv:Ge/Patient/SchneiderHounsfieldUnitSections = %i',repmat(' %g',1,numel(densityCorrection.unitSections)),'\n'],numel(densityCorrection.unitSections),densityCorrection.unitSections); + fprintf(fID,['uv:Ge/Patient/SchneiderDensityOffset = %i',repmat(' %g',1,numel(densityCorrection.offset)),'\n'],numel(densityCorrection.offset),densityCorrection.offset); + % this is needed for a custom fprintf format which formats integers i to 'i.' and floats without trailing zeros + % TODO: check whether this can be removed -> this is potentially not necessary but was done to mimick the original TOPAS Schneider converter file + TOPASisFloat = mod(densityCorrection.factor,1)==0; + fprintf(fID,['uv:Ge/Patient/SchneiderDensityFactor = %i ',strjoin(cellstr(char('%1.01f '.*TOPASisFloat' + '%1.15g '.*~TOPASisFloat'))),'\n'],numel(densityCorrection.factor),densityCorrection.factor); + TOPASisFloat = mod(densityCorrection.factorOffset,1)==0; + fprintf(fID,['uv:Ge/Patient/SchneiderDensityFactorOffset = %i ',strjoin(cellstr(char('%1.01f '.*TOPASisFloat' + '%1.15g '.*~TOPASisFloat'))),'\n'],numel(densityCorrection.factorOffset),densityCorrection.factorOffset); + % fprintf(fID,'uv:Ge/Patient/SchneiderDensityFactor = 8 0.001029700665188 0.000893 0.0 0.001169 0.000592 0.0005 0.0 0.0\n'); + % fprintf(fID,'uv:Ge/Patient/SchneiderDensityFactorOffset = 8 1000. 0. 1000. 0. 0. -2000. 0. 0.0\n\n'); + + % define HU to material sections + matRad_cfg.dispInfo('TOPAS: Writing HU to material sections\n'); + switch obj.materialConverter.HUToMaterial + case 'default' + HUToMaterial.sections = rspHlut(2,1); + case 'MCsquare' + HUToMaterial.sections = [-1000 -950 -120 -82 -52 -22 8 19 80 120 200 300 400 500 600 700 800 900 1000 1100 1200 1300 1400 1500]; + case 'advanced' + HUToMaterial.sections = [-950 -120 -83 -53 -23 7 18 80 120 200 300 400 500 600 700 800 900 1000 1100 1200 1300 1400 1500]; + end + HUToMaterial.sections = [densityCorrection.boundaries(1) HUToMaterial.sections densityCorrection.boundaries(2:end)]; + % write HU to material sections + % fprintf(fID,'i:Ge/Patient/MinImagingValue = %d\n',densityCorrection.boundaries(1)); + fprintf(fID,['iv:Ge/Patient/SchneiderHUToMaterialSections = %i ',repmat('%d ',1,numel(HUToMaterial.sections)),'\n\n'],numel(HUToMaterial.sections),HUToMaterial.sections); + % load defined material based on materialConverter.HUToMaterial + + fname = fullfile(obj.topasFolder,filesep,obj.converterFolder,filesep,obj.infilenames.matConv_Schneider_definedMaterials.(obj.materialConverter.HUToMaterial)); + materials = strsplit(fileread(fname),'\n')'; + switch obj.materialConverter.HUToMaterial + case 'default' + fprintf(fID,'%s \n',materials{1:end-1}); + ExcitationEnergies = str2double(strsplit(materials{end}(strfind(materials{end},'=')+4:end-3))); + if ~isempty(strfind(lower(obj.materialConverter.addSection),lower('lung'))) + fprintf(fID,'uv:Ge/Patient/SchneiderMaterialsWeight%i = 5 0.10404040 0.75656566 0.03131313 0.10606061 0.00202020\n',length(materials)-2); + ExcitationEnergies = [ExcitationEnergies' 75.3]; + end + fprintf(fID,['dv:Ge/Patient/SchneiderMaterialMeanExcitationEnergy = %i',repmat(' %.6g',1,numel(ExcitationEnergies)),' eV\n'],numel(ExcitationEnergies),ExcitationEnergies); + case 'advanced' + fprintf(fID,'\n%s\n',materials{:}); + case 'MCsquare' + fprintf(fID,'\n%s\n',materials{:}); end - fprintf(fID,['dv:Ge/Patient/SchneiderMaterialMeanExcitationEnergy = %i',repmat(' %.6g',1,numel(ExcitationEnergies)),' eV\n'],numel(ExcitationEnergies),ExcitationEnergies); - case 'advanced' - fprintf(fID,'\n%s\n',materials{:}); - case 'MCsquare' - fprintf(fID,'\n%s\n',materials{:}); - end - switch obj.materialConverter.HUToMaterial - case 'advanced' - counter = 25; - if isfield(obj.materialConverter,'addTitanium') && obj.materialConverter.addTitanium - fprintf(fID,'uv:Ge/Patient/SchneiderMaterialsWeight%i = 15 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0',counter); - counter = counter + 1; + switch obj.materialConverter.HUToMaterial + case 'advanced' + counter = 25; + if isfield(obj.materialConverter,'addTitanium') && obj.materialConverter.addTitanium + fprintf(fID,'uv:Ge/Patient/SchneiderMaterialsWeight%i = 15 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0',counter); + counter = counter + 1; + end + % fprintf(fID,'uv:Ge/Patient/SchneiderMaterialsWeight%i = 15 0.10404040 0.10606061 0.75656566 0.03131313 0.0 0.00202020 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0',counter); + fprintf(fID,'uv:Ge/Patient/SchneiderMaterialsWeight%i = 15 0.101278 0.102310 0.028650 0.757072 0.000730 0.000800 0.002250 0.002660 0.0 0.000090 0.001840 0.001940 0.0 0.000370 0.000010',counter); end - % fprintf(fID,'uv:Ge/Patient/SchneiderMaterialsWeight%i = 15 0.10404040 0.10606061 0.75656566 0.03131313 0.0 0.00202020 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0',counter); - fprintf(fID,'uv:Ge/Patient/SchneiderMaterialsWeight%i = 15 0.101278 0.102310 0.028650 0.757072 0.000730 0.000800 0.002250 0.002660 0.0 0.000090 0.001840 0.001940 0.0 0.000370 0.000010',counter); + else + fname = fullfile(obj.topasFolder,filesep,obj.converterFolder,filesep,obj.infilenames.matConv_Schneider_loadFromFile); + converter = fileread(fname); + fprintf(fID,'\n%s\n',converter); + end + + % write patient environment + matRad_cfg.dispInfo('TOPAS: Writing patient environment\n'); + fprintf(fID,'\n# -- Patient parameters\n'); + fprintf(fID,'s:Ge/Patient/Parent="Isocenter"\n'); + fprintf(fID,'s:Ge/Patient/Type = "TsImageCube"\n'); + fprintf(fID,'b:Ge/Patient/DumpImagingValues = "True"\n'); + fprintf(fID,'s:Ge/Patient/InputDirectory = "./"\n'); + if obj.calc4DInterplay + fprintf(fID,'s:Ge/Patient/InputFile = Tf/InputFile/Value \n'); + else + fprintf(fID,'s:Ge/Patient/InputFile = "%s"\n',dataFile); + end + fprintf(fID,'s:Ge/Patient/ImagingtoMaterialConverter = "Schneider"\n'); + fprintf(fID,'i:Ge/Patient/NumberOfVoxelsX = %d\n',ct.cubeDim(2)); + fprintf(fID,'i:Ge/Patient/NumberOfVoxelsY = %d\n',ct.cubeDim(1)); + fprintf(fID,'iv:Ge/Patient/NumberOfVoxelsZ = 1 %d\n',ct.cubeDim(3)); + fprintf(fID,'d:Ge/Patient/VoxelSizeX = %.3f mm\n',ct.resolution.x); + fprintf(fID,'d:Ge/Patient/VoxelSizeY = %.3f mm\n',ct.resolution.y); + fprintf(fID,'dv:Ge/Patient/VoxelSizeZ = 1 %.3f mm\n',ct.resolution.z); + fprintf(fID,'s:Ge/Patient/DataType = "SHORT"\n'); + + fclose(fID); + + % write HU data + matRad_cfg.dispInfo('TOPAS: Export patient cube\n'); + huCube = int32(permute(ct.cubeHU{ctScen},permutation)); + fID = fopen(fullfile(obj.workingDir, dataFile),'w'); + fwrite(fID,huCube,'short'); + fclose(fID); + cube = huCube; + catch ME + matRad_cfg.dispWarning(ME.message); + matRad_cfg.dispError(['TOPAS: Error in Schneider Converter! (line ',num2str(ME.stack(1).line),')']); end - else - fname = fullfile(obj.topasFolder,filesep,obj.converterFolder,filesep,obj.infilenames.matConv_Schneider_loadFromFile); - converter = fileread(fname); - fprintf(fID,'\n%s\n',converter); - end - % write patient environment - matRad_cfg.dispInfo('TOPAS: Writing patient environment\n'); - fprintf(fID,'\n# -- Patient parameters\n'); - fprintf(fID,'s:Ge/Patient/Parent="Isocenter"\n'); - fprintf(fID,'s:Ge/Patient/Type = "TsImageCube"\n'); - fprintf(fID,'b:Ge/Patient/DumpImagingValues = "True"\n'); - fprintf(fID,'s:Ge/Patient/InputDirectory = "./"\n'); - fprintf(fID,'s:Ge/Patient/InputFile = "%s"\n',dataFile); - fprintf(fID,'s:Ge/Patient/ImagingtoMaterialConverter = "Schneider"\n'); - fprintf(fID,'i:Ge/Patient/NumberOfVoxelsX = %d\n',ct.cubeDim(2)); - fprintf(fID,'i:Ge/Patient/NumberOfVoxelsY = %d\n',ct.cubeDim(1)); - fprintf(fID,'iv:Ge/Patient/NumberOfVoxelsZ = 1 %d\n',ct.cubeDim(3)); - fprintf(fID,'d:Ge/Patient/VoxelSizeX = %.3f mm\n',ct.resolution.x); - fprintf(fID,'d:Ge/Patient/VoxelSizeY = %.3f mm\n',ct.resolution.y); - fprintf(fID,'dv:Ge/Patient/VoxelSizeZ = 1 %.3f mm\n',ct.resolution.z); - fprintf(fID,'s:Ge/Patient/DataType = "SHORT"\n'); - - fclose(fID); - - % write HU data - matRad_cfg.dispInfo('TOPAS: Export patient cube\n'); - huCube = int32(permute(ct.cubeHU{ctScen},permutation)); - fID = fopen(fullfile(obj.workingDir, dataFile),'w'); - fwrite(fID,huCube,'short'); - fclose(fID); - cube = huCube; - catch ME - matRad_cfg.dispWarning(ME.message); - matRad_cfg.dispError(['TOPAS: Error in Schneider Converter! (line ',num2str(ME.stack(1).line),')']); + otherwise + matRad_cfg.dispError('Material Conversion rule "%s" not implemented (yet)!\n',obj.materialConverter.mode); end - - otherwise - matRad_cfg.dispError('Material Conversion rule "%s" not implemented (yet)!\n',obj.materialConverter.mode); + obj.MCparam.imageCube{ctScen} = cube; + end end - obj.MCparam.imageCube{ctScen} = cube; end @@ -2443,7 +2729,7 @@ function writeMCparam(obj) %Used to check against a machine file if a specific quantity can be %computed. function q = providedQuantities(machine) - q = {'physicalDose','LET','alpha','beta'}; + q = {'physicalDose','LET','alpha','beta'}; end end end diff --git a/matRad/doseCalc/FRED/hluts/matRad_default_FredMaterialConverter.txt b/matRad/doseCalc/FRED/hluts/matRad_default_FredMaterialConverter.txt new file mode 100644 index 000000000..597e7f654 --- /dev/null +++ b/matRad/doseCalc/FRED/hluts/matRad_default_FredMaterialConverter.txt @@ -0,0 +1,9 @@ +matColumns: HU rho RSP Ipot Lrad C Ca H N O P Ti S +mat: -1024 0.001 0.001 78.0 36.1 0 0 11.189400 0 88.810600 0 0 0 +mat: -999 0.001 0.0011 78.0 36.1 0 0 11.189400 0 88.810600 0 0 0 +mat: -90 0.95 0.95 78.0 36.1 0 0 11.189400 0 88.810600 0 0 0 +mat: -45 0.99 0.99 78.0 36.1 0 0 11.189400 0 88.810600 0 0 0 +mat: 0 1 1 78.0 36.1 0 0 11.189400 0 88.810600 0 0 0 +mat: 100 1.095 1.095 78.0 36.1 0 0 11.189400 0 88.810600 0 0 0 +mat: 350 1.199 1.199 78.0 36.1 0 0 11.189400 0 88.810600 0 0 0 +mat: 3000 2.505 2.505 78.0 36.1 0 0 11.189400 0 88.810600 0 0 0 \ No newline at end of file diff --git a/matRad/gui/matRad_MainGUI.m b/matRad/gui/matRad_MainGUI.m index 2e6a4c2cf..9111e67f8 100644 --- a/matRad/gui/matRad_MainGUI.m +++ b/matRad/gui/matRad_MainGUI.m @@ -210,6 +210,9 @@ if matRad_ispropCompat(obj.guiHandle,'WindowState') set(obj.guiHandle,'WindowState','maximized'); end + if matRad_cfg.isMatlab && isunix + movegui(obj.guiHandle,'onscreen'); + end if matRad_cfg.isOctave commonPanelProperties = {'Parent',obj.guiHandle,... diff --git a/matRad/gui/widgets/matRad_DVHWidget.m b/matRad/gui/widgets/matRad_DVHWidget.m index 61bb83f81..dc8fe26e5 100644 --- a/matRad/gui/widgets/matRad_DVHWidget.m +++ b/matRad/gui/widgets/matRad_DVHWidget.m @@ -109,15 +109,24 @@ function showDVH(this) resultGUI = evalin('base','resultGUI'); pln = evalin('base','pln'); cst = evalin('base','cst'); + ct = evalin('base','ct'); + % Calculate and show DVH doseCube = resultGUI.(this.selectedCube); dvh = matRad_calcDVH(cst,doseCube,'cum'); matRad_showDVH(dvh,cst,pln,'axesHandle',this.dvhAx); + + if ~isfield(pln,'multScen') + multScen = matRad_NominalScenario(ct); + else + multScen = pln.multScen; + end + multScen = matRad_ScenarioModel.create(multScen,ct); %check scenarios - if pln.multScen.totNumScen > 1 - for i = 1:pln.multScen.totNumScen + if multScen.totNumScen > 1 + for i = 1:multScen.totNumScen scenFieldName = sprintf('%s_scen%d',this.selectedCube,i); if isfield(resultGUI,scenFieldName) tmpDvh = matRad_calcDVH(cst,resultGUI.(scenFieldName),'cum'); % Calculate cumulative scenario DVH diff --git a/matRad/gui/widgets/matRad_PlanWidget.m b/matRad/gui/widgets/matRad_PlanWidget.m index 493415abc..e0b3a2a20 100644 --- a/matRad/gui/widgets/matRad_PlanWidget.m +++ b/matRad/gui/widgets/matRad_PlanWidget.m @@ -28,11 +28,12 @@ hTissueWindow; currentMachine; + plotPlan = false; end properties (Constant) - modalities = {'photons','protons','carbon', 'helium','brachy'}; + modalities = {'photons','protons','carbon', 'helium','brachy', 'VHEE'}; availableProjections = { 'physicalDose'; 'RBExDose'; 'effect'; 'BED'; } end @@ -803,7 +804,7 @@ matRad_cfg = MatRad_Config.instance(); - stfGen = matRad_StfGeneratorBase.getGeneratorFromPln(pln); + stfGen = matRad_StfGeneratorBase.getGeneratorFromPln(pln, false); set(handles.editBixelWidth,'String',num2str(stfGen.bixelWidth)); set(handles.editGantryAngle,'String',num2str(stfGen.gantryAngles)); @@ -946,33 +947,49 @@ function updatePlnInWorkspace(this,hObject,evtData) this.getMachines(); handles = this.handles; + oldGantryAngles = []; + oldCouchAngles = []; + % evalin pln (if existant) in order to decide whether isoCenter should be calculated % automatically if evalin('base','exist(''pln'',''var'')') pln = evalin('base','pln'); + if isfield(pln.propStf,'gantryAngles') && isfield(pln.propStf,'couchAngles') + oldGantryAngles = pln.propStf.gantryAngles; + oldCouchAngles = pln.propStf.couchAngles ; + end end pln.propStf.bixelWidth = this.parseStringAsNum(get(handles.editBixelWidth,'String'),false); % [mm] / also corresponds to lateral spot spacing for particles - + pln.propStf.gantryAngles = this.parseStringAsNum(get(handles.editGantryAngle,'String'),true); % [°] pln.propStf.couchAngles = this.parseStringAsNum(get(handles.editCouchAngle,'String'),true); % [°] - if ~isempty(hObject) && strcmp(hObject.Tag,'editGantryAngle') - if numel(this.parseStringAsNum(get(handles.editCouchAngle,'String'),true))==1 % Feature: autofill couch angles to single plane by entering a single value - pln.propStf.couchAngles = this.parseStringAsNum(get(handles.editCouchAngle,'String'),true) * ones(1,numel(pln.propStf.gantryAngles)); - else - pln.propStf.couchAngles = this.parseStringAsNum(get(handles.editCouchAngle,'String'),true); % [°] - end - elseif ~isempty(hObject) && strcmp(hObject.Tag,'editCouchAngle') - if numel(this.parseStringAsNum(get(handles.editGantryAngle,'String'),true))==1 % Feature: autofill gantry angles to single plane by entering a single value - pln.propStf.gantryAngles = this.parseStringAsNum(get(handles.editGantryAngle,'String'),true) * ones(1,numel(pln.propStf.couchAngles)); - else - pln.propStf.gantryAngles = this.parseStringAsNum(get(handles.editGantryAngle,'String'),true); % [°] + if ~isequal(pln.propStf.gantryAngles,oldGantryAngles) || ~isequal(pln.propStf.couchAngles,oldCouchAngles) + + pln.propStf.gantryAngles = this.parseStringAsNum(get(handles.editGantryAngle,'String'),true); % [°] + pln.propStf.couchAngles = this.parseStringAsNum(get(handles.editCouchAngle,'String'),true); % [°] + + objectTag = get(hObject,'Tag'); % Returns empty if hObject is empty, no check required + + if ~isempty(objectTag) && (strcmp(objectTag,'editGantryAngle')||strcmp(objectTag,'editCouchAngle')) + if numel(this.parseStringAsNum(get(handles.editCouchAngle,'String'),true))numel(this.parseStringAsNum(get(handles.editGantryAngle,'String'),true)) % Feature: autofill couch angles to single plane by entering a single value + couchGantryDifference = numel(this.parseStringAsNum(get(handles.editCouchAngle,'String'),true))-numel(this.parseStringAsNum(get(handles.editGantryAngle,'String'),true)); + pln.propStf.couchAngles = pln.propStf.couchAngles(1:end-couchGantryDifference); + end end + this.plotPlan = true; end - pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); - pln.propStf.isoCenter = this.parseStringAsNum(get(handles.editIsoCenter,'String'),true); + pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); + + isoStr = get(handles.editIsoCenter,'String'); + if ~isequal(isoStr,'multiple isoCenter') + pln.propStf.isoCenter = this.parseStringAsNum(isoStr,true); + end % switch machines depending on radmode selection selectedMachine = get(handles.popUpMachine,'Value'); @@ -1078,6 +1095,11 @@ function updatePlnInWorkspace(this,hObject,evtData) assignin('base','pln',pln); this.handles = handles; this.changedWorkspace('pln'); + if this.plotPlan + evt = matRad_WorkspaceChangedEvent('pln_angles'); + this.changedWorkspace('pln_angles'); + this.plotPlan = false; + end end end @@ -1234,6 +1256,11 @@ function popupRadMode_Callback(this, hObject, eventdata) set(handles.popMenuQuantityOpt,'Value',ix); end + if any(strcmp(newRadiationMode,{'photons','VHEE','brachy'})) + ix = find(strcmp(optimizationQuantityPopUpContents,'physicalDose')); + set(handles.popMenuQuantityOpt,'Value',ix); + end + pln.radiationMode = newRadiationMode; if isfield(defaultMachines,newRadiationMode) pln.machine = defaultMachines.(newRadiationMode); @@ -1278,7 +1305,7 @@ function popupRadMode_Callback(this, hObject, eventdata) if ismember('resultGUI',AllVarNames) resultGUI = evalin('base','resultGUI'); radMode = allRadiationModes(get(hObject,'Value')); - if any(strcmp(radMode,{'photons','brachy'})) + if any(strcmp(radMode,{'photons','brachy','VHEE'})) if isfield(resultGUI,'alpha'); resultGUI = rmfield(resultGUI,'alpha'); end if isfield(resultGUI,'beta'); resultGUI = rmfield(resultGUI,'beta'); end if isfield(resultGUI,'RBExDose'); resultGUI = rmfield(resultGUI,'RBExDose');end @@ -1446,6 +1473,7 @@ function btnSetTissue_Callback(this, hObject, eventdata) if evalin('base','exist(''cst'')') && evalin('base','exist(''pln'')') try + matRad_cfg = MatRad_Config.instance(); %parse variables from base-workspace cst = evalin('base','cst'); pln = evalin('base','pln'); @@ -1454,9 +1482,27 @@ function btnSetTissue_Callback(this, hObject, eventdata) fileName = [pln.radiationMode '_' pln.machine]; load(fileName); - % check for available cell types characterized by alphaX and betaX - for i = 1:size(machine.data(1).alphaX,2) - CellType{i} = [num2str(machine.data(1).alphaX(i)) ' ' num2str(machine.data(1).betaX(i))]; + %biological model + if isfield(matRad_cfg.defaults.bioModel,pln.radiationMode) + defaultModel = matRad_cfg.defaults.bioModel.(pln.radiationMode); + else + defaultModel = matRad_cfg.defaults.bioModel.fallback; + end + if ~isfield(pln,'bioModel') + pln.bioModel = defaultModel; + end + + bioModel = matRad_BiologicalModel.validate(pln.bioModel,pln.radiationMode); + + [availableAlphaX, availableBetaX] = bioModel.getAvailableTissueParameters(pln); + + if ~isempty(availableAlphaX) && ~isempty(availableBetaX) + for i = 1:size(availableAlphaX,2) + CellType{i} = [num2str(availableAlphaX(i)) ' ' num2str(availableBetaX(i))]; + columnformat = {'char',CellType,'numeric'}; + end + else + columnformat = {'char','numeric','numeric'}; end %fill table data array @@ -1485,16 +1531,42 @@ function btnSetTissue_Callback(this, hObject, eventdata) %set focus figure(figTissue); else - figTissue = figure('Name','Set Tissue Parameters','Color',[.5 .5 .5],'NumberTitle','off','OuterPosition',... - [ceil(ScreenSize(3)/2) 100 Width Height]); + figTissue = figure('Name','Set Tissue Parameters', ... + 'NumberTitle','off', ... + 'OuterPosition',[ceil(ScreenSize(3)/2) 100 Width Height],... + 'Color',matRad_cfg.gui.backgroundColor); end % define the tissue parameter table cNames = {'VOI','alphaX betaX','alpha beta ratio'}; - columnformat = {'char',CellType,'numeric'}; + % columnformat = {'char',CellType,'numeric'}; + + %design table colors + colorMatrix = repmat(matRad_cfg.gui.elementColor,size(data,1),1); + ix2 = 2:2:size(data,1); + if ~isempty(ix2) + shadeColor = rgb2hsv(matRad_cfg.gui.elementColor); + if shadeColor(3) < 0.5 + shadeColor(3) = shadeColor(3)*1.5+0.1; + else + shadeColor(3) = shadeColor(3)*0.5-0.1; + end + + colorMatrix(ix2,:) = repmat(hsv2rgb(shadeColor),numel(ix2),1); + end + + + % Create the uitable + tissueTable = uitable('Parent', figTissue, ... + 'Data', data, ... + 'ColumnEditable',[false true false],... + 'ColumnName',cNames, ... + 'ColumnFormat',columnformat, ... + 'Position',[50 150 10 10], ... + 'ForegroundColor',matRad_cfg.gui.textColor,... + 'BackgroundColor',colorMatrix,... + 'RowStriping','on'); - tissueTable = uitable('Parent', figTissue,'Data', data,'ColumnEditable',[false true false],... - 'ColumnName',cNames, 'ColumnFormat',columnformat,'Position',[50 150 10 10]); set(tissueTable,'CellEditCallback',@(hObject,eventdata) tissueTable_CellEditCallback(this,hObject,eventdata)); % set width and height currTablePos = get(tissueTable,'Position'); @@ -1503,14 +1575,27 @@ function btnSetTissue_Callback(this, hObject, eventdata) currTablePos(4) = currTableExt(4); set(tissueTable,'Position',currTablePos); + themeParams = {'BackgroundColor', matRad_cfg.gui.backgroundColor,... + 'ForegroundColor',matRad_cfg.gui.textColor,... + 'FontSize',matRad_cfg.gui.fontSize,... + 'FontName',matRad_cfg.gui.fontName,... + 'FontWeight',matRad_cfg.gui.fontWeight}; + % define two buttons with callbacks - uicontrol('Parent', figTissue,'Style', 'pushbutton', 'String', 'Save&Close',... + uicontrol('Parent', figTissue, ... + 'Style', 'pushbutton', ... + 'String', 'Save&Close',... 'Position', [Width-(0.25*Width) 0.1 * Height 70 30],... - 'Callback', @(hpb,eventdata)SaveTissueParameters(this,hpb,eventdata)); + 'Callback', @(hpb,eventdata)SaveTissueParameters(this,hpb,eventdata),... + themeParams{:}); - uicontrol('Parent', figTissue,'Style', 'pushbutton', 'String', 'Cancel&Close',... + uicontrol('Parent', ... + figTissue,'Style', ... + 'pushbutton', ... + 'String', 'Cancel&Close',... 'Position', [Width-(0.5*Width) 0.1 * Height 80 30],... - 'Callback', 'close'); + 'Callback', 'close', ... + themeParams{:}); catch ME this.showWarning('Could not set Tissue parameter update! Reason: %s\n',ME.message) end diff --git a/matRad/gui/widgets/matRad_ViewerOptionsWidget.m b/matRad/gui/widgets/matRad_ViewerOptionsWidget.m index 3f3ddadb3..5a2523dec 100644 --- a/matRad/gui/widgets/matRad_ViewerOptionsWidget.m +++ b/matRad/gui/widgets/matRad_ViewerOptionsWidget.m @@ -722,6 +722,7 @@ function checkbox_lockColormap_Callback(this, hObject, ~) set(handles.edit_windowCenter,'Enable',state); set(handles.edit_windowRange,'Enable',state); set(handles.popupmenu_chooseColormap,'Enable',state); + set(handles.btnSetIsoDoseLevels,'Enable',state); this.handles = handles; end diff --git a/matRad/gui/widgets/matRad_ViewingWidget.m b/matRad/gui/widgets/matRad_ViewingWidget.m index 0a6a075ad..2f8920e94 100644 --- a/matRad/gui/widgets/matRad_ViewingWidget.m +++ b/matRad/gui/widgets/matRad_ViewingWidget.m @@ -58,6 +58,7 @@ scrollHandle; lockColorSettings = false; %plotlegend=false; + evt; end properties (SetAccess=private) @@ -297,7 +298,7 @@ function notifyPlotUpdated(obj) function set.dispWindow(this,value) this.dispWindow=value; - evt = matRad_WorkspaceChangedEvent('image_display'); + evt = matRad_WorkspaceChangedEvent('viewer_options'); this.update(evt); end @@ -309,7 +310,7 @@ function notifyPlotUpdated(obj) function set.IsoDose_Levels(this,value) this.IsoDose_Levels=value; - evt = matRad_WorkspaceChangedEvent('image_display'); + evt = matRad_WorkspaceChangedEvent('viewer_options'); this.update(evt); end @@ -508,6 +509,7 @@ function notifyPlotUpdated(obj) %doUpdate = false; if nargin == 2 + this.evt = evt; %At pln changes and at cst/cst (for Isocenter and new settings) %we need to update if this.checkUpdateNecessary({'ct','cst'},evt) @@ -515,14 +517,16 @@ function notifyPlotUpdated(obj) end this.updateValues(); %doUpdate = this.checkUpdateNecessary({'pln_display','ct','cst','resultGUI','image_display'},evt); - if this.checkUpdateNecessary({'resultGUI','image_display'},evt) + if this.checkUpdateNecessary({'resultGUI','image_display','viewer_options','pln_angles'},evt) this.updateIsoDoseLineCache(); - end + this.UpdatePlot(); + end else this.updateValues(); + this.UpdatePlot(); end - this.UpdatePlot(); + this.evt =[]; end end @@ -933,10 +937,10 @@ function UpdatePlot(this) end dose = resultGUI.(this.SelectedDisplayOption); - %if function is called for the first time then set display parameters - if isempty(this.dispWindow{2,2}) || ~this.lockColorSettings - this.dispWindow{2,1} = [min(dose(:)) max(dose(:))*1.001]; % set default dose range - this.dispWindow{2,2} = [min(dose(:)) max(dose(:))*1.001]; % set min max values + %if function is called for the first time then set display parameters + if (isempty(this.dispWindow{2,2}) || ~this.checkUpdateNecessary({'viewer_options'},this.evt) ) && ~this.lockColorSettings + this.dispWindow{2,1} = [min(dose(:)) max(dose(:))*1.001]; % set default dose range + this.dispWindow{2,2} = [min(dose(:)) max(dose(:))*1.001]; % set min max values end minMaxRange = this.dispWindow{2,1}; @@ -947,7 +951,7 @@ function UpdatePlot(this) end %this creates a loop(needed the first time a dose cube is loaded) - if isempty(this.IsoDose_Levels) || ~this.NewIsoDoseFlag + if isempty(this.IsoDose_Levels) || ~this.NewIsoDoseFlag || ~this.checkUpdateNecessary({'viewer_options'},this.evt) vLevels = [0.1:0.1:0.9 0.95:0.05:upperMargin]; referenceDose = (minMaxRange(1,2))/(upperMargin); @@ -1138,29 +1142,16 @@ function initValues(this) planeCenters = ceil(ct.cubeDim./ 2); this.numOfBeams = 1; + + visQuantity = this.tryVisQuantityFromPln(); + if evalin('base','exist(''pln'')') pln = evalin('base','pln'); - - if isfield(pln,'propOpt') && isfield(pln.propOpt, 'quantityOpt') - switch pln.propOpt.quantityOpt - case 'physicalDose' - visQuantity = 'physicalDose'; - case {'RBExDose', 'effect'} - visQuantity = 'RBExDose'; - otherwise - visQuantity = []; - end - else - visQuantity = []; - end - if isfield(pln,'propStf') && isfield(pln.propStf,'isoCenter') isoCoordinates = matRad_world2cubeIndex(pln.propStf.isoCenter(1,:), ct); planeCenters = ceil(isoCoordinates); this.numOfBeams=pln.propStf.numOfBeams; end - else - visQuantity = []; end this.slice = planeCenters(this.plane); @@ -1193,13 +1184,8 @@ function initValues(this) this.updateDisplaySelection(visQuantity); else this.colorData=1; - if evalin('base','exist(''resultGUI'')') - this.SelectedDisplayAllOptions ='physicalDose'; - this.SelectedDisplayOption ='physicalDose'; - else - this.SelectedDisplayAllOptions = 'no option available'; - this.SelectedDisplayOption = ''; - end + this.SelectedDisplayAllOptions = 'no option available'; + this.SelectedDisplayOption = ''; end else %no data is loaded this.slice=1; @@ -1334,13 +1320,13 @@ function updateDisplaySelection(this,visSelection) if ~isempty(visSelection) && isfield(result,visSelection) this.SelectedDisplayOption = visSelection; elseif ~isfield(result,this.SelectedDisplayOption) - this.SelectedDisplayOption = 'physicalDose'; + this.SelectedDisplayOption = this.tryVisQuantityFromPln('physicalDose'); else %Keep option end if ~any(strcmp(this.SelectedDisplayOption,fieldnames(result))) - this.SelectedDisplayOption = 'physicalDose'; + this.SelectedDisplayOption = this.tryVisQuantityFromPln('physicalDose'); if ~any(strcmp(this.SelectedDisplayOption,fieldnames(result))) this.SelectedDisplayOption = this.DispInfo{find([this.DispInfo{:,2}],1,'first'),1}; end @@ -1354,5 +1340,32 @@ function updateDisplaySelection(this,visSelection) this.updateLock = currLock; end + + function visQuantity = tryVisQuantityFromPln(~, default) + if nargin < 2 + default = []; + end + visQuantity = default; + if evalin('base','exist(''pln'')') + pln = evalin('base','pln'); + if isfield(pln,'propOpt') && isfield(pln.propOpt, 'quantityOpt') + switch pln.propOpt.quantityOpt + case 'physicalDose' + visQuantity = 'physicalDose'; + case {'RBExDose', 'effect'} + visQuantity = 'RBExDose'; + otherwise + %Do Nothing + end + end + end + end + + function exportSlice(this,filename,varargin) + exportgraphics(this.handles.figure1,filename,varargin{:}); + end + end + + end \ No newline at end of file diff --git a/matRad/gui/widgets/matRad_VisualizationWidget.m b/matRad/gui/widgets/matRad_VisualizationWidget.m index 50a8aa224..d9e296a2d 100644 --- a/matRad/gui/widgets/matRad_VisualizationWidget.m +++ b/matRad/gui/widgets/matRad_VisualizationWidget.m @@ -248,7 +248,7 @@ function initialize(this) 'String','Plane Selection',... 'TooltipString','Display coronal, sagital or axial plane in intensity plots',... 'Style','text',... - 'Position',gridPos{3,2},... + 'Position',gridPos{3,2} - [0.01,0,0,0],... 'BackgroundColor',matRad_cfg.gui.backgroundColor,... 'ForegroundColor',matRad_cfg.gui.textColor,... 'FontSize',matRad_cfg.gui.fontSize,... diff --git a/matRad/gui/widgets/matRad_WorkflowWidget.m b/matRad/gui/widgets/matRad_WorkflowWidget.m index 9fa2e3489..e66759b76 100644 --- a/matRad/gui/widgets/matRad_WorkflowWidget.m +++ b/matRad/gui/widgets/matRad_WorkflowWidget.m @@ -20,6 +20,7 @@ % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% properties + savedResultTag = {}; end methods @@ -277,7 +278,7 @@ function btnLoadMat_Callback(this, hObject, event) 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'); @@ -286,9 +287,11 @@ function btnLoadMat_Callback(this, hObject, event) set(handles.exportDicomButton,'Enable','on'); % check if stf exists - if evalin('base','exist(''stf'')') + if evalin('base','exist(''stf'')') + stf = evalin('base','stf'); + % check if dij, stf and pln match - [plnStfMatch, msg] = matRad_comparePlnStf(evalin('base','pln'),evalin('base','stf')); + [plnStfMatch, msg] = matRad_comparePlnStf(pln,stf); if plnStfMatch % plan is ready for optimization set(handles.txtInfo,'String','ready for dose calculation'); @@ -300,8 +303,12 @@ function btnLoadMat_Callback(this, hObject, event) end % check if dij exist - if evalin('base','exist(''dij'')') && plnStfMatch - [dijStfMatch, msg] = matRad_compareDijStf(evalin('base','dij'),evalin('base','stf')); + 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 dijStfMatch set(handles.txtInfo,'String','ready for optimization'); set(handles.btnOptimize ,'Enable','on'); @@ -413,7 +420,11 @@ function btnCalcDose_Callback(this, hObject, eventdata) % carry out dose calculation try 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); + end % assign results to base worksapce assignin('base','dij',dij); @@ -465,22 +476,21 @@ function btnOptimize_Callback(this, hObject, eventdata) AllVarNames = evalin('base','who'); if ismember('resultGUI',AllVarNames) resultGUI = evalin('base','resultGUI'); - sNames = fieldnames(resultGUIcurrentRun); oldNames = fieldnames(resultGUI); - if(length(oldNames) > length(sNames)) + + if ~isempty(this.savedResultTag) for j = 1:length(oldNames) - if strfind(oldNames{j}, 'beam') - resultGUI = rmfield(resultGUI, oldNames{j}); + for k = 1:length(this.savedResultTag) + if ~isempty(strfind(oldNames{j}, this.savedResultTag{k})) + resultGUIcurrentRun.(oldNames{j}) = resultGUI.(oldNames{j}); + end end end - end - for j = 1:length(sNames) - resultGUI.(sNames{j}) = resultGUIcurrentRun.(sNames{j}); - end - else - resultGUI = resultGUIcurrentRun; + end end + resultGUI = resultGUIcurrentRun; + assignin('base','resultGUI',resultGUI); if ~pln.propOpt.runDAO || ~strcmp(pln.radiationMode,'photons') @@ -728,7 +738,8 @@ function SaveResultToGUI(this, ~, ~) Suffix = get(uiEdit(2),'String'); logIx = isstrprop(Suffix,'alphanum'); Suffix = ['_' Suffix(logIx)]; - + this.savedResultTag{end+1}= Suffix; + pln = evalin('base','pln'); resultGUI = evalin('base','resultGUI'); diff --git a/matRad/gui/widgets/matRad_importDicomWidget.m b/matRad/gui/widgets/matRad_importDicomWidget.m index 5cb106379..9c4deb25e 100644 --- a/matRad/gui/widgets/matRad_importDicomWidget.m +++ b/matRad/gui/widgets/matRad_importDicomWidget.m @@ -174,8 +174,8 @@ % to pass only selected objects to the matRad_importDicom % selected patient if ~isempty(handles.patient_listbox) - selected_patient = this.importer.patient{get(handles.patient_listbox,'Value')}; - this.importer.patient = selected_patient; + selected_patient = get(handles.patient_listbox,'Value'); + this.importer.selectedPatient = selected_patient; end % selected CT serie @@ -211,7 +211,7 @@ % selected dose serie allRTDoses = this.importer.importFiles.rtdose; if ~isempty(allRTDoses) && ~isempty(handles.doseseries_listbox.Value) - UIDSelected_rtdose = handles.doseseries_listbox.String{get(handles.doseseries_listbox,'Value'), 1}; + UIDSelected_rtdose = handles.doseseries_listbox.String(get(handles.doseseries_listbox,'Value')); selectedRTDose = allRTDoses(strcmp(allRTDoses(:, 4), UIDSelected_rtdose), :); this.importer.importFiles.rtdose = selectedRTDose; else @@ -222,7 +222,7 @@ %% save ct, cst, pln, dose matRad_cfg = MatRad_Config.instance(); - matRadFileName = fullfile(matRad_cfg.userfolders{1},[this.importer.patient '.mat']); % use default from dicom + matRadFileName = fullfile(matRad_cfg.userfolders{1},[this.importer.patients{this.importer.selectedPatient} '.mat']); % use default from dicom [FileName,PathName] = uiputfile('*.mat','Save as...',matRadFileName); ct = this.importer.ct; cst = this.importer.cst; @@ -970,13 +970,15 @@ this.importer = matRad_DicomImporter(get(handles.dir_path_field,'String')); - if iscell(this.importer.patient) - handles.fileList = this.importer.allfiles; - %handles.patient_listbox.String = patient_listbox; - set(handles.patient_listbox,'String',this.importer.patient,'Value',1); - % guidata(hObject, handles); - this.handles = handles; + handles.fileList = this.importer.allfiles; + %handles.patient_listbox.String = patient_listbox; + if isempty(this.importer.selectedPatient) || this.importer.selectedPatient > numel(this.importer.patients) + this.importer.selectedPatient = 1; end + + set(handles.patient_listbox,'String',this.importer.patients,'Value',this.importer.selectedPatient); + % guidata(hObject, handles); + this.handles = handles; end end diff --git a/matRad/matRad_calcDoseForward.m b/matRad/matRad_calcDoseForward.m index dbdb95753..436607e9e 100644 --- a/matRad/matRad_calcDoseForward.m +++ b/matRad/matRad_calcDoseForward.m @@ -2,8 +2,8 @@ % matRad forward dose calculation (no dij) % % call - % resultGUI = matRad_calcDoseForward(ct,stf,pln,cst) %If weights stored in stf - % resultGUI = matRad_calcDoseForward(ct,stf,pln,cst,w) + % resultGUI = matRad_calcDoseForward(ct,cst,stf,pln) %If weights stored in stf + % resultGUI = matRad_calcDoseForward(ct,cst,stf,pln,w) % % input % ct: ct cube diff --git a/matRad/matRad_fluenceOptimization.m b/matRad/matRad_fluenceOptimization.m index ecb213102..640b43e7c 100644 --- a/matRad/matRad_fluenceOptimization.m +++ b/matRad/matRad_fluenceOptimization.m @@ -146,9 +146,6 @@ % Check optimization quantity switch pln.propOpt.quantityOpt case 'effect' - if isa(pln.bioModel,'matRad_ConstantRBE') || (isstruct(pln.bioModel) && strcmp(pln.bioModel.model, 'constRBE')) - matRad_cfg.dispError('Effect optimization with constant RBE model not supported'); - end backProjection = matRad_EffectProjection; case 'RBExDose' %Capture special case of constant RBE @@ -281,21 +278,10 @@ wInit = ((doseTarget)/(TolEstBio*maxCurrRBE*max(doseTmp(V))))* wOnes; elseif strcmp(pln.propOpt.quantityOpt, 'BED') + abr = cst{ixTarget,5}.alphaX./cst{ixTarget,5}.betaX; + meanBED = mean((aTmp(V) + bTmp(V).^2)./cst{ixTarget,5}.alphaX); - if isfield(dij, 'mAlphaDose') && isfield(dij, 'mSqrtBetaDose') - abr = cst{ixTarget,5}.alphaX./cst{ixTarget,5}.betaX; - meanBED = mean((aTmp(V) + bTmp(V).^2)./cst{ixTarget,5}.alphaX) - %meanBED = mean((dij.mAlphaDose{1}(V,:)*wOnes + (dij.mSqrtBetaDose{1}(V,:)*wOnes).^2)./cst{ixTarget,5}.alphaX); - BEDTarget = doseTarget.*(1 + doseTarget./abr); - % elseif isfield(dij, 'RBE') - % abr = cst{ixTarget,5}.alphaX./cst{ixTarget,5}.betaX; - % meanBED = mean(dij.RBE.*dij.physicalDose{1}(V,:)*wOnes.*(1+dij.RBE.*dij.physicalDose{1}(V,:)*wOnes./abr)); - % BEDTarget = dij.RBE.*doseTarget.*(1 + dij.RBE.*doseTarget./abr); - % else - % abr = cst{ixTarget,5}.alphaX./cst{ixTarget,5}.betaX; - % meanBED = mean(dij.physicalDose{1}(V,:)*wOnes.*(1+dij.physicalDose{1}(V,:)*wOnes./abr)); - % BEDTarget = doseTarget.*(1 + doseTarget./abr); - end + BEDTarget = doseTarget.*(1 + doseTarget./abr); bixelWeight = BEDTarget/meanBED; wInit = wOnes * bixelWeight; @@ -311,6 +297,10 @@ matRad_cfg.dispInfo('chosen uniform weight of %f!\n',bixelWeight); end +if any(~isfinite(wInit)) + matRad_cfg.dispWarning('Invalid number in fluence weight initialization. Something might be off with your geometry. Setting invalid values to 1.'); + wInit(~isfinite(wInit)) = 1; +end %% calculate probabilistic quantities for probabilistic optimization if at least % one robust objective is defined diff --git a/matRad/matRad_version.m b/matRad/matRad_version.m index 5bf4b0ad9..f5f2b7ce8 100644 --- a/matRad/matRad_version.m +++ b/matRad/matRad_version.m @@ -32,7 +32,7 @@ %Hardcoded version name / numbers matRadVer.name = 'Cleve'; matRadVer.major = 3; -matRadVer.minor = 1; +matRadVer.minor = 2; matRadVer.patch = 0; tagged = false; diff --git a/matRad/optimization/@matRad_OptimizationProblem/matRad_constraintFunctions.m b/matRad/optimization/@matRad_OptimizationProblem/matRad_constraintFunctions.m index 41a7f9da5..7a1ab2956 100644 --- a/matRad/optimization/@matRad_OptimizationProblem/matRad_constraintFunctions.m +++ b/matRad/optimization/@matRad_OptimizationProblem/matRad_constraintFunctions.m @@ -53,7 +53,7 @@ for i = 1:size(cst,1) % Only take OAR or target VOI. - if ~isempty(cst{i,4}{1}) && ( isequal(cst{i,3},'OAR') || isequal(cst{i,3},'TARGET') ) + if ~isempty(cst{i,4}{1}) && any(strcmp(cst{i,3},{'OAR','TARGET','EXTERNAL'})) % loop over the number of constraints for the current VOI for j = 1:numel(cst{i,6}) diff --git a/matRad/optimization/@matRad_OptimizationProblem/matRad_constraintJacobian.m b/matRad/optimization/@matRad_OptimizationProblem/matRad_constraintJacobian.m index 4703f3777..7013cd891 100644 --- a/matRad/optimization/@matRad_OptimizationProblem/matRad_constraintJacobian.m +++ b/matRad/optimization/@matRad_OptimizationProblem/matRad_constraintJacobian.m @@ -62,7 +62,7 @@ for i = 1:size(cst,1) % Only take OAR or target VOI. - if ~isempty(cst{i,4}{1}) && ( isequal(cst{i,3},'OAR') || isequal(cst{i,3},'TARGET') ) + if ~isempty(cst{i,4}{1}) && any(strcmp(cst{i,3},{'OAR','TARGET','EXTERNAL'})) % loop over the number of constraints for the current VOI for j = 1:numel(cst{i,6}) diff --git a/matRad/optimization/@matRad_OptimizationProblem/matRad_getConstraintBounds.m b/matRad/optimization/@matRad_OptimizationProblem/matRad_getConstraintBounds.m index 0be9ad58c..9ad882bb0 100644 --- a/matRad/optimization/@matRad_OptimizationProblem/matRad_getConstraintBounds.m +++ b/matRad/optimization/@matRad_OptimizationProblem/matRad_getConstraintBounds.m @@ -39,7 +39,7 @@ for i = 1:size(cst,1) % Only take OAR or target VOI. - if ~any(cellfun(@isempty,cst{i,4})) && ( isequal(cst{i,3},'OAR') || isequal(cst{i,3},'TARGET') ) + if ~any(cellfun(@isempty,cst{i,4})) && any(strcmp(cst{i,3},{'OAR','TARGET','EXTERNAL'})) % loop over the number of constraints for the current VOI for j = 1:numel(cst{i,6}) diff --git a/matRad/optimization/@matRad_OptimizationProblem/matRad_getJacobianStructure.m b/matRad/optimization/@matRad_OptimizationProblem/matRad_getJacobianStructure.m index 28aec1666..ec5eabbff 100644 --- a/matRad/optimization/@matRad_OptimizationProblem/matRad_getJacobianStructure.m +++ b/matRad/optimization/@matRad_OptimizationProblem/matRad_getJacobianStructure.m @@ -32,23 +32,28 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Initializes constraints -jacobStruct = sparse([]); +jacobStruct = sparse([]); + +tmp = false(size(dij.physicalDose{1},1),1); % compute objective function for every VOI. for i = 1:size(cst,1) % Only take OAR or target VOI. - if ~any(cellfun(@isempty,cst{i,4})) && ( isequal(cst{i,3},'OAR') || isequal(cst{i,3},'TARGET') ) + if ~any(cellfun(@isempty,cst{i,4})) && any(strcmp(cst{i,3},{'OAR','TARGET','EXTERNAL'})) % loop over the number of constraints for the current VOI for j = 1:numel(cst{i,6}) obj = cst{i,6}{j}; % only perform computations for constraints - if isa(obj,'DoseConstraints.matRad_DoseConstraint') + if isa(obj,'DoseConstraints.matRad_DoseConstraint') + tmp(:) = false; + tmp(cst{i,4}{1}) = true; % get the jacobian structure depending on dose jacobDoseStruct = obj.getDoseConstraintJacobianStructure(numel(cst{i,4}{1})); nRows = size(jacobDoseStruct,2); - jacobStruct = [jacobStruct; repmat(spones(mean(dij.physicalDose{1}(cst{i,4}{1},:),1)),nRows,1)]; + %jacobStruct = [jacobStruct; repmat(spones(mean(dij.physicalDose{1}(cst{i,4}{1},:),1)),nRows,1)]; + jacobStruct = [jacobStruct; repmat(spones(double(tmp') * dij.physicalDose{1}),nRows,1)]; end end diff --git a/matRad/optimization/@matRad_OptimizationProblem/matRad_objectiveFunction.m b/matRad/optimization/@matRad_OptimizationProblem/matRad_objectiveFunction.m index 9d0539d00..b69e10bb5 100644 --- a/matRad/optimization/@matRad_OptimizationProblem/matRad_objectiveFunction.m +++ b/matRad/optimization/@matRad_OptimizationProblem/matRad_objectiveFunction.m @@ -58,7 +58,7 @@ for i = 1:size(cst,1) % Only take OAR or target VOI. - if ~isempty(cst{i,4}{1}) && ( isequal(cst{i,3},'OAR') || isequal(cst{i,3},'TARGET') ) + if ~isempty(cst{i,4}{1}) && any(strcmp(cst{i,3},{'OAR','TARGET','EXTERNAL'})) % loop over the number of constraints for the current VOI for j = 1:numel(cst{i,6}) diff --git a/matRad/optimization/@matRad_OptimizationProblem/matRad_objectiveGradient.m b/matRad/optimization/@matRad_OptimizationProblem/matRad_objectiveGradient.m index 0e5c9940e..15634cd78 100644 --- a/matRad/optimization/@matRad_OptimizationProblem/matRad_objectiveGradient.m +++ b/matRad/optimization/@matRad_OptimizationProblem/matRad_objectiveGradient.m @@ -66,7 +66,7 @@ for i = 1:size(cst,1) % Only take OAR or target VOI. - if ~isempty(cst{i,4}{1}) && ( isequal(cst{i,3},'OAR') || isequal(cst{i,3},'TARGET') ) + if ~isempty(cst{i,4}{1}) && any(strcmp(cst{i,3},{'OAR','TARGET','EXTERNAL'})) % loop over the number of constraints and objectives for the current VOI for j = 1:numel(cst{i,6}) diff --git a/matRad/optimization/optimizer/matRad_OptimizerSimulannealbnd.m b/matRad/optimization/optimizer/matRad_OptimizerSimulannealbnd.m index a8248a31a..77279cddc 100644 --- a/matRad/optimization/optimizer/matRad_OptimizerSimulannealbnd.m +++ b/matRad/optimization/optimizer/matRad_OptimizerSimulannealbnd.m @@ -76,11 +76,6 @@ % Informing user to press q to terminate optimization matRad_cfg.dispInfo('Optimization initiating...\n'); matRad_cfg.dispInfo('Press q to terminate the optimization...\n'); - - if matRad_cfg.isMatlab && str2double(matRad_cfg.envVersion) <= 9.13 && strcmp(obj.options.Diagnostics, 'on') - matRad_cfg.dispWarning('Diagnostics in simulannealbnd will be turned off due to a bug when using lbfgs with specified number of histories!'); - obj.options.Diagnostics = 'off'; - end % Define the objective function objectiveFunction = @(x) optiProb.matRad_objectiveFunction(x, dij, cst); diff --git a/matRad/optimization/projections/matRad_EffectProjection.m b/matRad/optimization/projections/matRad_EffectProjection.m index ea06d6c22..ca3e0dc7b 100644 --- a/matRad/optimization/projections/matRad_EffectProjection.m +++ b/matRad/optimization/projections/matRad_EffectProjection.m @@ -35,6 +35,8 @@ effect = []; matRad_cfg = MatRad_Config.instance(); matRad_cfg.dispWarning('Empty dij.ax scenario in optimization detected! This should not happen...\n'); + elseif isfield(dij,'RBE') && (isscalar(dij.RBE) || numel(dij.RBE) == size(dij.physicalDose{scen},1)) && all(isfinite(dij.RBE)) + effect = dij.ax{ctScen} .* (dij.physicalDose{scen} * w .* dij.RBE) + dij.bx{ctScen} .* (dij.physicalDose{scen} * w .* dij.RBE).^2; else effect = dij.ax{ctScen} .* (dij.physicalDose{scen} * w) + dij.bx{ctScen} .* (dij.physicalDose{scen}*w).^2; end @@ -48,10 +50,10 @@ matRad_cfg = MatRad_Config.instance(); matRad_cfg.dispWarning('Empty mAlphaDose scenario in optimization detected! This should not happen...\n'); else - vBias = (doseGrad{scen}' * dij.mAlphaDose{scen})'; + alphaTerm = (doseGrad{scen}' * dij.mAlphaDose{scen})'; quadTerm = dij.mSqrtBetaDose{scen} * w; - mPsi = (2*(doseGrad{scen}.*quadTerm)' * dij.mSqrtBetaDose{scen})'; - wGrad = vBias + mPsi; + betaTerm = (2*(doseGrad{scen}.*quadTerm)' * dij.mSqrtBetaDose{scen})'; + wGrad = alphaTerm + betaTerm; end else [ctScen,~,~] = ind2sub(size(dij.physicalDose),scen); %TODO: Workaround for now @@ -60,10 +62,18 @@ matRad_cfg = MatRad_Config.instance(); matRad_cfg.dispWarning('Empty dij.ax/dij.bx scenario in optimization detected! This should not happen...\n'); else - vBias = ((doseGrad{scen}.*dij.ax{ctScen})' * dij.physicalDose{scen})'; - quadTerm = dij.physicalDose{scen} * w; - mPsi = (2*(doseGrad{scen}.*quadTerm.*dij.bx{ctScen})' * dij.physicalDose{scen})'; - wGrad = vBias + mPsi; + physDose = dij.physicalDose{scen} * w; + if isfield(dij,'RBE') && (isscalar(dij.RBE) || numel(dij.RBE) == size(dij.physicalDose{scen},1)) && all(isfinite(dij.RBE)) + alpha = dij.ax{ctScen} .* dij.RBE; + beta = dij.bx{ctScen} .* dij.RBE.^2; + else + alpha = dij.ax{ctScen}; + beta = dij.bx{ctScen}; + end + alphaTerm = ((doseGrad{scen} .* alpha)' * dij.physicalDose{scen})'; + betaTerm = (2 * (doseGrad{scen} .* physDose .* beta)' * dij.physicalDose{scen})'; + + wGrad = alphaTerm + betaTerm; end end end @@ -91,10 +101,10 @@ if isempty(dij.mAlphaDoseExp{scen}) || isempty(dij.mSqrtBetaDoseExp{scen}) wGrad = []; else - vBias = (dExpGrad{scen}' * dij.mAlphaDoseExp{scen})'; + alphaTerm = (dExpGrad{scen}' * dij.mAlphaDoseExp{scen})'; quadTerm = dij.mSqrtBetaDoseExp{scen} * w; - mPsi = (2*(dExpGrad{scen}.*quadTerm)' * dij.mSqrtBetaDoseExp{scen})'; - wGrad = vBias + mPsi; + betaTerm = (2*(dExpGrad{scen}.*quadTerm)' * dij.mSqrtBetaDoseExp{scen})'; + wGrad = alphaTerm + betaTerm; wGrad = wGrad + 2 * dOmegaVgrad; end end diff --git a/matRad/planAnalysis/matRad_compareDose.m b/matRad/planAnalysis/matRad_compareDose.m index f03f63a8b..f79feb831 100644 --- a/matRad/planAnalysis/matRad_compareDose.m +++ b/matRad/planAnalysis/matRad_compareDose.m @@ -173,8 +173,9 @@ hfig.(planeName{plane}).('cube1').Ct,... hfig.(planeName{plane}).('cube1').Contour,... hfig.(planeName{plane}).('cube1').IsoDose] = ... - matRad_plotSliceWrapper(gca,ct,cstHandle,1,cube1,plane,sliceName{plane},[],[],colorcube,jet,doseWindow,[],100); - + matRad_plotSlice(ct, 'axesHandle', gca, 'cst', cstHandle, 'cubeIdx', 1, 'dose', cube1, 'plane', plane, 'slice', sliceName{plane}, 'contourColorMap', colorcube, 'doseColorMap', jet, 'doseWindow', doseWindow, 'voiSelection', 100); + %matRad_plotSliceWrapper(gca,ct,cstHandle,1,cube1,plane,sliceName{plane},[],[],colorcube,jet,doseWindow,[],100); + % Plot Dose 2 hfig.(planeName{plane}).('cube2').Axes = subplot(2,2,2,colorSpec{:}); [hfig.(planeName{plane}).('cube2').CMap,... @@ -182,7 +183,8 @@ hfig.(planeName{plane}).('cube2').Ct,... hfig.(planeName{plane}).('cube2').Contour,... hfig.(planeName{plane}).('cube2').IsoDose] = ... - matRad_plotSliceWrapper(gca,ct,cstHandle,1,cube2,plane,sliceName{plane},[],[],colorcube,jet,doseWindow,[],100); + matRad_plotSlice(ct, 'axesHandle', gca, 'cst', cstHandle, 'cubeIdx', 1, 'dose', cube2, 'plane', plane, 'slice', sliceName{plane}, 'contourColorMap', colorcube, 'doseColorMap', jet, 'doseWindow', doseWindow, 'voiSelection', 100); + %matRad_plotSliceWrapper(gca,ct,cstHandle,1,cube2,plane,sliceName{plane},[],[],colorcube,jet,doseWindow,[],100); % Plot absolute difference hfig.(planeName{plane}).('diff').Axes = subplot(2,2,3,colorSpec{:}); @@ -191,7 +193,8 @@ hfig.(planeName{plane}).('diff').Ct,... hfig.(planeName{plane}).('diff').Contour,... hfig.(planeName{plane}).('diff').IsoDose] = ... - matRad_plotSliceWrapper(gca,ct,cstHandle,1,differenceCube,plane,sliceName{plane},[],[],colorcube,diffCMap,doseDiffWindow,[],100); + matRad_plotSlice(ct, 'axesHandle', gca, 'cst', cstHandle, 'cubeIdx', 1, 'dose', differenceCube, 'plane', plane, 'slice', sliceName{plane}, 'contourColorMap', colorcube, 'doseColorMap', diffCMap, 'doseWindow', doseDiffWindow, 'voiSelection', 100); + %matRad_plotSliceWrapper(gca,ct,cstHandle,1,differenceCube,plane,sliceName{plane},[],[],colorcube,diffCMap,doseDiffWindow,[],100); % Plot gamma analysis hfig.(planeName{plane}).('gamma').Axes = subplot(2,2,4,colorSpec{:}); @@ -201,7 +204,8 @@ hfig.(planeName{plane}).('gamma').Ct,... hfig.(planeName{plane}).('gamma').Contour,... hfig.(planeName{plane}).('gamma').IsoDose]=... - matRad_plotSliceWrapper(gca,ct,cstHandle,1,gammaCube,plane,sliceName{plane},[],[],colorcube,gammaCMap,doseGammaWindow,[],100); + matRad_plotSlice(ct, 'axesHandle', gca, 'cst', cstHandle, 'cubeIdx', 1, 'dose', gammaCube, 'plane', plane, 'slice', sliceName{plane}, 'contourColorMap', colorcube, 'doseColorMap', gammaCMap, 'doseWindow', doseGammaWindow, 'voiSelection', 100); + %matRad_plotSliceWrapper(gca,ct,cstHandle,1,gammaCube,plane,sliceName{plane},[],[],colorcube,gammaCMap,doseGammaWindow,[],100); % Adjusting axes matRad_plotAxisLabels(hfig.(planeName{plane}).('cube1').Axes,ct,plane,sliceName{plane},[],100); diff --git a/matRad/planAnalysis/matRad_showDVH.m b/matRad/planAnalysis/matRad_showDVH.m index cbf67a700..b9ebde1cb 100644 --- a/matRad/planAnalysis/matRad_showDVH.m +++ b/matRad/planAnalysis/matRad_showDVH.m @@ -163,7 +163,7 @@ function matRad_showDVH(dvh,cst,varargin) pln.bioModel = matRad_BiologicalModel.validate(pln.bioModel,pln.radiationMode); if strcmp(pln.bioModel.model,'none') - xlabel('Dose [Gy]','FontSize',fontSizeValue); + xlabel(axesHandle,'Dose [Gy]','FontSize',fontSizeValue); else xlabel(axesHandle,'RBE x Dose [Gy(RBE)]','FontSize',fontSizeValue); end diff --git a/matRad/planAnalysis/samplingAnalysis/matRad_createAnimationForLatexReport.m b/matRad/planAnalysis/samplingAnalysis/matRad_createAnimationForLatexReport.m index 050b2632f..136ff19e0 100644 --- a/matRad/planAnalysis/samplingAnalysis/matRad_createAnimationForLatexReport.m +++ b/matRad/planAnalysis/samplingAnalysis/matRad_createAnimationForLatexReport.m @@ -103,7 +103,8 @@ function matRad_createAnimationForLatexReport(confidenceValue, ct, cst, slice, m for f=1:nFrames sampleCube = zeros(size(meanCube)); sampleCube(selectIx) = samples(:,f); - matRad_plotSliceWrapper(gca,ct,cst,1,sampleCube,3,slice,0,alpha,colorcube,jet,[0.01*dPres dPres*1.3],[0.1 0.25 0.6 0.9 0.95 1 1.05 1.25]'*dPres,[],legendColorbar,false);%,figXzoom,[figYzoom]); + matRad_plotSlice(ct,'axesHandle', gca, 'cst', cst, 'cubeIdx', 1, 'dose', sampleCube, 'plane', 3, 'slice', slice, 'thresh', 0, 'alpha', alpha, 'contourColorMap', colorcube, 'doseColorMap', jet, 'doseWindow', [0.01*dPres dPres*1.3], 'doseIsoLevels', [0.1 0.25 0.6 0.9 0.95 1 1.05 1.25]'*dPres, 'colorBarLabel', legendColorbar, 'boolPlotLegend', false); + %matRad_plotSliceWrapper(gca,ct,cst,1,sampleCube,3,slice,0,alpha,colorcube,jet,[0.01*dPres dPres*1.3],[0.1 0.25 0.6 0.9 0.95 1 1.05 1.25]'*dPres,[],legendColorbar,false);%,figXzoom,[figYzoom]); F(f) = getframe(gcf); im = frame2im(F(f)); [imind,cm] = rgb2ind(im,256); diff --git a/matRad/planAnalysis/samplingAnalysis/matRad_latexReport.m b/matRad/planAnalysis/samplingAnalysis/matRad_latexReport.m index a6bb26a73..21bde12ea 100644 --- a/matRad/planAnalysis/samplingAnalysis/matRad_latexReport.m +++ b/matRad/planAnalysis/samplingAnalysis/matRad_latexReport.m @@ -245,7 +245,8 @@ colorMapLabel = 'physical Dose [Gy]'; end fileSuffix = 'nominal'; - matRad_plotSliceWrapper(ax,ct,cst,1,doseCube,plane,slice,[],[],colors,[],[],[],[],colorMapLabel); + matRad_plotSlice(ct,'axesHandle', ax, 'cst', cst, 'cubeIdx', 1, 'dose', doseCube, 'plane', plane, 'slice', slice, 'contourColorMap', colors, 'colorBarLabel', colorMapLabel); + %matRad_plotSliceWrapper(ax,ct,cst,1,doseCube,plane,slice,[],[],colors,[],[],[],[],colorMapLabel); elseif cubesToPlot == 2 @@ -253,7 +254,8 @@ colorMapLabel = 'gamma index'; fileSuffix = 'gamma'; gammaColormap = matRad_getColormap('gammaIndex'); - matRad_plotSliceWrapper(ax,ct,cst,1,doseCube,plane,slice,[],[],colors,gammaColormap,[0 2],[],[],colorMapLabel); + matRad_plotSlice(ct,'axesHandle', ax, 'cst', cst, 'cubeIdx', 1, 'dose', doseCube, 'plane', plane, 'slice', slice, 'contourColorMap', colors, 'doseColorMap', gammaColormap, 'doseWindow', [0 2], 'colorBarLabel', colorMapLabel); + %matRad_plotSliceWrapper(ax,ct,cst,1,doseCube,plane,slice,[],[],colors,gammaColormap,[0 2],[],[],colorMapLabel); elseif cubesToPlot == 3 @@ -264,7 +266,8 @@ end doseCube = doseStat.stdCubeW; fileSuffix = 'stdW'; - matRad_plotSliceWrapper(ax,ct,cst,1,doseCube,plane,slice,[],[],colors,[],[],[],[],colorMapLabel); + matRad_plotSlice(ct,'axesHandle', ax, 'cst', cst, 'cubeIdx', 1, 'dose', doseCube, 'plane', plane, 'slice', slice, 'contourColorMap', colors, 'colorBarLabel', colorMapLabel); + %matRad_plotSliceWrapper(ax,ct,cst,1,doseCube,plane,slice,[],[],colors,[],[],[],[],colorMapLabel); end drawnow(); diff --git a/matRad/plotting/matRad_plotIsoDoseLines.m b/matRad/plotting/matRad_plotIsoDoseLines.m index 195ffb8ae..bbeb32278 100644 --- a/matRad/plotting/matRad_plotIsoDoseLines.m +++ b/matRad/plotting/matRad_plotIsoDoseLines.m @@ -58,11 +58,11 @@ %fly if isempty(isoContours) if plane == 1 - C = contourc(doseCube(slice,:,:),isoLevels); + C = contourc(squeeze(doseCube(slice,:,:)),isoLevels); elseif plane == 2 - C = contourc(doseCube(:,slice,:),isoLevels); + C = contourc(squeeze(doseCube(:,slice,:)),isoLevels); elseif plane == 3 - C = contourc(doseCube(:,:,slice),isoLevels); + C = contourc(squeeze(doseCube(:,:,slice)),isoLevels); end isoContours{slice,plane} = C; end diff --git a/matRad/scenarios/matRad_ImportanceScenarios.m b/matRad/scenarios/matRad_ImportanceScenarios.m index cee133084..a8f015a90 100644 --- a/matRad/scenarios/matRad_ImportanceScenarios.m +++ b/matRad/scenarios/matRad_ImportanceScenarios.m @@ -59,7 +59,7 @@ matRad_cfg = MatRad_Config.instance(); matRad_cfg.dispError('Invalid number of setup grid points, needs to be a positive scalar!'); end - this.numOfSetupGridPoints = inumGridPoints; + this.numOfSetupGridPoints = numGridPoints; this.updateScenarios(); end @@ -69,7 +69,7 @@ matRad_cfg = MatRad_Config.instance(); matRad_cfg.dispError('Invalid number of range grid points, needs to be a positive scalar!'); end - this.numOfRAngeGridPoints = inumGridPoints; + this.numOfRangeGridPoints = numGridPoints; this.updateScenarios(); end end diff --git a/matRad/scenarios/matRad_ScenarioModel.m b/matRad/scenarios/matRad_ScenarioModel.m index 7c5542468..f84a3dfc3 100644 --- a/matRad/scenarios/matRad_ScenarioModel.m +++ b/matRad/scenarios/matRad_ScenarioModel.m @@ -232,7 +232,19 @@ function listAllScenarios(this) %Use the root folder and the scenarios folder only folders = {fileparts(mfilename('fullpath'))}; folders = [folders matRad_cfg.userfolders]; - metaScenarioModels = matRad_findSubclasses(meta.class.fromName(mfilename('class')),'folders',folders,'includeSubfolders',true); + + persistent metaScenarioModels lastOptionalPaths + + %First we do a sanity check if persistently stored metaclasses are valid + if ~matRad_cfg.isOctave && ~isempty(metaScenarioModels) && ~all(cellfun(@isvalid,metaScenarioModels)) + matRad_cfg.dispWarning('Found invalid ScenarioModels, updating model cache.'); + metaScenarioModels = []; + end + + if isempty(metaScenarioModels) || (~isempty(lastOptionalPaths) && ~isequal(lastOptionalPaths, folders)) + lastOptionalPaths = folders; + metaScenarioModels = matRad_findSubclasses(meta.class.fromName(mfilename('class')),'folders',folders,'includeSubfolders',true); + end classList = matRad_identifyClassesByConstantProperties(metaScenarioModels,'shortName','defaults',{'nomScen'}); if isempty(classList) diff --git a/matRad/steering/matRad_StfGeneratorBase.m b/matRad/steering/matRad_StfGeneratorBase.m index 0f2353461..31c5a11ee 100644 --- a/matRad/steering/matRad_StfGeneratorBase.m +++ b/matRad/steering/matRad_StfGeneratorBase.m @@ -337,10 +337,14 @@ function createPatientGeometry(this) end methods (Static) - function generator = getGeneratorFromPln(pln) + function generator = getGeneratorFromPln(pln, warnDefault) %GETENGINE Summary of this function goes here % Detailed explanation goes here + if nargin < 2 + warnDefault = true; + end + matRad_cfg = MatRad_Config.instance(); generator = []; @@ -384,7 +388,9 @@ function createPatientGeometry(this) generatorHandle = generatorHandle{1}; end generator = generatorHandle(pln); - matRad_cfg.dispWarning('Using default stf generator %s!', generator.name); + if warnDefault + matRad_cfg.dispWarning('Using default stf generator %s!', generator.name); + end elseif ~isempty(classList) generatorHandle = classList(1).handle; generator = generatorHandle(pln); @@ -439,7 +445,20 @@ function createPatientGeometry(this) %Get available, valid classes through call to matRad helper function %for finding subclasses - availableStfGenerators = matRad_findSubclasses(mfilename('class'),'folders',optionalPaths,'includeAbstract',false); + persistent allAvailableStfGenerators lastOptionalPaths + + %First we do a sanity check if persistently stored metaclasses are valid + if ~matRad_cfg.isOctave && ~isempty(allAvailableStfGenerators) && ~all(cellfun(@isvalid,allAvailableStfGenerators)) + matRad_cfg.dispWarning('Found invalid Steering Geometry Generators, updating cache.'); + allAvailableStfGenerators = []; + end + + if isempty(allAvailableStfGenerators) || (~isempty(lastOptionalPaths) && ~isequal(lastOptionalPaths, optionalPaths)) + lastOptionalPaths = optionalPaths; + allAvailableStfGenerators = matRad_findSubclasses(mfilename('class'),'folders',optionalPaths,'includeAbstract',false); + end + + availableStfGenerators = allAvailableStfGenerators; %Now filter for pln ix = []; diff --git a/matRad/steering/matRad_StfGeneratorBrachy.m b/matRad/steering/matRad_StfGeneratorBrachy.m index 3a4de97f6..defaf258b 100644 --- a/matRad/steering/matRad_StfGeneratorBrachy.m +++ b/matRad/steering/matRad_StfGeneratorBrachy.m @@ -148,7 +148,7 @@ function initialize(this) % Prostate = plot3(TargX, TargY, TargZ, '.', 'Color', 'b', 'DisplayName', 'prostate'); % Prepare points for boundary calculation - P = [TargX', TargY', TargZ']; + P = [TargX, TargY, TargZ]; if ~isempty(P) % Determine the environment diff --git a/matRad/steering/matRad_StfGeneratorParticleIMPT.m b/matRad/steering/matRad_StfGeneratorParticleIMPT.m index 432011f58..7e8e2845b 100644 --- a/matRad/steering/matRad_StfGeneratorParticleIMPT.m +++ b/matRad/steering/matRad_StfGeneratorParticleIMPT.m @@ -1,6 +1,8 @@ classdef matRad_StfGeneratorParticleIMPT < matRad_StfGeneratorParticleRayBixelAbstract -% matRad_ParticleStfGenerator: Abstract Superclass for Steering information -% generators. Steering information is used to guide the dose calculation +% matRad_StfGeneratorParticleIMPT: IMPT Steering Geometry Setup (stf) +% Creates the stf data structure containing the steering information / +% field geometry for standard IMPT plans on a regular lateral spot grid +% with modulation in depth through energy layers % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % @@ -19,6 +21,7 @@ name = 'Particle IMPT stf Generator'; shortName = 'ParticleIMPT'; possibleRadiationModes = {'protons','helium','carbon'}; + airOffsetCorrection = true; end properties @@ -44,6 +47,7 @@ function beam = setBeamletEnergies(this,beam) %Assigns the max particle machine energy layers to all rays + matRad_cfg = MatRad_Config.instance(); isoCenterInCubeCoords = matRad_world2cubeCoords(beam.isoCenter,this.ct); @@ -52,11 +56,24 @@ else LUTspotSize = this.machine.meta.LUTspotSize; end - + + %Air Offset Correction + if this.airOffsetCorrection + if ~isfield(this.machine.meta, 'fitAirOffset') + this.machine.meta.fitAirOffset = 0; %By default we assume that the base data was fitted to a phantom with surface at isocenter + matRad_cfg.dispDebug('Asked for correction of Base Data Air Offset, but no value found. Using default value of %f mm.\n',this.machine.meta.fitAirOffset); + end + else + this.machine.meta.fitAirOffset = 0; + end beam.numOfBixelsPerRay = zeros(1,beam.numOfRays); for j = beam.numOfRays:-1:1 + + ctEntryPoint = zeros(this.multScen.totNumShiftScen,1); + + radDepthOffset = zeros(this.multScen.totNumShiftScen,1); for shiftScen = 1:this.multScen.totNumShiftScen % ray tracing necessary to determine depth of the target @@ -67,7 +84,14 @@ [this.ct.cube {this.voiTarget}]); %Used for generic range-shifter placement - ctEntryPoint = alphas(1) * d12; + ctEntryPoint(shiftScen) = alphas(1) * d12; + + if this.airOffsetCorrection + nozzleToSkin = (ctEntryPoint(shiftScen) + this.machine.meta.BAMStoIsoDist) - this.machine.meta.SAD; + radDepthOffset(shiftScen) = 0.0011 * (nozzleToSkin - this.machine.meta.fitAirOffset); + else + radDepthOffset(shiftScen) = 0; + end end % target hit @@ -90,8 +114,8 @@ % compute radiological depths % http://www.ncbi.nlm.nih.gov/pubmed/4000088, eq 14 - rSP = l{shiftScen} .* rho{shiftScen}{ctScen}; - radDepths = cumsum(rSP) - 0.5*rSP; + rSP = l{shiftScen} .* rho{shiftScen}{ctScen} ; + radDepths = cumsum(rSP) - 0.5*rSP + radDepthOffset(shiftScen); if this.multScen.relRangeShift(rangeShiftScen) ~= 0 || this.multScen.absRangeShift(rangeShiftScen) ~= 0 radDepths = radDepths +... % original cube @@ -154,7 +178,7 @@ raShi.ID = 1; raShi.eqThickness = rangeShifterEqD; - raShi.sourceRashiDistance = round(ctEntryPoint - 2*rangeShifterEqD,-1); %place a little away from entry, round to cms to reduce number of unique settings + raShi.sourceRashiDistance = round(min(ctEntryPoint) - 2*rangeShifterEqD,-1); %place a little away from entry, round to cms to reduce number of unique settings beam.ray(j).energy = [beam.ray(j).energy raShiEnergies]; beam.ray(j).rangeShifter = [beam.ray(j).rangeShifter repmat(raShi,1,length(raShiEnergies))]; @@ -288,6 +312,9 @@ % Check superclass availability [available,msg] = matRad_StfGeneratorParticleRayBixelAbstract.isAvailable(pln,machine); + + %Check additional base data + available = available && all(isfield(machine.data,{'peakPos','offset'})); if ~available return; diff --git a/matRad/steering/matRad_StfGeneratorParticleRayBixelAbstract.m b/matRad/steering/matRad_StfGeneratorParticleRayBixelAbstract.m index 1118d4fe0..7862cfecb 100644 --- a/matRad/steering/matRad_StfGeneratorParticleRayBixelAbstract.m +++ b/matRad/steering/matRad_StfGeneratorParticleRayBixelAbstract.m @@ -37,7 +37,7 @@ this@matRad_StfGeneratorExternalRayBixelAbstract(pln); if isempty(this.radiationMode) - this.radiationMode = 'protons'; + this.radiationMode = this.possibleRadiationModes{1}; end end @@ -53,7 +53,11 @@ function initialize(this) %Initialize Metadata needed for stf generators this.availableEnergies = [this.machine.data.energy]; - this.availablePeakPos = [this.machine.data.peakPos] + [this.machine.data.offset]; + if isfield(this.machine.data, 'peakPos') + this.availablePeakPos = [this.machine.data.peakPos] + [this.machine.data.offset]; + else + this.availablePeakPos = []; + end availableWidths = [this.machine.data.initFocus]; availableWidths = [availableWidths.SisFWHMAtIso]; this.maxPBwidth = max(availableWidths) / 2.355; @@ -93,9 +97,7 @@ function initialize(this) end available = available && isstruct(machine.data); - - available = available && all(isfield(machine.data,{'energy','peakPos','initFocus','offset'})); - + available = available && all(isfield(machine.data,{'energy','initFocus'})); if ~available msg = 'Your machine file is invalid and does not contain the basic fields required for photon machines!'; diff --git a/matRad/steering/matRad_StfGeneratorParticleSingleBeamlet.m b/matRad/steering/matRad_StfGeneratorParticleSingleBeamlet.m index e8f30dbb5..775617a6f 100644 --- a/matRad/steering/matRad_StfGeneratorParticleSingleBeamlet.m +++ b/matRad/steering/matRad_StfGeneratorParticleSingleBeamlet.m @@ -23,7 +23,7 @@ properties (Constant) name = 'Particle Single Spot'; shortName = 'ParticleSingleSpot'; - possibleRadiationModes = {'protons','helium','carbon'}; + possibleRadiationModes = {'protons','helium','carbon','VHEE'}; end methods diff --git a/matRad/steering/matRad_StfGeneratorParticleVHEE.m b/matRad/steering/matRad_StfGeneratorParticleVHEE.m new file mode 100644 index 000000000..fb097e639 --- /dev/null +++ b/matRad/steering/matRad_StfGeneratorParticleVHEE.m @@ -0,0 +1,134 @@ +classdef matRad_StfGeneratorParticleVHEE < matRad_StfGeneratorParticleRayBixelAbstract +% matRad_StfGeneratorParticleVHEE: VHEE Steering Geometry Setup (stf) +% Creates the stf data structure containing the steering information / +% field geometry for VHEE plans on a regular lateral spot grid by using a +% single, manually defined energy setting. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% 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 +% 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 = 'VHEE Geometry Generator'; + shortName = 'VHEE'; + possibleRadiationModes = {'VHEE'}; + end + + properties + energy; + end + + methods + function this = matRad_StfGeneratorParticleVHEE(pln) + if nargin < 1 + pln = []; + end + + this@matRad_StfGeneratorParticleRayBixelAbstract(pln); + end + end + + + methods (Access = protected) + + function beam = initBeamData(this, beam) + % Initialize beam data from the superclass, then handle single-energy logic + beam = initBeamData@matRad_StfGeneratorParticleRayBixelAbstract(this, beam); + % If user doesn't specify an energy, default to 200 MeV + if isempty(this.energy) + beam.VHEEenergy = 200; % Default + else + beam.VHEEenergy = this.energy; + end + this.energy = beam.VHEEenergy; + % Optional: check if that energy is in the machine data + if isfield(this.machine.data,'energies') && ~isempty(this.machine.data.energies) + if ~ismember(beam.VHEEenergy, this.machine.data.energies) + error(['The specified VHEE energy (',num2str(beam.VHEEenergy), ... + ' MeV) is not found in machine.data.energies!']); + end + end + end + + function beam = setBeamletEnergies(~,beam) + %Assigns defined particle machine energy from plan to all rays + + beam.numOfBixelsPerRay = zeros(1,beam.numOfRays); + beam.numOfRays = numel(beam.ray); + for j = beam.numOfRays:-1:1 + + %fix the energy for all rays for VHEE + beam.ray(j).focusIx = 1; + + beam.ray(j).energy = beam.VHEEenergy; + beam.ray(j).rangeShifter.ID = 0; + beam.ray(j).rangeShifter.eqThickness = 0; + beam.ray(j).rangeShifter.sourceRashiDistance = 0; + + beam.numOfBixelsPerRay(j) = 1; + end + end + + function beam = finalizeBeam(this,beam) + for j = beam.numOfRays:-1:1 + if isempty(beam.ray(j).energy) + beam.ray(j) = []; + beam.numOfBixelsPerRay(j) = []; + beam.numOfRays = beam.numOfRays - 1; + end + end + beam = this.finalizeBeam@matRad_StfGeneratorParticleRayBixelAbstract(beam); + 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_StfGeneratorParticleRayBixelAbstract.isAvailable(pln,machine); + + if ~available + return; + else + available = false; + msg = []; + end + + %checkBasic + try + %check modality + checkModality = any(strcmp(matRad_StfGeneratorParticleVHEE.possibleRadiationModes, machine.meta.radiationMode)) && any(strcmp(matRad_StfGeneratorParticleVHEE.possibleRadiationModes, pln.radiationMode)); + + %Sanity check compatibility + if checkModality + checkModality = strcmp(machine.meta.radiationMode,pln.radiationMode); + end + + preCheck = checkModality; + + if ~preCheck + return; + end + catch + msg = 'Your machine file is invalid and does not contain the basic field (meta/data/radiationMode)!'; + return; + end + + available = preCheck; + end + end +end diff --git a/matRad/util/matRad_comparePlnStf.m b/matRad/util/matRad_comparePlnStf.m index 6447019d8..7ac29ec60 100644 --- a/matRad/util/matRad_comparePlnStf.m +++ b/matRad/util/matRad_comparePlnStf.m @@ -46,18 +46,18 @@ end %% compare gantry angles in stf and pln -stf_gantryAngles=[stf.gantryAngle]; -if ~isfield(pln.propStf,'gantryAngles') || numel(stf_gantryAngles) ~= numel(pln.propStf.gantryAngles) ... % different size - || ~isempty(find(stf_gantryAngles-pln.propStf.gantryAngles, 1)) % values in stf and pln do not match % values in stf and pln do not match +stfGantryAngles=[stf.gantryAngle]; +if ~isfield(pln.propStf,'gantryAngles') || numel(stfGantryAngles) ~= numel(pln.propStf.gantryAngles) ... % different size + || ~isempty(find(stfGantryAngles-pln.propStf.gantryAngles, 1)) % values in stf and pln do not match % values in stf and pln do not match allMatch=false; msg= 'Gantry angles do not match'; return end %% compare couch angles in stf and pln -stf_couchAngles=[stf.couchAngle]; -if ~isfield(pln.propStf,'couchAngles') || numel(stf_couchAngles) ~= numel(pln.propStf.couchAngles) ... % different size - || ~isempty(find(stf_couchAngles-pln.propStf.couchAngles, 1)) % values in stf and pln do not match +stfCouchAngles=[stf.couchAngle]; +if ~isfield(pln.propStf,'couchAngles') || numel(stfCouchAngles) ~= numel(pln.propStf.couchAngles) ... % different size + || ~isempty(find(stfCouchAngles-pln.propStf.couchAngles, 1)) % values in stf and pln do not match allMatch=false; msg= 'Couch angles do not match'; return @@ -88,7 +88,12 @@ %% compare isocenter in stf and pln for each gantry angle for i = 1:numel(pln.propStf.gantryAngles) - if ~isempty(find(stf(i).isoCenter - pln.propStf.isoCenter(i,:) ,1)) + if size(pln.propStf.isoCenter,1) == 1 + isoCenter = repmat(pln.propStf.isoCenter,numel(stf),1); + else + isoCenter = pln.propStf.isoCenter; + end + if size(isoCenter,1) ~= numel(stf) || any(stf(i).isoCenter - isoCenter(i,:) ~= 0) allMatch=false; msg= 'Isocenters do not match'; return diff --git a/matRad/util/matRad_plotSlice.m b/matRad/util/matRad_plotSlice.m new file mode 100644 index 000000000..0ff7e4a56 --- /dev/null +++ b/matRad/util/matRad_plotSlice.m @@ -0,0 +1,259 @@ +function [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSlice(ct, varargin) +% matRad tool function to directly plot a complete slice of a ct with dose +% optionally including contours and isolines +% +% call +% [] = matRad_plotSlice(ct, dose, varargin) +% +% input (required) +% ct matRad ct struct +% +% input (optional/empty) to be called as Name-value pair arguments: +% dose dose cube +% axesHandle handle to axes the slice should be displayed in +% cst matRad cst struct +% cubeIdx Index of the desired cube in the ct struct +% plane plane view (coronal=1,sagittal=2,axial=3) +% slice slice in the selected plane of the 3D cube +% thresh threshold for display of dose values +% alpha alpha value for the dose overlay +% contourColorMap colormap for the VOI contours +% doseColorMap colormap for the dose +% doseWindow dose value window +% doseIsoLevels levels defining the isodose contours +% voiSelection logicals defining the current selection of contours +% that should be plotted. Can be set to [] to plot +% all non-ignored contours. +% colorBarLabel string defining the yLabel of the colorBar +% boolPlotLegend boolean if legend should be plottet or not +% showCt boolean if CT slice should be displayed or not +% varargin Additional MATLAB Line or Text Properties (e.g. 'LineWidth', 'FontSize', etc.) +% +% References +% - +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% 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 +% 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. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +defaultDose = []; +defaultCst = []; +defaultSlice = floor(min(ct.cubeDim)./2); +defaultAxesHandle = []; +defaultCubeIdx = 1; +defaultPlane = 1; +defaultDoseWindow = []; +defaultThresh = []; +defaultAlpha = []; +defaultDoseColorMap = jet; +defaultDoseIsoLevels = []; +defaultVOIselection = []; +defaultContourColorMap = []; +defaultBoolPlotLegend = false; +defaultColorBarLabel = []; +defaultShowCt = true; +defaultTitle = []; + +isDose = @(x) isnumeric(x) && all(size(x) == ct.cubeDim); +isSlice = @(x) x>=1 && x<=max(ct.cubeDim) && floor(x)==x; +isAxes = @(x) strcmp(get(x, 'type'), 'axes') || isempty(x); +isCubeIdx = @(x) isscalar(x); +isPlane = @(x) isscalar(x) && (sum(x==[1, 2, 3])==1); +isDoseWindow = @(x) (length(x) == 2 && isvector(x) && diff(x) > 0); +isThresh = @(x) (isscalar(x) && (x>=0) && (x<=1)) || isempty(x); +isAlpha = @(x) isscalar(x) && (x>=0) && (x<=1) || isempty(x); +isDoseColorMap = @(x) (isnumeric(x) && (size(x, 2)==3) && all(x(:) >= 0) && all(x(:) <= 1)) || isempty(x); +isDoseIsoLevels = @(x) isnumeric(x) && isvector(x)|| isempty(x); +isVOIselection = @(x) isnumeric(x) || isempty(x); %all(x(:)==1 | x(:)==0) || isempty(x); +isContourColorMap = @(x) (isnumeric(x) && (size(x, 2)==3) && size(x, 1)>=2 && all(x(:) >= 0) && all(x(:) <= 1)) || isempty(x); +isBoolPlotLegend = @(x) x==0 || x ==1; +isColorBarLabel = @(x) isstring(x) || ischar(x) || isempty(x); +isShowCt = @(x) isscalar(x) && (x==0) || (x==1); +isTitle = @(x) isstring(x) || ischar(x) || isempty(x); + +p = inputParser; +p.KeepUnmatched = true; + +addRequired(p, 'ct'); + +addParameter(p, 'dose', defaultDose, isDose); +addParameter(p, 'cst', defaultCst); +addParameter(p, 'slice', defaultSlice, isSlice); +addParameter(p, 'axesHandle', defaultAxesHandle, isAxes); +addParameter(p, 'cubeIdx', defaultCubeIdx, isCubeIdx); +addParameter(p, 'plane', defaultPlane, isPlane); +addParameter(p, 'doseWindow', defaultDoseWindow, isDoseWindow); +addParameter(p, 'thresh', defaultThresh, isThresh); +addParameter(p, 'alpha', defaultAlpha, isAlpha); +addParameter(p, 'doseColorMap', defaultDoseColorMap, isDoseColorMap); +addParameter(p, 'doseIsoLevels', defaultDoseIsoLevels, isDoseIsoLevels); +addParameter(p, 'voiSelection', defaultVOIselection, isVOIselection); +addParameter(p, 'contourColorMap', defaultContourColorMap, isContourColorMap); +addParameter(p, 'boolPlotLegend', defaultBoolPlotLegend, isBoolPlotLegend); +addParameter(p, 'colorBarLabel', defaultColorBarLabel, isColorBarLabel); +addParameter(p, 'showCt', defaultShowCt, isShowCt); +addParameter(p, 'title', defaultTitle, isTitle); + +p.parse(ct, varargin{:}); + +%% Unmatched properties +% General properties +% This is a hack with an invisible figure to obtain the properties +hTmpFig = figure('Visible','off','HandleVisibility','off'); +hTmpAx = axes(hTmpFig); +axesFieldNames = fieldnames(set(hTmpAx)); +lineFieldNames = fieldnames(set(line(hTmpAx))); +textFieldNames = fieldnames(set(text(hTmpAx))); +delete(hTmpAx); +close(hTmpFig); + +% Filter line properties from Unmatched +unmParamNames = fieldnames(p.Unmatched); +lineFields = unmParamNames(ismember(unmParamNames, lineFieldNames)); +lineValues = struct2cell(p.Unmatched); +lineValues = lineValues(ismember(unmParamNames, lineFieldNames)); +lineVarargin = reshape([lineFields, lineValues]', 1, []); + +% Filter text properties from Unmatched +textFields = unmParamNames(ismember(unmParamNames, textFieldNames)); +textValues = struct2cell(p.Unmatched); +textValues = textValues(ismember(unmParamNames, textFieldNames)); +textVarargin = reshape([textFields, textValues]', 1, []); + +% Filter axes properties from Unmatched +axesFields = unmParamNames(ismember(unmParamNames, axesFieldNames)); +axesValues = struct2cell(p.Unmatched); +axesValues = axesValues(ismember(unmParamNames, axesFieldNames)); +axesVarargin = reshape([axesFields, axesValues]', 1, []); + + +%% Plot ct slice +matRad_cfg = MatRad_Config.instance(); + +if isempty(p.Results.axesHandle) + axesHandle = axes(figure()); +else + axesHandle = p.Results.axesHandle; +end + +% Flip axes direction +set(axesHandle,'XTick',[],'YTick',[]); +set(axesHandle,'YDir','Reverse'); +% plot ct slice +if p.Results.showCt + hCt = matRad_plotCtSlice(axesHandle,p.Results.ct.cubeHU,p.Results.cubeIdx,p.Results.plane,p.Results.slice, [], []); +end +axis(axesHandle, 'off'); + +hold on; + +%% Plot dose +if ~isempty(p.Results.dose) + doseWindow = [min(p.Results.dose(:)) max(p.Results.dose(:))]; + if ~isempty(p.Results.doseWindow) + doseWindow = p.Results.doseWindow; + end + [hDose,doseColorMap,doseWindow] = matRad_plotDoseSlice(axesHandle, p.Results.dose, p.Results.plane, p.Results.slice, p.Results.thresh, p.Results.alpha, p.Results.doseColorMap, doseWindow); + hold on; + + %% Plot iso dose lines + hIsoDose = []; + if ~isempty(p.Results.doseIsoLevels) + hIsoDose = matRad_plotIsoDoseLines(axesHandle,p.Results.dose,[],p.Results.doseIsoLevels,false,p.Results.plane,p.Results.slice,p.Results.doseColorMap,p.Results.doseWindow, lineVarargin{:}); + end + + %% Set Colorbar + hCMap = matRad_plotColorbar(axesHandle,doseColorMap,doseWindow,'Location','EastOutside'); + set(hCMap,'Color',matRad_cfg.gui.textColor); + if ~isempty(p.Results.colorBarLabel) + set(get(hCMap,'YLabel'),'String', p.Results.colorBarLabel,'FontSize',matRad_cfg.gui.fontSize); + end + set(get(hCMap,'YLabel'),'String', p.Results.colorBarLabel, textVarargin{:}); +end +%% Plot VOI contours & Legend + +if ~isempty(p.Results.cst) + [hContour,~] = matRad_plotVoiContourSlice(axesHandle, p.Results.cst, p.Results.ct, p.Results.cubeIdx, p.Results.voiSelection, p.Results.plane, p.Results.slice, p.Results.contourColorMap, lineVarargin{:}); + + if p.Results.boolPlotLegend + visibleOnSlice = (~cellfun(@isempty,hContour)); + hContourTmp = cellfun(@(X) X(1),hContour(visibleOnSlice),'UniformOutput',false); + voiSelection = visibleOnSlice; + if ~isempty(p.Results.voiSelection) + voiSelection = visibleOnSlice(find(p.Results.voiSelection)); + end + hLegend = legend(axesHandle,[hContourTmp{:}],[p.Results.cst(voiSelection,2)],'AutoUpdate','off','TextColor',matRad_cfg.gui.textColor); + set(hLegend,'Box','On'); + set(hLegend,'TextColor',matRad_cfg.gui.textColor); + if ~isempty(textVarargin) + set(hLegend, textVarargin{:}); + else + set(hLegend,'FontSize',matRad_cfg.gui.fontSize); + end + end +else + hContour = []; +end + +%% Adjust axes +axis(axesHandle,'tight'); +set(axesHandle,'xtick',[],'ytick',[]); +%colormap(p.Results.axesHandle,p.Results.doseColorMap); +fontSize = []; +if isfield(p.Unmatched, 'FontSize') + fontSize = p.Unmatched.FontSize; +end +matRad_plotAxisLabels(axesHandle,p.Results.ct,p.Results.plane,p.Results.slice, fontSize, []) + +% Set axis ratio. +ratios = [1/p.Results.ct.resolution.x 1/p.Results.ct.resolution.y 1/p.Results.ct.resolution.z]; + +set(axesHandle,'DataAspectRatioMode','manual'); +if p.Results.plane == 1 + res = [ratios(3) ratios(2)]./max([ratios(3) ratios(2)]); + set(axesHandle,'DataAspectRatio',[res 1]) +elseif p.Results.plane == 2 % sagittal plane + res = [ratios(3) ratios(1)]./max([ratios(3) ratios(1)]); + set(axesHandle,'DataAspectRatio',[res 1]) +elseif p.Results.plane == 3 % Axial plane + res = [ratios(2) ratios(1)]./max([ratios(2) ratios(1)]); + set(axesHandle,'DataAspectRatio',[res 1]) +end + +%% Title +if ~isempty(p.Results.title) + title(axesHandle,p.Results.title); +end + +%% Set text properties +if ~isempty(textVarargin) + set(axesHandle, textVarargin{:}) + set(axesHandle.Title, textVarargin{:}) +end + +if ~exist('hCMap', 'var') + hCMap = []; +end +if ~exist('hDose', 'var') + hDose = []; +end +if ~exist('hCt', 'var') + hCt = []; +end +if ~exist('hContour', 'var') + hContour = []; +end +if ~exist('hIsoDose', 'var') + hIsoDose = []; +end + +end diff --git a/matRad/util/matRad_plotSliceWrapper.m b/matRad/util/matRad_plotSliceWrapper.m index bcc7d9436..4e73d3796 100644 --- a/matRad/util/matRad_plotSliceWrapper.m +++ b/matRad/util/matRad_plotSliceWrapper.m @@ -101,76 +101,9 @@ matRad_cfg = MatRad_Config.instance(); -set(axesHandle,'YDir','Reverse'); -% plot ct slice -hCt = matRad_plotCtSlice(axesHandle,ct.cubeHU,cubeIdx,plane,slice); -hold on; +matRad_cfg.dispDeprecationWarning('Deprecation warning: matRad_plotSliceWrapper is deprecated. Using matRad_plot_Slice instead'); -% plot dose -if ~isempty(doseWindow) && doseWindow(2) - doseWindow(1) <= 0 - doseWindow = [0 2]; -end - -[hDose,doseColorMap,doseWindow] = matRad_plotDoseSlice(axesHandle,dose,plane,slice,thresh,alpha,doseColorMap,doseWindow); - -% plot iso dose lines -if ~isempty(doseIsoLevels) - hIsoDose = matRad_plotIsoDoseLines(axesHandle,dose,[],doseIsoLevels,false,plane,slice,doseColorMap,doseWindow,varargin{:}); - hold on; -else - hIsoDose = []; -end - -%plot VOI contours -if ~isempty(cst) - - [hContour,~] = matRad_plotVoiContourSlice(axesHandle,cst,ct,cubeIdx,voiSelection,plane,slice,contourColorMap,varargin{:}); - -if boolPlotLegend - visibleOnSlice = (~cellfun(@isempty,hContour)); - ixLegend = find(voiSelection); - hContourTmp = cellfun(@(X) X(1),hContour(visibleOnSlice),'UniformOutput',false); - if ~isempty(voiSelection) - hLegend = legend(axesHandle,[hContourTmp{:}],[cst(ixLegend(visibleOnSlice),2)],'AutoUpdate','off','TextColor',matRad_cfg.gui.textColor); - else - hLegend = legend(axesHandle,[hContourTmp{:}],[cst(visibleOnSlice,2)],'AutoUpdate','off','TextColor',matRad_cfg.gui.textColor); - end - set(hLegend,'Box','Off'); - set(hLegend,'TextColor',matRad_cfg.gui.textColor); - set(hLegend,'FontSize',matRad_cfg.gui.fontSize); - -end -else - hContour = []; -end - -axis(axesHandle,'tight'); -set(axesHandle,'xtick',[],'ytick',[]); -colormap(axesHandle,doseColorMap); - -matRad_plotAxisLabels(axesHandle,ct,plane,slice,[]) - -% set axis ratio - -ratios = [1/ct.resolution.x 1/ct.resolution.y 1/ct.resolution.z]; - -set(axesHandle,'DataAspectRatioMode','manual'); -if plane == 1 - res = [ratios(3) ratios(2)]./max([ratios(3) ratios(2)]); - set(axesHandle,'DataAspectRatio',[res 1]) -elseif plane == 2 % sagittal plane - res = [ratios(3) ratios(1)]./max([ratios(3) ratios(1)]); - set(axesHandle,'DataAspectRatio',[res 1]) -elseif plane == 3 % Axial plane - res = [ratios(2) ratios(1)]./max([ratios(2) ratios(1)]); - set(axesHandle,'DataAspectRatio',[res 1]) -end - -hCMap = matRad_plotColorbar(axesHandle,doseColorMap,doseWindow,'Location','EastOutside'); -set(hCMap,'Color',matRad_cfg.gui.textColor); -if ~isempty(colorBarLabel) - set(get(hCMap,'YLabel'),'String', colorBarLabel,'FontSize',matRad_cfg.gui.fontSize); -end +[hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSlice(ct, 'axesHandle', axesHandle, 'cst', cst, 'cubeIdx', cubeIdx, 'dose', dose, 'plane', plane, 'slice', slice,'thresh', thresh, 'alpha', alpha, 'contourColorMap', contourColorMap, 'doseColorMap', doseColorMap, 'doseWindow', doseWindow, 'doseIsoLevels', doseIsoLevels, 'voiSelection', voiSelection, 'colorBarLabel', colorBarLabel, 'boolPlotLegend', boolPlotLegend, 'others', varargin); end diff --git a/matRadGUI.m b/matRadGUI.m index cb25e7f82..7d5e1a8a7 100644 --- a/matRadGUI.m +++ b/matRadGUI.m @@ -62,11 +62,10 @@ if matRad_cfg.disableGUI matRad_cfg.dispInfo('matRad GUI disabled in matRad_cfg!\n'); + hGUI = []; return; end -handleValid = true; - try handleValid = ishandle(hMatRadGUI.guiHandle); catch diff --git a/matRad_buildStandalone.m b/matRad_buildStandalone.m index 6feebb3e2..55de5a97e 100644 --- a/matRad_buildStandalone.m +++ b/matRad_buildStandalone.m @@ -23,8 +23,8 @@ matRadRoot = matRad_cfg.matRadRoot; standaloneFolder = fullfile(matRadRoot,'standalone'); +%Setup Input Parsing p = inputParser; - p.addParameter('isRelease',false,@islogical); %By default we compile a snapshot of the current branch p.addParameter('compileWithRT',false,@islogical); %By default we don't package installers with runtime p.addParameter('buildDir',fullfile(matRadRoot,'build'),@(x) ischar(x) || isstring(x)); %Build directory @@ -34,8 +34,8 @@ p.addParameter('python',false,@islogical); p.addParameter('json',[],@(x) isempty(x) || ischar(x) || isstring(x)); +%Parse and manage inputs p.parse(varargin{:}); - isRelease = p.Results.isRelease; compileWithRT = p.Results.compileWithRT; buildDir = p.Results.buildDir; @@ -56,12 +56,20 @@ rtOption = 'web'; end -try +%Display OS for debugging and information +arch = computer('arch'); +fprintf('Build Architecture: %s\n',arch); +archcheck = string([ispc,isunix,ismac]).cellstr(); +fprintf('pc\t\tunix\tmac\n%s\t%s\t%s\n',archcheck{:}); + +%Check build directory +try mkdir(buildDir); catch ME error(ME.identifier,'Could not create build directory %s\n Error:',buildDir,ME.message); end +%Docker build? if buildDocker && isunix && ~ismac warning('Can''t build docker container. Only works on linux!'); buildDocker = false; @@ -103,7 +111,7 @@ 'ExecutableVersion',vernumApp,... 'TreatInputsAsNumeric','off',... 'Verbose',verbose); - + if ispc resultsStandalone = compiler.build.standaloneWindowsApplication(buildOpts); else @@ -120,20 +128,12 @@ %% Package if ispc readmeFile = 'readme_standalone_windows.txt'; - installerId = 'Win64'; elseif ismac readmeFile = 'readme_standalone_mac.txt'; - [~,result] = system('uname -m'); - if any(strfind(result,'ARM64')) %is m1mac - installerId = 'MacARM'; - else - installerId = 'Mac64'; - end - else readmeFile = 'readme_standalone_linux.txt'; - installerId = 'Linux64'; end +installerId = arch; try packageOpts = compiler.package.InstallerOptions(resultsStandalone,... @@ -147,7 +147,7 @@ 'InstallerIcon',fullfile(standaloneFolder,'matRad_icon.png'),... 'InstallerLogo',fullfile(standaloneFolder,'matRad_installscreen.png'),... 'InstallerSplash',fullfile(standaloneFolder,'matRad_splashscreen.png'),... - 'InstallerName',sprintf('matRad_installer%s_v%s',installerId,vernumApp),... + 'InstallerName',sprintf('matRad_installer_%s_v%s',installerId,vernumApp),... 'OutputDir',fullfile(buildDir,'installer'),... 'RuntimeDelivery',rtOption,... 'Summary','matRad is an open source treatment planning system for radiation therapy written in Matlab.',... @@ -208,34 +208,33 @@ %% Docker if buildDocker try + if isRelease + imageName = ['matRad:' vernumInstall]; + else + imageName = 'matRad:develop'; + end dockerOpts = compiler.package.DockerOptions(results,... 'AdditionalInstructions','',... 'AdditionalPackages','',... 'ContainerUser','appuser',... 'DockerContext',fullfile(buildDir,'docker'),... 'ExecuteDockerBuild','on',... - 'ImageName','e0404/matrad'); + 'ImageName',imageName); compiler.package.docker(results,'Options',dockerOpts); buildResult.docker.image = dockerOpts.ImageName; catch ME warning(ME.identifier,'Java build failed due to %s!',ME.message); end -end +end if ~isempty(json) try fH = fopen(json,'w'); - jsonStr = jsonencode(buildResult,"PrettyPrint",true); + jsonStr = jsonencode(buildResult,"PrettyPrint",true); fwrite(fH,jsonStr); - fclose(fH); + fclose(fH); catch ME warning(ME.identifier,'Could not open JSON file for writing: %s',ME.message); end end - - - - - - diff --git a/test/autoExampleTest/test_examples.m b/test/autoExampleTest/test_examples.m index d533ed34c..e95ad3cda 100644 --- a/test/autoExampleTest/test_examples.m +++ b/test/autoExampleTest/test_examples.m @@ -23,6 +23,8 @@ 'examples/matRad_example12_simpleParticleMonteCarlo.m',... 'examples/matRad_example15_brachy.m',... 'examples/matRad_example17_biologicalModels.m',... + 'examples/matRad_example19_CT_sCT_DVH_difference_photons.m',... + 'examples/matRad_example20_VHEE.m',... 'matRad.m',... }; diff --git a/test/bioModel/test_biologicalModel.m b/test/bioModel/test_biologicalModel.m index ebe801c9b..51af189d8 100644 --- a/test/bioModel/test_biologicalModel.m +++ b/test/bioModel/test_biologicalModel.m @@ -23,12 +23,39 @@ if moxunit_util_platform_is_octave() assertExceptionThrown(@(model) matRad_bioModel('photons', 'MCN')); assertExceptionThrown(@(model) matRad_bioModel('protons', 'HEL')); - else assertExceptionThrown(@(model) matRad_bioModel('photons', 'MCN'), 'matRad:Error'); assertExceptionThrown(@(model) matRad_bioModel('protons', 'HEL'),'matRad:Error'); end + +function test_setBiologicalModelProvidedQuantities + bioModel = matRad_bioModel('protons', 'MCN', {'physicalDose','LET'}); + assertTrue(isa(bioModel, 'matRad_MCNamara')); + + if moxunit_util_platform_is_octave() + assertExceptionThrown(@(model) matRad_bioModel('protons', 'MCN', {'physicalDose'})); + else + assertExceptionThrown(@(model) matRad_bioModel('photons', 'MCN', {'physicalDose'}), 'matRad:Error'); + end + +function test_tissueParameters_emptyModel + bioModel = matRad_EmptyBiologicalModel(); + abx = bioModel.getAvailableTissueParameters(struct()); + assertTrue(isempty(abx)); + +function test_tissueParameters_kernelModel + bioModel = matRad_KernelBasedLEM(); + abx = bioModel.getAvailableTissueParameters(struct('machine','Generic','radiationMode','carbon')); + assertTrue(isnumeric(abx)); + assertEqual(size(abx,2),2); + assertTrue(size(abx,1) >= 1); + if moxunit_util_platform_is_octave() + assertExceptionThrown(@() bioModel.getAvailableTissueParameters(struct('machine','Generic','radiationMode','photons'))); + else + assertExceptionThrown(@() bioModel.getAvailableTissueParameters(struct('machine','Generic','radiationMode','photons')), 'matRad:Error'); + end + function test_calcBiologicalQuantitiesForBixel_MCN bioModel = matRad_bioModel('protons','MCN'); diff --git a/test/brachy/test_calcBrachyDose.m b/test/brachy/test_calcBrachyDose.m index ce464a04c..74fd777f1 100644 --- a/test/brachy/test_calcBrachyDose.m +++ b/test/brachy/test_calcBrachyDose.m @@ -8,6 +8,7 @@ function test_rightOutput() engine = DoseEngines.matRad_TG43BrachyEngine; pln.bioModel = matRad_bioModel('brachy', 'none'); + pln.radiationMode = {'brachy'}; engine.assignPropertiesFromPln(pln); load PROSTATE.mat ct cst; diff --git a/test/doseCalc/test_Analytical.m b/test/doseCalc/test_Analytical.m index a04f53fdb..02d6d83fd 100644 --- a/test/doseCalc/test_Analytical.m +++ b/test/doseCalc/test_Analytical.m @@ -11,15 +11,6 @@ engine = DoseEngines.matRad_ParticleAnalyticalBortfeldEngine.getEngineFromPln(testData.pln); assertTrue(isa(engine,'DoseEngines.matRad_ParticleAnalyticalBortfeldEngine')); - % Double Gaussian lateral model - % If you don't have my clusterDose basedata you cannot try this :P - %{ - testData.pln = struct('radiationMode','protons','machine','Generic_clusterDose'); - testData.pln.propDoseCalc.engine = 'AnalyticalPB'; - engine = DoseEngines.matRad_ParticleAnalyticalBortfeldEngine.getEngineFromPln(testData.pln); - assertTrue(isa(engine,'DoseEngines.matRad_ParticleAnalyticalBortfeldEngine')); - %} - function test_loadMachineForAnalytical possibleRadModes = DoseEngines.matRad_ParticleAnalyticalBortfeldEngine.possibleRadiationModes; for i = 1:numel(possibleRadModes) diff --git a/test/doseCalc/test_FREDEngine.m b/test/doseCalc/test_FREDEngine.m new file mode 100644 index 000000000..b455c9f2b --- /dev/null +++ b/test/doseCalc/test_FREDEngine.m @@ -0,0 +1,141 @@ +function test_suite = test_FREDEngine + +test_functions=localfunctions(); + +initTestSuite; + +function test_constructFREDEngine + radModes = DoseEngines.matRad_ParticleFREDEngine.possibleRadiationModes; + for i = 1:numel(radModes) + plnDummy = struct('radiationMode',radModes{i},'machine','Generic','propDoseCalc',struct('engine','FRED')); + engine = DoseEngines.matRad_ParticleFREDEngine(plnDummy); + assertTrue(isa(engine,'DoseEngines.matRad_ParticleFREDEngine')); + end + +function test_constructFailOnWrongRadMode + plnDummy = struct('radiationMode','brachy','machine','HDR','propDoseCalc',struct('engine','FRED')); + assertExceptionThrown(@()DoseEngines.matRad_ParticleFREDEngine(plnDummy)); + +function test_propertyAssignmentFromPln + + radModes = DoseEngines.matRad_ParticleFREDEngine.possibleRadiationModes; + + for i = 1:numel(radModes) + pln = struct('radiationMode',radModes{i},'machine','Generic','propDoseCalc',struct('engine','FRED')); + + pln.propDoseCalc.HUclamping = false; + pln.propDoseCalc.HUtable = 'matRad_default_FredMaterialConverter'; + pln.propDoseCalc.externalCalculation = 'write'; + pln.propDoseCalc.sourceModel = 'gaussian'; + pln.propDoseCalc.useGPU = false; + pln.propDoseCalc.roomMaterial = 'Vacuum'; + pln.propDoseCalc.printOutput = false; + pln.propDoseCalc.numHistoriesDirect = 42; + pln.propDoseCalc.numHistoriesPerBeamlet = 42; + + engine = DoseEngines.matRad_ParticleFREDEngine(pln); + + assertTrue(isa(engine,'DoseEngines.matRad_ParticleFREDEngine')); + + plnFields = fieldnames(pln.propDoseCalc); + plnFields(strcmp([plnFields(:)], 'engine')) = []; + + for fieldIdx=1:numel(plnFields) + assertTrue(isequal(engine.(plnFields{fieldIdx}), pln.propDoseCalc.(plnFields{fieldIdx}))); + end + end + +function test_writeFiles + + matRad_cfg = MatRad_Config.instance(); + radModes = DoseEngines.matRad_ParticleFREDEngine.possibleRadiationModes; + + load([radModes{1} '_testData.mat']); + pln.radiationMode = radModes{1}; + pln.machine = 'Generic'; + pln.propDoseCalc.engine = 'FRED'; + pln.propDoseCalc.externalCalculation = 'write'; + + w = ones(sum([stf(:).totalNumOfBixels]),1); + + resultGUI = matRad_calcDoseForward(ct,cst,stf,pln,w); + + fredMainDir = fullfile(matRad_cfg.primaryUserFolder, 'FRED'); + runFolder = fullfile(fredMainDir, 'MCrun'); + inputFolder = fullfile(runFolder, 'inp'); + planFolder = fullfile(inputFolder, 'plan'); + regionsFolder = fullfile(inputFolder, 'regions'); + + + assertTrue(all(cellfun(@isfolder, {fredMainDir,runFolder,inputFolder,planFolder,regionsFolder}))); + assertTrue(all(cellfun(@isfile, {fullfile(planFolder, 'plan.inp'),... + fullfile(planFolder, 'planDelivery.inp'),... + fullfile(regionsFolder, 'CTpatient.raw'),... + fullfile(regionsFolder, 'CTpatient.mhd'),... + fullfile(regionsFolder, 'regions.inp'),... + fullfile(runFolder, 'fred.inp'),... + }))); + +function test_loadDij + + matRad_cfg = MatRad_Config.instance(); + load(['protons_testData.mat']); + pln.machine = 'Generic'; + pln.propDoseCalc.engine = 'FRED'; + pln.propDoseCalc.useGPU = true; + pln.propDoseCalc.externalCalculation = fullfile(matRad_cfg.matRadRoot, 'test', 'testData', 'FRED_data'); + + % Test dij-load + dijFredLoad = matRad_calcDoseInfluence(ct,cst,stf,pln); + + % Test forward calculation cube load + w = ones(sum([stf(:).totalNumOfBixels]),1); + forwardDoseFredLoad = matRad_calcDoseForward(ct,cst,stf,pln,w); + + resultGUI = matRad_calcCubes(w, dijFredLoad, 1); + + nBixels = sum([stf(:).totalNumOfBixels]); + nVoxles = prod(ct.cubeDim); + + % Assert basic parameters + assertTrue(isequal(dijFredLoad.externalCalculationLodPath, fullfile(pln.propDoseCalc.externalCalculation, 'MCrun', 'out', 'scoreij', 'Phantom.Dose.bin'))); + assertTrue(isequal(size(dijFredLoad.physicalDose{1}),[nVoxles, nBixels])); + assertTrue(isequal(size(forwardDoseFredLoad.physicalDose), size(resultGUI.physicalDose))); + + +function test_bioCalculation + + matRad_cfg = MatRad_Config.instance(); + load(['protons_testData.mat']); + pln.machine = 'Generic'; + pln.propDoseCalc.bioModel = matRad_bioModel('protons', 'MCN'); + + pln.propDoseCalc.engine = 'FRED'; + pln.propDoseCalc.useGPU = true; + pln.propDoseCalc.externalCalculation = fullfile(matRad_cfg.matRadRoot, 'test', 'testData', 'FRED_data'); + + % Test dij-load + dijFredLoad = matRad_calcDoseInfluence(ct,cst,stf,pln); + + % Test forward calculation cube load + w = ones(sum([stf(:).totalNumOfBixels]),1); + forwardDoseFredLoad = matRad_calcDoseForward(ct,cst,stf,pln,w); + + % Assert basic parameters + assertTrue(all(cellfun(@(x) isfield(dijFredLoad, x), {'physicalDose', 'mLETd', 'mAlphaDose', 'mSqrtBetaDose'}))); + assertTrue(all(cellfun(@(x) isfield(forwardDoseFredLoad, x), {'physicalDose', 'LET', 'alpha', 'beta', 'effect'}))); + + +function test_additionalParameters + + matRad_cfg = MatRad_Config.instance(); + load(['protons_testData.mat']); + pln.machine = 'Generic'; + + pln.propDoseCalc.HUtable = 'internal'; + pln.propDoseCalc.sourceModel = 'emittance'; + pln.propDoseCalc.HUclamping = true; + + engine = DoseEngines.matRad_ParticleFREDEngine(pln); + + assertTrue(all(cellfun(@(x,y) isequal(engine.(x), y), {'sourceModel','HUtable', 'HUclamping'}, {'emittance','internal', true}))); \ No newline at end of file diff --git a/test/doseCalc/test_FSPB.m b/test/doseCalc/test_FSPB.m new file mode 100644 index 000000000..9f59ed3d5 --- /dev/null +++ b/test/doseCalc/test_FSPB.m @@ -0,0 +1,124 @@ +function test_suite = test_FSPB + + test_functions=localfunctions(); + + initTestSuite; + + function test_getSubsamplingPBEngineFromPln + % Single gaussian lateral model + testData.pln = struct('radiationMode','protons','machine','Generic'); + testData.pln.propDoseCalc.engine = 'SubsamplingPB'; + engine = DoseEngines.matRad_ParticleFineSamplingPencilBeamEngine.getEngineFromPln(testData.pln); + assertTrue(isa(engine,'DoseEngines.matRad_ParticleFineSamplingPencilBeamEngine')); + + function test_loadMachineForSubsamplingPB + possibleRadModes = DoseEngines.matRad_ParticleFineSamplingPencilBeamEngine.possibleRadiationModes; + for i = 1:numel(possibleRadModes) + machine = DoseEngines.matRad_ParticleFineSamplingPencilBeamEngine.loadMachine(possibleRadModes{i},'Generic'); + assertTrue(isstruct(machine)); + assertTrue(isfield(machine, 'meta')); + assertTrue(isfield(machine.meta, 'radiationMode')); + assertTrue(strcmp(machine.meta.radiationMode, possibleRadModes{i})); + end + + + function test_calcDoseSubsamplingPBprotonsFitCircle + testData = load('protons_testData.mat'); + + assertTrue(DoseEngines.matRad_ParticleFineSamplingPencilBeamEngine.isAvailable(testData.pln)); + + testData.pln.propDoseCalc.engine = 'SubsamplingPB'; + testData.pln.propDoseCalc.dosimetricLateralCutOff = 0.995; + testData.pln.propDoseCalc.geometricLateralCutOff = 50; + testData.pln.propDoseCalc.fineSampling.method = 'fitCircle'; + + for N = [2,3,8] + testData.pln.propDoseCalc.fineSampling.N = N; + + resultGUI = matRad_calcDoseForward(testData.ct, testData.cst, testData.stf, testData.pln, ones(sum([testData.stf(:).totalNumOfBixels]),1)); + + assertTrue(isequal(fieldnames(resultGUI),fieldnames(testData.resultGUI))); + assertTrue(isequal(testData.ct.cubeDim, size(resultGUI.physicalDose))); + assertElementsAlmostEqual(resultGUI.physicalDose,testData.resultGUI.physicalDose,'relative',1e-2,1e-2); + end + + function test_calcDoseSubsamplingPBprotonsFitSquare + testData = load('protons_testData.mat'); + + assertTrue(DoseEngines.matRad_ParticleFineSamplingPencilBeamEngine.isAvailable(testData.pln)); + + testData.pln.propDoseCalc.engine = 'SubsamplingPB'; + testData.pln.propDoseCalc.dosimetricLateralCutOff = 0.995; + testData.pln.propDoseCalc.geometricLateralCutOff = 50; + testData.pln.propDoseCalc.fineSampling.method = 'fitSquare'; + + for N = [2,3] + testData.pln.propDoseCalc.fineSampling.N = N; + resultGUI = matRad_calcDoseForward(testData.ct, testData.cst, testData.stf, testData.pln, ones(sum([testData.stf(:).totalNumOfBixels]),1)); + + assertTrue(isequal(fieldnames(resultGUI),fieldnames(testData.resultGUI))); + assertTrue(isequal(testData.ct.cubeDim, size(resultGUI.physicalDose))); + assertElementsAlmostEqual(resultGUI.physicalDose,testData.resultGUI.physicalDose,'relative',1e-2,1e-2); + end + + function test_calcDoseSubsamplingPBprotonsRusso + testData = load('protons_testData.mat'); + + testData.pln.propDoseCalc.fineSampling.N = 10; + testData.pln.propDoseCalc.fineSampling.sigmaSub = 2; + testData.pln.propDoseCalc.fineSampling.method = 'russo'; + + resultGUI = matRad_calcDoseForward(testData.ct, testData.cst, testData.stf, testData.pln, ones(sum([testData.stf(:).totalNumOfBixels]),1)); + + assertTrue(isequal(fieldnames(resultGUI),fieldnames(testData.resultGUI))); + assertTrue(isequal(testData.ct.cubeDim, size(resultGUI.physicalDose))); + assertElementsAlmostEqual(resultGUI.physicalDose,testData.resultGUI.physicalDose,'relative',1e-2,1e-2); + + function test_calcDoseSubsamplingPBhelium + testData = load('helium_testData.mat'); + assertTrue(DoseEngines.matRad_ParticleFineSamplingPencilBeamEngine.isAvailable(testData.pln)); + + testData.pln.propDoseCalc.engine = 'SubsamplingPB'; + testData.pln.propDoseCalc.dosimetricLateralCutOff = 0.995; + testData.pln.propDoseCalc.geometricLateralCutOff = 50; + testData.pln.propDoseCalc.fineSampling.N = 10; + testData.pln.propDoseCalc.fineSampling.sigmaSub = 2; + testData.pln.propDoseCalc.fineSampling.method = 'russo'; + resultGUI = matRad_calcDoseForward(testData.ct, testData.cst, testData.stf, testData.pln, ones(sum([testData.stf(:).totalNumOfBixels]),1)); + + assertTrue(isequal(fieldnames(resultGUI),fieldnames(testData.resultGUI))); + assertTrue(isequal(testData.ct.cubeDim, size(resultGUI.physicalDose))); + assertElementsAlmostEqual(resultGUI.physicalDose,testData.resultGUI.physicalDose,'relative',1e-2,1e-2); + + function test_calcDoseSubsamplingPBcarbon + testData = load('carbon_testData.mat'); + assertTrue(DoseEngines.matRad_ParticleFineSamplingPencilBeamEngine.isAvailable(testData.pln)); + + testData.pln.propDoseCalc.engine = 'SubsamplingPB'; + testData.pln.propDoseCalc.dosimetricLateralCutOff = 0.995; + testData.pln.propDoseCalc.geometricLateralCutOff = 50; + testData.pln.propDoseCalc.fineSampling.N = 10; + testData.pln.propDoseCalc.fineSampling.sigmaSub = 2; + testData.pln.propDoseCalc.fineSampling.method = 'russo'; + resultGUI = matRad_calcDoseForward(testData.ct, testData.cst, testData.stf, testData.pln, ones(sum([testData.stf(:).totalNumOfBixels]),1)); + + assertTrue(isequal(fieldnames(resultGUI),fieldnames(testData.resultGUI))); + assertTrue(isequal(testData.ct.cubeDim, size(resultGUI.physicalDose))); + assertElementsAlmostEqual(resultGUI.physicalDose,testData.resultGUI.physicalDose,'relative',1e-2,1e-2); + + + function test_nonSupportedSettings + % Radiation mode other than protons not implemented + testData = load('photons_testData.mat'); + testData.pln.propDoseCalc.engine = 'SubsamplingPB'; + assertFalse(DoseEngines.matRad_ParticleFineSamplingPencilBeamEngine.isAvailable(testData.pln)); + + % Invalid machine without radiation mode field + testData.pln.machine = 'Empty'; + testData.pln.propDoseCalc.engine = 'SubsamplingPB'; + assertExceptionThrown(@() DoseEngines.matRad_ParticleFineSamplingPencilBeamEngine.isAvailable(testData.pln)); + assertFalse(DoseEngines.matRad_ParticleFineSamplingPencilBeamEngine.isAvailable(testData.pln,[])); + + + + \ No newline at end of file diff --git a/test/doseCalc/test_HongPB.m b/test/doseCalc/test_HongPB.m new file mode 100644 index 000000000..40f5300b4 --- /dev/null +++ b/test/doseCalc/test_HongPB.m @@ -0,0 +1,102 @@ +function test_suite = test_HongPB + + test_functions=localfunctions(); + + initTestSuite; + + function test_getHongPBEngineFromPln + % Single gaussian lateral model + testData.pln = struct('radiationMode','protons','machine','Generic'); + testData.pln.propDoseCalc.engine = 'HongPB'; + engine = DoseEngines.matRad_ParticleHongPencilBeamEngine.getEngineFromPln(testData.pln); + assertTrue(isa(engine,'DoseEngines.matRad_ParticleHongPencilBeamEngine')); + + function test_loadMachineForHongPB + possibleRadModes = DoseEngines.matRad_ParticleHongPencilBeamEngine.possibleRadiationModes; + for i = 1:numel(possibleRadModes) + machine = DoseEngines.matRad_ParticleHongPencilBeamEngine.loadMachine(possibleRadModes{i},'Generic'); + assertTrue(isstruct(machine)); + assertTrue(isfield(machine, 'meta')); + assertTrue(isfield(machine.meta, 'radiationMode')); + assertTrue(strcmp(machine.meta.radiationMode, possibleRadModes{i})); + end + + function test_calcDoseHongPBprotons + testData = load('protons_testData.mat'); + + assertTrue(DoseEngines.matRad_ParticleHongPencilBeamEngine.isAvailable(testData.pln)); + + testData.pln.propDoseCalc.engine = 'HongPB'; + testData.pln.propDoseCalc.dosimetricLateralCutOff = 0.995; + testData.pln.propDoseCalc.geometricLateralCutOff = 50; + resultGUI = matRad_calcDoseForward(testData.ct, testData.cst, testData.stf, testData.pln, ones(sum([testData.stf(:).totalNumOfBixels]),1)); + + assertTrue(isequal(fieldnames(resultGUI),fieldnames(testData.resultGUI))); + assertTrue(isequal(testData.ct.cubeDim, size(resultGUI.physicalDose))); + assertElementsAlmostEqual(resultGUI.physicalDose,testData.resultGUI.physicalDose,'relative',1e-2,1e-2); + + function test_calcDoseHongPBhelium + testData = load('helium_testData.mat'); + assertTrue(DoseEngines.matRad_ParticleHongPencilBeamEngine.isAvailable(testData.pln)); + + testData.pln.propDoseCalc.engine = 'HongPB'; + testData.pln.propDoseCalc.dosimetricLateralCutOff = 0.995; + testData.pln.propDoseCalc.geometricLateralCutOff = 50; + resultGUI = matRad_calcDoseForward(testData.ct, testData.cst, testData.stf, testData.pln, ones(sum([testData.stf(:).totalNumOfBixels]),1)); + + assertTrue(isequal(fieldnames(resultGUI),fieldnames(testData.resultGUI))); + assertTrue(isequal(testData.ct.cubeDim, size(resultGUI.physicalDose))); + assertElementsAlmostEqual(resultGUI.physicalDose,testData.resultGUI.physicalDose,'relative',1e-2,1e-2); + + function test_calcDoseHongPBcarbon + testData = load('carbon_testData.mat'); + assertTrue(DoseEngines.matRad_ParticleHongPencilBeamEngine.isAvailable(testData.pln)); + + testData.pln.propDoseCalc.engine = 'HongPB'; + testData.pln.propDoseCalc.dosimetricLateralCutOff = 0.995; + testData.pln.propDoseCalc.geometricLateralCutOff = 50; + resultGUI = matRad_calcDoseForward(testData.ct, testData.cst, testData.stf, testData.pln, ones(sum([testData.stf(:).totalNumOfBixels]),1)); + + assertTrue(isequal(fieldnames(resultGUI),fieldnames(testData.resultGUI))); + assertTrue(isequal(testData.ct.cubeDim, size(resultGUI.physicalDose))); + assertElementsAlmostEqual(resultGUI.physicalDose,testData.resultGUI.physicalDose,'relative',1e-2,1e-2); + + function test_calcDoseHongPBVHEE + testData = load('VHEE_testData.mat'); + assertTrue(DoseEngines.matRad_ParticleHongPencilBeamEngine.isAvailable(testData.pln)); + testData.pln.propDoseCalc.engine = 'HongPB'; + testData.pln.propDoseCalc.dosimetricLateralCutOff = 0.995; + testData.pln.propDoseCalc.geometricLateralCutOff = 50; + resultGUI = matRad_calcDoseForward(testData.ct, testData.cst, testData.stf, testData.pln, ones(sum([testData.stf(:).totalNumOfBixels]),1)); + + assertTrue(isequal(fieldnames(resultGUI),fieldnames(testData.resultGUI))); + assertTrue(isequal(testData.ct.cubeDim, size(resultGUI.physicalDose))); + assertElementsAlmostEqual(resultGUI.physicalDose,testData.resultGUI.physicalDose,'relative',1e-2,1e-2); + + function test_calcDoseHongPBVHEE_Focused + testData = load('VHEE_testData_Focused.mat'); + assertTrue(DoseEngines.matRad_ParticleHongPencilBeamEngine.isAvailable(testData.pln)); + testData.pln.propDoseCalc.engine = 'HongPB'; + testData.pln.propDoseCalc.dosimetricLateralCutOff = 0.995; + testData.pln.propDoseCalc.geometricLateralCutOff = 50; + resultGUI = matRad_calcDoseForward(testData.ct, testData.cst, testData.stf, testData.pln, ones(sum([testData.stf(:).totalNumOfBixels]),1)); + + assertTrue(isequal(fieldnames(resultGUI),fieldnames(testData.resultGUI))); + assertTrue(isequal(testData.ct.cubeDim, size(resultGUI.physicalDose))); + assertElementsAlmostEqual(resultGUI.physicalDose,testData.resultGUI.physicalDose,'relative',1e-2,1e-2); + + function test_nonSupportedSettings + % Radiation mode other than protons not implemented + testData = load('photons_testData.mat'); + testData.pln.propDoseCalc.engine = 'HongPB'; + assertFalse(DoseEngines.matRad_ParticleHongPencilBeamEngine.isAvailable(testData.pln)); + + % Invalid machine without radiation mode field + testData.pln.machine = 'Empty'; + testData.pln.propDoseCalc.engine = 'HongPB'; + assertExceptionThrown(@() DoseEngines.matRad_ParticleHongPencilBeamEngine.isAvailable(testData.pln)); + assertFalse(DoseEngines.matRad_ParticleHongPencilBeamEngine.isAvailable(testData.pln,[])); + + + + \ No newline at end of file diff --git a/test/doseCalc/test_SVDPB.m b/test/doseCalc/test_SVDPB.m new file mode 100644 index 000000000..c06d1eed9 --- /dev/null +++ b/test/doseCalc/test_SVDPB.m @@ -0,0 +1,58 @@ +function test_suite = test_SVDPB + + test_functions=localfunctions(); + + initTestSuite; + + function test_getSVDPBEngineFromPln + % Single gaussian lateral model + testData.pln = struct('radiationMode','photons','machine','Generic'); + testData.pln.propDoseCalc.engine = 'SVDPB'; + engine = DoseEngines.matRad_PhotonPencilBeamSVDEngine.getEngineFromPln(testData.pln); + assertTrue(isa(engine,'DoseEngines.matRad_PhotonPencilBeamSVDEngine')); + + function test_loadMachineForSVDPB + possibleRadModes = DoseEngines.matRad_PhotonPencilBeamSVDEngine.possibleRadiationModes; + for i = 1:numel(possibleRadModes) + machine = DoseEngines.matRad_PhotonPencilBeamSVDEngine.loadMachine(possibleRadModes{i},'Generic'); + assertTrue(isstruct(machine)); + assertTrue(isfield(machine, 'meta')); + assertTrue(isfield(machine.meta, 'radiationMode')); + assertTrue(strcmp(machine.meta.radiationMode, possibleRadModes{i})); + end + + function test_calcDoseSVDPBphotons + testData = load('photons_testData.mat'); + + assertTrue(DoseEngines.matRad_PhotonPencilBeamSVDEngine.isAvailable(testData.pln)); + + testData.pln.propDoseCalc.engine = 'SVDPB'; + testData.pln.propDoseCalc.dosimetricLateralCutOff = 0.995; + testData.pln.propDoseCalc.geometricLateralCutOff = 50; + testData.pln.propDoseCalc.kernelCutOff = Inf; + if moxunit_util_platform_is_octave() + %The random number generator is not consistent between octave and matlab + testData.pln.propDoseCalc.enableDijSampling = false; + end + resultGUI = matRad_calcDoseForward(testData.ct, testData.cst, testData.stf, testData.pln, ones(sum([testData.stf(:).totalNumOfBixels]),1)); + + assertTrue(isequal(fieldnames(resultGUI),fieldnames(testData.resultGUI))); + assertTrue(isequal(testData.ct.cubeDim, size(resultGUI.physicalDose))); + assertElementsAlmostEqual(resultGUI.physicalDose,testData.resultGUI.physicalDose,'relative',1e-2,1e-2); + + + function test_nonSupportedSettings + % Radiation mode other than photons not implemented + testData = load('protons_testData.mat'); + testData.pln.propDoseCalc.engine = 'SVDPB'; + assertFalse(DoseEngines.matRad_PhotonPencilBeamSVDEngine.isAvailable(testData.pln)); + + % Invalid machine without radiation mode field + testData.pln.machine = 'Empty'; + testData.pln.propDoseCalc.engine = 'SVDPB'; + assertExceptionThrown(@() DoseEngines.matRad_PhotonPencilBeamSVDEngine.isAvailable(testData.pln)); + assertFalse(DoseEngines.matRad_PhotonPencilBeamSVDEngine.isAvailable(testData.pln,[])); + + + + diff --git a/test/doseCalc/test_TopasMCEngine.m b/test/doseCalc/test_TopasMCEngine.m index bee074bd4..361463771 100644 --- a/test/doseCalc/test_TopasMCEngine.m +++ b/test/doseCalc/test_TopasMCEngine.m @@ -7,7 +7,8 @@ function test_loadMachine radModes = DoseEngines.matRad_TopasMCEngine.possibleRadiationModes; for i = 1:numel(radModes) - machine = DoseEngines.matRad_TopasMCEngine.loadMachine(radModes{i},'Generic'); + machineName = 'Generic'; + machine = DoseEngines.matRad_TopasMCEngine.loadMachine(radModes{i},machineName); assertTrue(isstruct(machine)); end assertExceptionThrown(@() DoseEngines.matRad_TopasMCEngine.loadMachine('grbl','grbl'),'matRad:Error') @@ -15,7 +16,8 @@ function test_getEngineFromPlnByName radModes = DoseEngines.matRad_TopasMCEngine.possibleRadiationModes; for i = 1:numel(radModes) - plnDummy = struct('radiationMode',radModes{i},'machine','Generic','propDoseCalc',struct('engine','TOPAS')); + machineName = 'Generic'; + plnDummy = struct('radiationMode',radModes{i},'machine',machineName,'propDoseCalc',struct('engine','TOPAS')); engine = DoseEngines.matRad_TopasMCEngine.getEngineFromPln(plnDummy); assertTrue(isa(engine,'DoseEngines.matRad_TopasMCEngine')); end @@ -35,30 +37,78 @@ end for i = 1:numel(radModes) - if ~strcmp(radModes{i},'photons') - load([radModes{i} '_testData.mat']); - pln.propDoseCalc.engine = 'TOPAS'; - pln.propDoseCalc.externalCalculation = 'write'; - pln.propDoseCalc.numHistoriesDirect = 1e6; - pln.bioModel = matRad_bioModel(radModes{i},'none'); - resultGUI = matRad_calcDoseForward(ct,cst,stf,pln, ones(1,sum([stf(:).totalNumOfBixels]))); - - elseif strcmp(radModes{i},'photons') - load([radModes{i} '_testData.mat']); + load([radModes{i} '_testData.mat']); + pln.bioModel = matRad_bioModel(radModes{i},'none'); + + w = ones(1,sum([stf(:).totalNumOfBixels])); + + if strcmp(radModes{i},'photons') pln.propOpt.runSequencing = 1; pln.propOpt.runDAO = 1; - pln.bioModel = matRad_bioModel(radModes{i}, 'none'); 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.propDoseCalc.engine = 'TOPAS'; - pln.propDoseCalc.externalCalculation = 'write'; + w = resultGUI.w; pln.propDoseCalc.beamProfile = 'phasespace'; - pln.propDoseCalc.numHistoriesDirect = 1e6; - resultGUI = matRad_calcDoseForward(ct,cst,stf,pln, resultGUI.w ); end + + pln.propDoseCalc.engine = 'TOPAS'; + pln.propDoseCalc.externalCalculation = 'write'; + pln.propDoseCalc.numHistoriesDirect = 1e6; + resultGUI = matRad_calcDoseForward(ct,cst,stf,pln, w); + + folderName = [matRad_cfg.primaryUserFolder filesep 'TOPAS' filesep]; + folderName = [folderName stf(1).radiationMode,'_',stf(1).machine,'_',datestr(now, 'dd-mm-yy')]; + %check of outputfolder exists + assertTrue(isfolder(folderName)); + %check if file in folder existi + assertTrue(isfile([folderName filesep 'matRad_cube.dat'])); + assertTrue(isfile([folderName filesep 'matRad_cube.txt'])); + assertTrue(isfile([folderName filesep 'MCparam.mat'])); + for j = 1:pln.propStf.numOfBeams + assertTrue(isfile([folderName filesep 'beamSetup_matRad_plan_field' num2str(j) '.txt'])); + assertTrue(isfile([folderName filesep 'matRad_plan_field' num2str(j) '_run1.txt'])); + end + rmdir(folderName,'s'); %clean up +end + + +function test_TopasMCdoseCalcBasicRBE +% test if all the necessary output files are written vor a couple of cases. +% i am not using the default number of histories for testing her, insted 1e6. +% Because the files are just writen and not simulated so we dont care about simulation time. +% To few histories may result in wierd behavior in the topas interface, i.e if a beam +% recieves no histories because there are not enough to be distributed accros the spots, +% it causes an error +radModes = DoseEngines.matRad_TopasMCEngine.possibleRadiationModes; +matRad_cfg = MatRad_Config.instance(); + +if moxunit_util_platform_is_octave + confirm_recursive_rmdir(false,'local'); +end + +for i = 1:numel(radModes) + switch radModes{i} + case 'protons' + RBEmodel = {'mcn', 'wed'}; + case {'helium', 'carbon'} + RBEmodel ={'libamtrack','lem'}; + otherwise + continue; + end + matRad_cfg = MatRad_Config.instance(); + load([radModes{i} '_testData.mat']); + + pln.propDoseCalc.engine = 'TOPAS'; + pln.propDoseCalc.externalCalculation = 'write'; + pln.propDoseCalc.numHistoriesDirect = 1e6; + pln.propDoseCalc.scorer.RBE = true; + pln.propDoseCalc.scorer.RBE_model = RBEmodel; + pln.bioModel = matRad_bioModel(radModes{i},'none'); + resultGUI = matRad_calcDoseForward(ct,cst,stf,pln, ones(1,sum([stf(:).totalNumOfBixels]))); + folderName = [matRad_cfg.primaryUserFolder filesep 'TOPAS' filesep]; folderName = [folderName stf(1).radiationMode,'_',stf(1).machine,'_',datestr(now, 'dd-mm-yy')]; %check of outputfolder exists @@ -84,32 +134,29 @@ end for i = 1:numel(radModes) - if ~strcmp(radModes{i},'photons') - load([radModes{i} '_testData.mat']); - pln.propDoseCalc.engine = 'TOPAS'; - pln.propDoseCalc.externalCalculation = 'write'; - pln.propDoseCalc.numHistoriesDirect = 1e6; - pln.propDoseCalc.numOfRuns = numOfRuns; - pln.bioModel = matRad_bioModel(radModes{i},'none'); - resultGUI = matRad_calcDoseForward(ct,cst,stf,pln, ones(1,sum([stf(:).totalNumOfBixels]))); - - elseif strcmp(radModes{i},'photons') - load([radModes{i} '_testData.mat']); + load([radModes{i} '_testData.mat']); + pln.bioModel = matRad_bioModel(radModes{i},'none'); + w = ones(1,sum([stf(:).totalNumOfBixels])); + + if strcmp(radModes{i},'photons') pln.propOpt.runSequencing = 1; pln.propOpt.runDAO = 1; - pln.bioModel = matRad_bioModel(radModes{i},'none'); 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.propDoseCalc.engine = 'TOPAS'; - pln.propDoseCalc.externalCalculation = 'write'; pln.propDoseCalc.beamProfile = 'phasespace'; - pln.propDoseCalc.numHistoriesDirect = 1e6; - pln.propDoseCalc.numOfRuns = numOfRuns; - resultGUI = matRad_calcDoseForward(ct,cst,stf,pln, resultGUI.w ); + w = resultGUI.w; end + + pln.propDoseCalc.engine = 'TOPAS'; + pln.propDoseCalc.externalCalculation = 'write'; + pln.propDoseCalc.numHistoriesDirect = 1e6; + pln.propDoseCalc.numOfRuns = numOfRuns; + + resultGUI = matRad_calcDoseForward(ct,cst,stf,pln, w); + folderName = [matRad_cfg.primaryUserFolder filesep 'TOPAS' filesep]; folderName = [folderName stf(1).radiationMode,'_',stf(1).machine,'_',datestr(now, 'dd-mm-yy')]; %check of outputfolder exists @@ -129,7 +176,90 @@ end +function test_TopasMCdoseCalc4D +numOfPhases = 5; +radModes = DoseEngines.matRad_TopasMCEngine.possibleRadiationModes; +matRad_cfg = MatRad_Config.instance(); + +if moxunit_util_platform_is_octave + confirm_recursive_rmdir(false,'local'); +end +% physical Dose +for i = 1:numel(radModes) + if ~strcmp(radModes{i},'photons') + 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'); + pln.propDoseCalc.engine = 'TOPAS'; + pln.propDoseCalc.externalCalculation = 'write'; + pln.propDoseCalc.calc4DInterplay = true; + pln.propDoseCalc.calcTimeSequence = timeSequence; + pln.propDoseCalc.numHistoriesDirect = 1e6; + resultGUI = matRad_calcDoseForward(ct,cst,stf,pln, resultGUI.w); + + folderName = [matRad_cfg.primaryUserFolder filesep 'TOPAS' filesep]; + folderName = [folderName stf(1).radiationMode,'_',stf(1).machine,'_',datestr(now, 'dd-mm-yy')]; + %check of outputfolder exists + assertTrue(isfolder(folderName)); + %check if file in folder existi + assertTrue(isfile([folderName filesep 'MCparam.mat'])); + for j = 1:pln.propStf.numOfBeams + assertTrue(isfile([folderName filesep 'beamSetup_matRad_plan_field' num2str(j) '.txt'])); + assertTrue(isfile([folderName filesep 'matRad_plan_field' num2str(j) '_run1.txt'])); + assertTrue(isfile([folderName filesep 'matRad_cube_field' num2str(j) '.txt'])); + for k = 1:numOfPhases + assertTrue(isfile([folderName filesep 'matRad_cube' num2str(k) '.dat'])); + end + end + rmdir(folderName,'s'); %clean up + end +end +%RBExDose +for i = 1:numel(radModes) + switch radModes{i} + case 'protons' + RBEmodel = {'mcn', 'wed'}; + case {'helium', 'carbon'} + RBEmodel ={'libamtrack','lem'}; + otherwise + continue; + end + + 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'); + pln.propDoseCalc.engine = 'TOPAS'; + pln.propDoseCalc.externalCalculation = 'write'; + pln.propDoseCalc.calc4DInterplay = true; + pln.propDoseCalc.calcTimeSequence = timeSequence; + pln.propDoseCalc.numHistoriesDirect = 1e6; + pln.propDoseCalc.scorer.RBE = true; + pln.propDoseCalc.scorer.RBE_model = RBEmodel; + resultGUI = matRad_calcDoseForward(ct,cst,stf,pln, resultGUI.w); + + folderName = [matRad_cfg.primaryUserFolder filesep 'TOPAS' filesep]; + folderName = [folderName stf(1).radiationMode,'_',stf(1).machine,'_',datestr(now, 'dd-mm-yy')]; + %check of outputfolder exists + assertTrue(isfolder(folderName)); + %check if file in folder existi + assertTrue(isfile([folderName filesep 'MCparam.mat'])); + for j = 1:pln.propStf.numOfBeams + assertTrue(isfile([folderName filesep 'beamSetup_matRad_plan_field' num2str(j) '.txt'])); + assertTrue(isfile([folderName filesep 'matRad_plan_field' num2str(j) '_run1.txt'])); + assertTrue(isfile([folderName filesep 'matRad_cube_field' num2str(j) '.txt'])); + for k = 1:numOfPhases + assertTrue(isfile([folderName filesep 'matRad_cube' num2str(k) '.dat'])); + end + end + rmdir(folderName,'s'); %clean up +end diff --git a/test/doseCalc/test_baseEngine.m b/test/doseCalc/test_baseEngine.m index 6280f3949..c1777bb64 100644 --- a/test/doseCalc/test_baseEngine.m +++ b/test/doseCalc/test_baseEngine.m @@ -57,6 +57,14 @@ engine = DoseEngines.matRad_DoseEngineBase.getEngineFromPln(heliumDummyPln); assertTrue(isa(engine,'DoseEngines.matRad_ParticleHongPencilBeamEngine')); + vheeDummyPln = struct('radiationMode','VHEE','machine','Generic'); + engine = DoseEngines.matRad_DoseEngineBase.getEngineFromPln(vheeDummyPln); + assertTrue(isa(engine,'DoseEngines.matRad_ParticleHongPencilBeamEngine')); + + vheeDummyPlnFocused = struct('radiationMode','VHEE','machine','Focused'); + engine = DoseEngines.matRad_DoseEngineBase.getEngineFromPln(vheeDummyPlnFocused); + assertTrue(isa(engine,'DoseEngines.matRad_ParticleHongPencilBeamEngine')); + function test_getEngineFromPlnByName protonDummyPln = struct('radiationMode','protons','machine','Generic','propDoseCalc',struct('engine','MCsquare')); engine = DoseEngines.matRad_DoseEngineBase.getEngineFromPln(protonDummyPln); diff --git a/test/gui/test_gui_DVHStatsWidget.m b/test/gui/test_gui_DVHStatsWidget.m index 0d1024de7..2a9cbc111 100644 --- a/test/gui/test_gui_DVHStatsWidget.m +++ b/test/gui/test_gui_DVHStatsWidget.m @@ -40,4 +40,61 @@ delete(h); close(get(p,'Parent')); +function test_DVHStatsWidget_constructWithPhotonPln + evalin('base','load photons_testData.mat'); + h = matRad_DVHStatsWidget(); + h.selectedDisplayOption = 'physicalDose'; + try + assertTrue(isa(h, 'matRad_DVHStatsWidget')); + assertTrue(isa(h, 'matRad_Widget')); + catch ME + evalin('base','clear ct cst pln stf dij resultGUI'); + delete(h); + rethrow(ME); + end + evalin('base','clear ct cst pln stf dij resultGUI'); + delete(h); + +function test_DVHStatsWidget_constructWithProtonPln + evalin('base','load protons_testData.mat'); + h = matRad_DVHStatsWidget(); + try + assertTrue(isa(h, 'matRad_DVHStatsWidget')); + assertTrue(isa(h, 'matRad_Widget')); + catch ME + evalin('base','clear ct cst pln stf dij resultGUI'); + delete(h); + rethrow(ME); + end + evalin('base','clear ct cst pln stf dij resultGUI'); + delete(h); + +function test_DVHStatsWidget_constructWithCarbonPln + evalin('base','load carbon_testData.mat'); + h = matRad_DVHStatsWidget(); + try + assertTrue(isa(h, 'matRad_DVHStatsWidget')); + assertTrue(isa(h, 'matRad_Widget')); + catch ME + evalin('base','clear ct cst pln stf dij resultGUI'); + delete(h); + rethrow(ME); + end + evalin('base','clear ct cst pln stf dij resultGUI'); + delete(h); + +function test_DVHStatsWidget_constructWithHeliumPln + evalin('base','load helium_testData.mat'); + h = matRad_DVHStatsWidget(); + try + assertTrue(isa(h, 'matRad_DVHStatsWidget')); + assertTrue(isa(h, 'matRad_Widget')); + catch ME + evalin('base','clear ct cst pln stf dij resultGUI'); + delete(h); + rethrow(ME); + end + evalin('base','clear ct cst pln stf dij resultGUI'); + delete(h); + %TODO: Test Buttons / visibility depending on data \ No newline at end of file diff --git a/test/gui/test_gui_PlanWidget.m b/test/gui/test_gui_PlanWidget.m index 483aa8b86..ec0fd963e 100644 --- a/test/gui/test_gui_PlanWidget.m +++ b/test/gui/test_gui_PlanWidget.m @@ -40,7 +40,7 @@ delete(h); close(get(p,'Parent')); -function test_PlanWidget_constructWithData +function test_PlanWidget_constructWithTG119 evalin('base','load TG119.mat'); h = matRad_PlanWidget(); try @@ -52,6 +52,107 @@ rethrow(ME); end evalin('base','clear ct cst pln'); - delete(h); + delete(h); + +function test_PlanWidget_constructWithPhotonPln + evalin('base','load photons_testData.mat'); + h = matRad_PlanWidget(); + try + assertTrue(isa(h, 'matRad_PlanWidget')); + assertTrue(isa(h, 'matRad_Widget')); + catch ME + evalin('base','clear ct cst pln stf dij resultGUI'); + delete(h); + rethrow(ME); + end + evalin('base','clear ct cst pln stf dij resultGUI'); + delete(h); + +function test_PlanWidget_constructWithProtonPln + evalin('base','load protons_testData.mat'); + h = matRad_PlanWidget(); + try + assertTrue(isa(h, 'matRad_PlanWidget')); + assertTrue(isa(h, 'matRad_Widget')); + catch ME + evalin('base','clear ct cst pln stf dij resultGUI'); + delete(h); + rethrow(ME); + end + evalin('base','clear ct cst pln stf dij resultGUI'); + delete(h); + +function test_PlanWidget_constructWithCarbonPln + evalin('base','load carbon_testData.mat'); + h = matRad_PlanWidget(); + try + assertTrue(isa(h, 'matRad_PlanWidget')); + assertTrue(isa(h, 'matRad_Widget')); + catch ME + evalin('base','clear ct cst pln stf dij resultGUI'); + delete(h); + rethrow(ME); + end + evalin('base','clear ct cst pln stf dij resultGUI'); + delete(h); + +function test_PlanWidget_constructWithHeliumPln + evalin('base','load helium_testData.mat'); + h = matRad_PlanWidget(); + try + assertTrue(isa(h, 'matRad_PlanWidget')); + assertTrue(isa(h, 'matRad_Widget')); + catch ME + evalin('base','clear ct cst pln stf dij resultGUI'); + delete(h); + rethrow(ME); + end + evalin('base','clear ct cst pln stf dij resultGUI'); + delete(h); + +function test_PlanWidget_multiisocenter + evalin('base','load protons_testData.mat'); + + %Modify to have multiple isocenters + pln = evalin('base','pln'); + pln.propStf.isoCenter(2,:) = [0 0 0]; + iso = pln.propStf.isoCenter; + assignin('base','pln',pln); + + h = matRad_PlanWidget(); + + % check correct value in isocenter edit field + str = get(h.handles.editIsoCenter,'String'); + assertEqual(str,'multiple isoCenter'); + + %Now force an update by changing a value and executing the callback + set(h.handles.editBixelWidth,'String','1'); + cb = get(h.handles.editBixelWidth,'Callback'); + cb(h.handles.editBixelWidth,[]); + + str = get(h.handles.editIsoCenter,'String'); + assertEqual(str,'multiple isoCenter'); + pln = evalin('base','pln'); + assertEqual(pln.propStf.isoCenter,iso); + + evalin('base','clear ct cst pln stf dij resultGUI'); + delete(h); + +function test_PlanWidget_tissuetable + evalin('base','load carbon_testData.mat'); + + %Modify to have multiple isocenters + h = matRad_PlanWidget(); + + cb = get(h.handles.btnSetTissue,'Callback'); + cb(h.handles.btnSetTissue,[]); + + figHandles = get(0,'Children'); + assertTrue(strcmp(get(figHandles,'Name'),'Set Tissue Parameters')); + + close(figHandles); + delete(h); + + -%TODO: Test Buttons / visibility depending on data \ No newline at end of file +%TODO: Test Buttons \ No newline at end of file diff --git a/test/gui/test_gui_exportWidget.m b/test/gui/test_gui_exportWidget.m index fb39d2a47..4042973d5 100644 --- a/test/gui/test_gui_exportWidget.m +++ b/test/gui/test_gui_exportWidget.m @@ -55,4 +55,18 @@ evalin('base','clear ct cst pln'); delete(h); +function test_exportWidget_constructWithCarbonPln + evalin('base','load carbon_testData.mat'); + h = matRad_exportWidget(); + try + assertTrue(isa(h, 'matRad_exportWidget')); + assertTrue(isa(h, 'matRad_Widget')); + catch ME + evalin('base','clear ct cst pln stf dij resultGUI'); + delete(h); + rethrow(ME); + end + evalin('base','clear ct cst pln stf dij resultGUI'); + delete(h); + %TODO: Test Buttons / visibility depending on data \ No newline at end of file diff --git a/test/gui/test_gui_viewingWidget.m b/test/gui/test_gui_viewingWidget.m index 91a33bd72..db112f800 100644 --- a/test/gui/test_gui_viewingWidget.m +++ b/test/gui/test_gui_viewingWidget.m @@ -40,7 +40,7 @@ delete(h); close(get(p,'Parent')); -function test_viewingWidget_constructWithData +function test_viewingWidget_constructWithTG119 evalin('base','load TG119.mat'); h = matRad_ViewingWidget(); try @@ -53,4 +53,60 @@ evalin('base','clear ct cst pln'); delete(h); +function test_ViewingWidget_constructWithPhotonPln + evalin('base','load photons_testData.mat'); + h = matRad_ViewingWidget(); + try + assertTrue(isa(h, 'matRad_ViewingWidget')); + assertTrue(isa(h, 'matRad_Widget')); + catch ME + evalin('base','clear ct cst pln stf dij resultGUI'); + delete(h); + rethrow(ME); + end + evalin('base','clear ct cst pln stf dij resultGUI'); + delete(h); + +function test_ViewingWidget_constructWithProtonPln + evalin('base','load protons_testData.mat'); + h = matRad_ViewingWidget(); + try + assertTrue(isa(h, 'matRad_ViewingWidget')); + assertTrue(isa(h, 'matRad_Widget')); + catch ME + evalin('base','clear ct cst pln stf dij resultGUI'); + delete(h); + rethrow(ME); + end + evalin('base','clear ct cst pln stf dij resultGUI'); + delete(h); + +function test_ViewingWidget_constructWithCarbonPln + evalin('base','load carbon_testData.mat'); + h = matRad_ViewingWidget(); + try + assertTrue(isa(h, 'matRad_ViewingWidget')); + assertTrue(isa(h, 'matRad_Widget')); + catch ME + evalin('base','clear ct cst pln stf dij resultGUI'); + delete(h); + rethrow(ME); + end + evalin('base','clear ct cst pln stf dij resultGUI'); + delete(h); + +function test_ViewingWidget_constructWithHeliumPln + evalin('base','load helium_testData.mat'); + h = matRad_ViewingWidget(); + try + assertTrue(isa(h, 'matRad_ViewingWidget')); + assertTrue(isa(h, 'matRad_Widget')); + catch ME + evalin('base','clear ct cst pln stf dij resultGUI'); + delete(h); + rethrow(ME); + end + evalin('base','clear ct cst pln stf dij resultGUI'); + delete(h); + %TODO: Test Buttons / visibility depending on data \ No newline at end of file diff --git a/test/scenarios/test_importanceScenarios.m b/test/scenarios/test_importanceScenarios.m index 120563f06..bc7e4713f 100644 --- a/test/scenarios/test_importanceScenarios.m +++ b/test/scenarios/test_importanceScenarios.m @@ -143,47 +143,58 @@ end - function test_importanceScenarioCombineRange +function test_importanceScenarioCombineRange - model = matRad_ImportanceScenarios(); - - assertExceptionThrown(@() helper_assignmentTest(model,'combineRange','hello'),'matRad:Error'); - assertTrue(model.combineRange); - - nRangeScen = model.totNumRangeScen; - - model.combineRange = false; - assertFalse(model.combineRange); - assertEqual(model.totNumRangeScen,nRangeScen^2); - assertEqual(model.totNumScen,model.totNumRangeScen - 1 + model.totNumShiftScen); - - function test_importanceScenarioShiftCombinations - - model = matRad_ImportanceScenarios(); - - assertExceptionThrown(@() helper_assignmentTest(model,'combinations','hello'),'matRad:Error'); - assertEqual(model.combinations,'none'); - - nSetupPoints = model.numOfSetupGridPoints; - nRangePoints = model.numOfRangeGridPoints; + model = matRad_ImportanceScenarios(); - model.combinations = 'shift'; - assertEqual(model.combinations,'shift'); - assertEqual(model.totNumShiftScen,nSetupPoints^3); - assertEqual(model.totNumScen,model.totNumRangeScen - 1 + model.totNumShiftScen); + assertExceptionThrown(@() helper_assignmentTest(model,'combineRange','hello'),'matRad:Error'); + assertTrue(model.combineRange); - model.combinations = 'all'; - assertEqual(model.combinations,'all'); - assertEqual(model.totNumShiftScen,nSetupPoints^3); - assertEqual(model.totNumRangeScen,nRangePoints); - assertEqual(model.totNumScen,model.totNumRangeScen * model.totNumShiftScen); - - model.combineRange = false; - assertEqual(model.totNumShiftScen,nSetupPoints^3); - assertEqual(model.totNumRangeScen,nRangePoints^2); - assertEqual(model.totNumScen,model.totNumRangeScen * model.totNumShiftScen); + nRangeScen = model.totNumRangeScen; + + model.combineRange = false; + assertFalse(model.combineRange); + assertEqual(model.totNumRangeScen,nRangeScen^2); + assertEqual(model.totNumScen,model.totNumRangeScen - 1 + model.totNumShiftScen); - model.combinations = 'shift'; - assertEqual(model.totNumShiftScen,nSetupPoints^3); - assertEqual(model.totNumRangeScen,nRangePoints^2); - assertEqual(model.totNumScen,model.totNumRangeScen + model.totNumShiftScen - 1); \ No newline at end of file +function test_importanceScenarioShiftCombinations + + model = matRad_ImportanceScenarios(); + + assertExceptionThrown(@() helper_assignmentTest(model,'combinations','hello'),'matRad:Error'); + assertEqual(model.combinations,'none'); + + nSetupPoints = model.numOfSetupGridPoints; + nRangePoints = model.numOfRangeGridPoints; + + model.combinations = 'shift'; + assertEqual(model.combinations,'shift'); + assertEqual(model.totNumShiftScen,nSetupPoints^3); + assertEqual(model.totNumScen,model.totNumRangeScen - 1 + model.totNumShiftScen); + + model.combinations = 'all'; + assertEqual(model.combinations,'all'); + assertEqual(model.totNumShiftScen,nSetupPoints^3); + assertEqual(model.totNumRangeScen,nRangePoints); + assertEqual(model.totNumScen,model.totNumRangeScen * model.totNumShiftScen); + + model.combineRange = false; + assertEqual(model.totNumShiftScen,nSetupPoints^3); + assertEqual(model.totNumRangeScen,nRangePoints^2); + assertEqual(model.totNumScen,model.totNumRangeScen * model.totNumShiftScen); + + model.combinations = 'shift'; + assertEqual(model.totNumShiftScen,nSetupPoints^3); + assertEqual(model.totNumRangeScen,nRangePoints^2); + assertEqual(model.totNumScen,model.totNumRangeScen + model.totNumShiftScen - 1); + +function test_importanceScenariosPropertySetters + model = matRad_ImportanceScenarios(); + + model.numOfSetupGridPoints = 5; + assertEqual(model.numOfSetupGridPoints,5); + + model.numOfRangeGridPoints = 5; + assertEqual(model.numOfRangeGridPoints,5); + + \ No newline at end of file diff --git a/test/steering/test_stfGeneratorVHEE.m b/test/steering/test_stfGeneratorVHEE.m new file mode 100644 index 000000000..3ff61b713 --- /dev/null +++ b/test/steering/test_stfGeneratorVHEE.m @@ -0,0 +1,100 @@ +function test_suite = test_stfGeneratorVHEE + + test_functions=localfunctions(); + + initTestSuite; + + function test_basic_construct() + stfGen = matRad_StfGeneratorParticleVHEE(); + assertTrue(isa(stfGen, 'matRad_StfGeneratorParticleVHEE')); + + function test_pln_construct() + load VHEE_testData.mat + pln.propStf.energy = 150; + stfGen = matRad_StfGeneratorParticleVHEE(pln); + stfGen.isAvailable(pln); + assertTrue(isa(stfGen, 'matRad_StfGeneratorParticleVHEE')); + assertEqual(stfGen.gantryAngles, pln.propStf.gantryAngles); + assertEqual(stfGen.couchAngles, pln.propStf.couchAngles); + assertEqual(stfGen.isoCenter, pln.propStf.isoCenter); + assertEqual(stfGen.radiationMode, pln.radiationMode); + assertEqual(stfGen.machine, pln.machine); + assertEqual(stfGen.bixelWidth, pln.propStf.bixelWidth); + assertEqual(stfGen.energy, pln.propStf.energy); + + function test_pln_construct_focused() + load VHEE_testData.mat + pln.machine = 'Focused'; + pln.propStf.energy = 150; + stfGen = matRad_StfGeneratorParticleVHEE(pln); + stfGen.isAvailable(pln); + assertTrue(isa(stfGen, 'matRad_StfGeneratorParticleVHEE')); + assertEqual(stfGen.gantryAngles, pln.propStf.gantryAngles); + assertEqual(stfGen.couchAngles, pln.propStf.couchAngles); + assertEqual(stfGen.isoCenter, pln.propStf.isoCenter); + assertEqual(stfGen.radiationMode, pln.radiationMode); + assertEqual(stfGen.machine, pln.machine); + assertEqual(stfGen.bixelWidth, pln.propStf.bixelWidth); + assertEqual(stfGen.energy, pln.propStf.energy); + + function test_generate_multibeams() + % geometry settings + load VHEE_testData.mat ct cst pln stf; + + stfGen = matRad_StfGeneratorParticleVHEE(pln); + stf2 = stfGen.generate(ct,cst); + + assertTrue(isfield(stf2, 'radiationMode')); + assertTrue(isfield(stf2, 'machine')); + assertTrue(isfield(stf2, 'gantryAngle')); + assertTrue(isfield(stf2, 'couchAngle')); + assertTrue(isfield(stf2, 'isoCenter')); + assertTrue(isfield(stf2, 'bixelWidth')); + assertTrue(isfield(stf2, 'SAD')); + assertTrue(isfield(stf2, 'numOfRays')); + assertTrue(isfield(stf2, 'numOfBixelsPerRay')); + assertTrue(isfield(stf2, 'totalNumOfBixels')); + assertTrue(isfield(stf2, 'sourcePoint')); + assertTrue(isfield(stf2, 'sourcePoint_bev')); + assertTrue(isfield(stf2, 'ray')); + + for i = 1:numel(stf2) + + assertEqual(stf2(i).totalNumOfBixels,stf(i).totalNumOfBixels); + assertEqual(stf2(i).numOfBixelsPerRay,stf(i).numOfBixelsPerRay); + assertEqual(stf2(i).numOfRays,stf(i).numOfRays); + assertEqual(stf2(i).bixelWidth,stfGen.bixelWidth); + assertEqual(stf2(i).radiationMode,stfGen.radiationMode); + assertEqual(stf2(i).machine,pln.machine); + assertEqual(stf2(i).gantryAngle,stfGen.gantryAngles(i)); + assertEqual(stf2(i).couchAngle,stfGen.couchAngles(i)); + assertTrue(all([stf2(i).ray.energy] == stfGen.energy)); + + rotMat = matRad_getRotationMatrix(stf2(i).gantryAngle,stf2(i).couchAngle); + assertEqual(stf2(i).sourcePoint,stf2(i).sourcePoint_bev*rotMat); + assertEqual(stf2(i).sourcePoint_bev,[0 -stf2(i).SAD 0]); + + assertTrue(isstruct(stf2(i).ray)); + assertEqual(numel(stf2(i).ray),numel(stf(i).ray)); + assertEqual(numel(stf2(i).ray),stf2(i).numOfRays); + + rayPosTest = vertcat(stf2(i).ray.rayPos); + rayPosTest_bev = rayPosTest*rotMat; + rayPos_bevTest = vertcat(stf2(i).ray.rayPos_bev); + rayPosRef = vertcat(stf(i).ray.rayPos); + assertElementsAlmostEqual(sort(rayPosTest,1),sort(rayPosRef,1)); + assertElementsAlmostEqual(sort(rayPos_bevTest,1),sort(rayPosTest_bev,1)); + + targetPointTest = vertcat(stf2(i).ray.targetPoint); + targetPointRef = vertcat(stf(i).ray.targetPoint); + targetPoint_bevTest = vertcat(stf2(i).ray.targetPoint_bev); + targetPoint_bevRef = vertcat(stf(i).ray.targetPoint_bev); + assertElementsAlmostEqual(sort(targetPointTest,1),sort(targetPointRef,1)); + assertElementsAlmostEqual(sort(targetPoint_bevTest,1),sort(targetPoint_bevRef,1)); + assertElementsAlmostEqual(sort(targetPoint_bevTest,1),sort(targetPointTest*rotMat,1)); + + energiesTest = [stf2(i).ray.energy]; + energiesRef = [stf(i).ray.energy]; + assertEqual(unique(energiesTest),unique(energiesRef)); + + end diff --git a/test/testData/FRED_data/MCrun/fred.inp b/test/testData/FRED_data/MCrun/fred.inp new file mode 100644 index 000000000..1d94acd24 --- /dev/null +++ b/test/testData/FRED_data/MCrun/fred.inp @@ -0,0 +1,2 @@ +include: inp/regions/regions.inp +include: inp/plan/planDelivery.inp diff --git a/test/testData/FRED_data/MCrun/inp/plan/plan.inp b/test/testData/FRED_data/MCrun/inp/plan/plan.inp new file mode 100644 index 000000000..d726263ca --- /dev/null +++ b/test/testData/FRED_data/MCrun/inp/plan/plan.inp @@ -0,0 +1,30 @@ +nprim = 83333 +#Bixels Field0, Layer0 + def: S0 = {'beamletID': 0, 'P': [-0.900, -0.900, 0.000], 'v': [-0.00100, -0.00100, 1.00000], 'w': 1000000.0000000} + def: S1 = {'beamletID': 1, 'P': [0.900, -0.900, 0.000], 'v': [0.00100, -0.00100, 1.00000], 'w': 1000000.0000000} + def: S2 = {'beamletID': 2, 'P': [0.000, 0.000, 0.000], 'v': [0.00000, 0.00000, 1.00000], 'w': 1000000.0000000} + def: S3 = {'beamletID': 3, 'P': [-0.900, 0.900, 0.000], 'v': [-0.00100, 0.00100, 1.00000], 'w': 1000000.0000000} + def: S4 = {'beamletID': 4, 'P': [0.900, 0.900, 0.000], 'v': [0.00100, 0.00100, 1.00000], 'w': 1000000.0000000} + def: L0 = {'Energy': 106.0783, 'Espread': 3.7329, 'FWHM': 1.4576, 'beamlets': [S0, S1, S2, S3, S4]} + +#Bixels Field0, Layer1 + def: S5 = {'beamletID': 5, 'P': [0.000, 0.000, 0.000], 'v': [0.00000, 0.00000, 1.00000], 'w': 1000000.0000000} + def: L1 = {'Energy': 112.3394, 'Espread': 3.5049, 'FWHM': 1.3972, 'beamlets': [S5]} + +def: F0 = {'fieldNumber': 0, 'GA': 0, 'CA': 0, 'ISO': [0, 0, 0], 'dim': [15.4758, 15.4758, 0.1], 'Layers': [L0, L1]} + +#Bixels Field1, Layer2 + def: S6 = {'beamletID': 6, 'P': [-0.900, -0.900, 0.000], 'v': [-0.00100, -0.00100, 1.00000], 'w': 1000000.0000000} + def: S7 = {'beamletID': 7, 'P': [0.900, -0.900, 0.000], 'v': [0.00100, -0.00100, 1.00000], 'w': 1000000.0000000} + def: S8 = {'beamletID': 8, 'P': [0.000, 0.000, 0.000], 'v': [0.00000, 0.00000, 1.00000], 'w': 1000000.0000000} + def: S9 = {'beamletID': 9, 'P': [-0.900, 0.900, 0.000], 'v': [-0.00100, 0.00100, 1.00000], 'w': 1000000.0000000} + def: S10 = {'beamletID': 10, 'P': [0.900, 0.900, 0.000], 'v': [0.00100, 0.00100, 1.00000], 'w': 1000000.0000000} + def: L2 = {'Energy': 106.0783, 'Espread': 3.7329, 'FWHM': 1.4576, 'beamlets': [S6, S7, S8, S9, S10]} + +#Bixels Field1, Layer3 + def: S11 = {'beamletID': 11, 'P': [0.000, 0.000, 0.000], 'v': [0.00000, 0.00000, 1.00000], 'w': 1000000.0000000} + def: L3 = {'Energy': 112.3394, 'Espread': 3.5049, 'FWHM': 1.3972, 'beamlets': [S11]} + +def: F1 = {'fieldNumber': 1, 'GA': 180, 'CA': 0, 'ISO': [0, 0, 0], 'dim': [15.4758, 15.4758, 0.1], 'Layers': [L2, L3]} + +def: plan = {'SAD': 100, 'Fields': [F0, F1]} diff --git a/test/testData/FRED_data/MCrun/inp/plan/planDelivery.inp b/test/testData/FRED_data/MCrun/inp/plan/planDelivery.inp new file mode 100644 index 000000000..781cb4756 --- /dev/null +++ b/test/testData/FRED_data/MCrun/inp/plan/planDelivery.inp @@ -0,0 +1,76 @@ +#Include file defining fields and layers geometry +include: inp/plan/plan.inp + +#Define the fields +for(currField in plan.get('Fields'))< + field< + ID = ${currField.get('fieldNumber')} + O = [0,${plan.get('SAD')},0] + L = ${currField.get('dim')} + pivot = [0.5,0.5,0.5] + l = [0, 0, -1] + u = [1, 0 ,0] + field> + + #Deactivate the fields to avoid geometrical overlap + deactivate: field_${currField.get('fieldNumber')} +for> + +for(currField in plan.get('Fields'))< + + def: fieldIdx = currField.get('fieldNumber') + + #Activate current field + activate: field_$fieldIdx + + #Collect Gantry and Couch angles + def: GA = currField.get('GA') + def: CA = currField.get('CA') + + #Collect Isocenter + def: ISO = currField.get('ISO') + + #First move the patient so that the Isocenter is now in the center of the Room coordinate system + transform: Phantom move_to ${ISO.item(0)} ${ISO.item(1)} ${ISO.item(2)} Room + + #Second rotate the patient according to the gantry and couch angles. + #In this configuration the fileds are always fixed in +SAD in y direction and the patient is rotated accordingly + transform: Phantom rotate y ${CA} Room + transform: Phantom rotate z ${GA} Room + + for(layer in currField.get('Layers'))< + + #Recover parameters of the current energy layer + def: currEnergy = layer.get('Energy') + def: currEspread = layer.get('Espread') + def: currFWHM = layer.get('FWHM') + + for(beamlet in layer.get('beamlets'))< + pb< + ID = ${beamlet.get('beamletID')} + fieldID = $fieldIdx + particle = proton + T = $currEnergy + EFWHM = $currEspread + Xsec = gauss + FWHM = $currFWHM + + P = ${beamlet.get('P')} + v = ${beamlet.get('v')} + N = ${beamlet.get('w')} + pb> + for> + for> + + #Deliver all the pecil beams in this field + deliver: field_$fieldIdx + + #Deactivate the current field + deactivate: field_$fieldIdx + + #Restore the patient to original position + transform: Phantom rotate z ${-1*GA} Room + transform: Phantom rotate y ${-1*CA} Room + transform: Phantom move_to 0 0 0 Room +for> + diff --git a/test/testData/FRED_data/MCrun/inp/regions/CTpatient.mhd b/test/testData/FRED_data/MCrun/inp/regions/CTpatient.mhd new file mode 100644 index 000000000..2e8f27ea6 --- /dev/null +++ b/test/testData/FRED_data/MCrun/inp/regions/CTpatient.mhd @@ -0,0 +1,11 @@ +ObjectType = Image +NDims = 3 +BinaryData = True +BinaryDataByteOrderMSB = False +TransformMatrix = 1 0 0 0 1 0 0 0 1 +Offset = 0.000000 0.000000 0.000000 +AnatomicalOrientation = RAI +ElementSpacing = 10.000000 10.000000 10.000000 +DimSize = 10 20 10 +ElementType = MET_SHORT +ElementDataFile = CTpatient.raw diff --git a/test/testData/FRED_data/MCrun/inp/regions/CTpatient.raw b/test/testData/FRED_data/MCrun/inp/regions/CTpatient.raw new file mode 100644 index 000000000..847594b9e Binary files /dev/null and b/test/testData/FRED_data/MCrun/inp/regions/CTpatient.raw differ diff --git a/test/testData/FRED_data/MCrun/inp/regions/hLut.inp b/test/testData/FRED_data/MCrun/inp/regions/hLut.inp new file mode 100644 index 000000000..597e7f654 --- /dev/null +++ b/test/testData/FRED_data/MCrun/inp/regions/hLut.inp @@ -0,0 +1,9 @@ +matColumns: HU rho RSP Ipot Lrad C Ca H N O P Ti S +mat: -1024 0.001 0.001 78.0 36.1 0 0 11.189400 0 88.810600 0 0 0 +mat: -999 0.001 0.0011 78.0 36.1 0 0 11.189400 0 88.810600 0 0 0 +mat: -90 0.95 0.95 78.0 36.1 0 0 11.189400 0 88.810600 0 0 0 +mat: -45 0.99 0.99 78.0 36.1 0 0 11.189400 0 88.810600 0 0 0 +mat: 0 1 1 78.0 36.1 0 0 11.189400 0 88.810600 0 0 0 +mat: 100 1.095 1.095 78.0 36.1 0 0 11.189400 0 88.810600 0 0 0 +mat: 350 1.199 1.199 78.0 36.1 0 0 11.189400 0 88.810600 0 0 0 +mat: 3000 2.505 2.505 78.0 36.1 0 0 11.189400 0 88.810600 0 0 0 \ No newline at end of file diff --git a/test/testData/FRED_data/MCrun/inp/regions/regions.inp b/test/testData/FRED_data/MCrun/inp/regions/regions.inp new file mode 100644 index 000000000..ba495c73d --- /dev/null +++ b/test/testData/FRED_data/MCrun/inp/regions/regions.inp @@ -0,0 +1,14 @@ +region< + ID=Phantom + CTscan=inp/regions/CTpatient.mhd + O=[0,0,0] + pivot=[0.5,0.5,0.5] + l=[1.0,0.0,0.0] + u=[0.0,-1.0,0.0] + score=[Dose] +region> +region< + ID=Room + material=Air +region> +include: inp/regions/hLut.inp diff --git a/test/testData/FRED_data/MCrun/out/score/Phantom.Dose.mhd b/test/testData/FRED_data/MCrun/out/score/Phantom.Dose.mhd new file mode 100644 index 000000000..6a58ff0b5 Binary files /dev/null and b/test/testData/FRED_data/MCrun/out/score/Phantom.Dose.mhd differ diff --git a/test/testData/FRED_data/MCrun/out/score/Phantom.LETd.mhd b/test/testData/FRED_data/MCrun/out/score/Phantom.LETd.mhd new file mode 100644 index 000000000..6d103b257 Binary files /dev/null and b/test/testData/FRED_data/MCrun/out/score/Phantom.LETd.mhd differ diff --git a/test/testData/FRED_data/MCrun/out/scoreij/Phantom.Dose.bin b/test/testData/FRED_data/MCrun/out/scoreij/Phantom.Dose.bin new file mode 100644 index 000000000..23a525c81 Binary files /dev/null and b/test/testData/FRED_data/MCrun/out/scoreij/Phantom.Dose.bin differ diff --git a/test/testData/FRED_data/MCrun/out/scoreij/Phantom.LETd.bin b/test/testData/FRED_data/MCrun/out/scoreij/Phantom.LETd.bin new file mode 100644 index 000000000..b3ca89c51 Binary files /dev/null and b/test/testData/FRED_data/MCrun/out/scoreij/Phantom.LETd.bin differ diff --git a/test/testData/Fred.sparseDij.bin b/test/testData/Fred.sparseDij.bin new file mode 100644 index 000000000..46a6a22ec Binary files /dev/null and b/test/testData/Fred.sparseDij.bin differ diff --git a/test/testData/VHEE_testData.mat b/test/testData/VHEE_testData.mat new file mode 100644 index 000000000..9d4aeebfb Binary files /dev/null and b/test/testData/VHEE_testData.mat differ diff --git a/test/testData/VHEE_testData_Focused.mat b/test/testData/VHEE_testData_Focused.mat new file mode 100644 index 000000000..4c2f01f5d Binary files /dev/null and b/test/testData/VHEE_testData_Focused.mat differ diff --git a/test/testData/carbon_testData.mat b/test/testData/carbon_testData.mat index 097b54828..f9224109f 100644 Binary files a/test/testData/carbon_testData.mat and b/test/testData/carbon_testData.mat differ diff --git a/test/testData/helium_testData.mat b/test/testData/helium_testData.mat index 3f3aba6c6..8d70fa274 100644 Binary files a/test/testData/helium_testData.mat and b/test/testData/helium_testData.mat differ diff --git a/test/testData/helper_testDataCreater.m b/test/testData/helper_testDataCreater.m index d42979bd3..daec8339e 100644 --- a/test/testData/helper_testDataCreater.m +++ b/test/testData/helper_testDataCreater.m @@ -2,7 +2,6 @@ % therapy, that can be red in and used in testing %% create ct - ct = struct(); ct.cubeDim = [20,10,10]; ct.resolution.x = 10; @@ -40,29 +39,32 @@ clear VolHelper ixBody ixTarget i %% create pln, stf -radMode = 'carbon'; %protons,helium,carbon; - -pln.radiationMode = radMode; -pln.machine = 'Generic'; -pln.numOfFractions = 30; -pln.propStf.gantryAngles = [0,180]; -pln.propStf.couchAngles = zeros(size(pln.propStf.gantryAngles)); -pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); -pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0); - -pln.propStf.longitudinalSpotSpacing = 8; -pln.propStf.bixelWidth = 10; -pln.propDoseCalc.doseGrid.resolution = struct('x',10,'y',10,'z',10); %[mm] - -%pln.bioModel = matRad_bioModel(pln.radiationMode,'none'); - -%% Generate Beam Geometry STF -pln.propStf.addMargin = false; %to make smaller stf, les bixel -stf = matRad_generateStf(ct,cst,pln); -ct = matRad_calcWaterEqD(ct, pln); -%% Dose Calculation -%dij = matRad_calcDoseInfluence(ct,cst,stf,pln); -%resultGUI = matRad_calcCubes(ones(dij.totalNumOfBixels,1),dij); - -%% save basic data -save([radMode '_testData.mat'],'ct','cst','pln','stf','-v7') \ No newline at end of file +radModes = ["protons","helium","carbon","VHEE"]; +for radMode = radModes + %radMode = 'carbon'; %protons,helium,carbon; + + pln.radiationMode = char(radMode); + pln.machine = 'Generic'; + pln.numOfFractions = 30; + pln.propStf.gantryAngles = [0,180]; + pln.propStf.couchAngles = zeros(size(pln.propStf.gantryAngles)); + pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); + pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0); + + pln.propStf.longitudinalSpotSpacing = 8; + pln.propStf.bixelWidth = 10; + pln.propDoseCalc.doseGrid.resolution = struct('x',10,'y',10,'z',10); %[mm] + + %pln.bioModel = matRad_bioModel(pln.radiationMode,'none'); + + %% Generate Beam Geometry STF + pln.propStf.addMargin = false; %to make smaller stf, les bixel + stf = matRad_generateStf(ct,cst,pln); + ct = matRad_calcWaterEqD(ct, pln); + %% Dose Calculation + dij = matRad_calcDoseInfluence(ct,cst,stf,pln); + resultGUI = matRad_calcCubes(ones(dij.totalNumOfBixels,1),dij); + + %% save basic data + save([char(radMode) '_testData.mat'],'ct','cst','pln','stf','dij','resultGUI','-v7'); +end \ No newline at end of file diff --git a/test/testData/protons_testData.mat b/test/testData/protons_testData.mat index d845f082d..65f170ae4 100644 Binary files a/test/testData/protons_testData.mat and b/test/testData/protons_testData.mat differ diff --git a/test/util/test_plotSlice.m b/test/util/test_plotSlice.m new file mode 100644 index 000000000..9aa1e82dc --- /dev/null +++ b/test/util/test_plotSlice.m @@ -0,0 +1,115 @@ +function test_suite = test_plotSlice + +test_functions=localfunctions(); + +initTestSuite; + +function test_plot_ct_only + + load BOXPHANTOM.mat + [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSlice(ct); + assertTrue(isempty(hCMap)); + assertTrue(isempty(hDose)); + assertFalse(isempty(hCt)); + if ~moxunit_util_platform_is_octave + assertTrue(isa(hCt, 'matlab.graphics.primitive.Image')) + end + assertTrue(isempty(hContour)); + assertTrue(isempty(hIsoDose)); + close(gcf); + + load PROSTATE.mat + [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSlice(ct, 'slice', 91, 'cst', cst); + assertTrue(isempty(hCMap)); + assertTrue(isempty(hDose)); + assertFalse(isempty(hCt)); + if ~moxunit_util_platform_is_octave + assertTrue(isa(hCt, 'matlab.graphics.primitive.Image')) + end + assertTrue(isa(hContour, 'cell')); + assertTrue(isempty(hIsoDose)); + close(gcf); + +function test_plot_dose_slice + + load protons_testData.mat + [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSlice(ct, 'dose', resultGUI.physicalDose); + assertFalse(isempty(hCt)); + assertTrue(isempty(hContour)); + assertTrue(isempty(hIsoDose)); + if ~moxunit_util_platform_is_octave + assertTrue(isa(hCMap, 'matlab.graphics.illustration.ColorBar')); + assertTrue(isa(hDose, 'matlab.graphics.primitive.Image')); + assertTrue(isa(hCt, 'matlab.graphics.primitive.Image')) + end + close(gcf); + + load helium_testData.mat + [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSlice(ct, 'dose', resultGUI.physicalDose); + assertFalse(isempty(hCt)); + assertTrue(isempty(hContour)); + assertTrue(isempty(hIsoDose)); + if ~moxunit_util_platform_is_octave + assertTrue(isa(hCMap, 'matlab.graphics.illustration.ColorBar')); + assertTrue(isa(hDose, 'matlab.graphics.primitive.Image')); + assertTrue(isa(hCt, 'matlab.graphics.primitive.Image')) + end + close(gcf); + + load carbon_testData.mat + [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSlice(ct, 'dose', resultGUI.physicalDose); + assertFalse(isempty(hCt)); + assertTrue(isempty(hContour)); + assertTrue(isempty(hIsoDose)); + if ~moxunit_util_platform_is_octave + assertTrue(isa(hCMap, 'matlab.graphics.illustration.ColorBar')); + assertTrue(isa(hDose, 'matlab.graphics.primitive.Image')); + assertTrue(isa(hCt, 'matlab.graphics.primitive.Image')) + end + close(gcf); + + load photons_testData.mat + [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSlice(ct, 'dose', resultGUI.physicalDose, 'plane', 3); + assertFalse(isempty(hCt)); + assertTrue(isempty(hContour)); + assertTrue(isempty(hIsoDose)); + if ~moxunit_util_platform_is_octave + assertTrue(isa(hCMap, 'matlab.graphics.illustration.ColorBar')); + assertTrue(isa(hDose, 'matlab.graphics.primitive.Image')); + assertTrue(isa(hCt, 'matlab.graphics.primitive.Image')) + end + close(gcf); + +function test_optional_input + + load photons_testData.mat + hF = figure(); + doseCube = resultGUI.physicalDose; + boolVOIselection = ones(1, size(cst, 1)); + [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSlice(ct, ... + 'dose', doseCube, 'axesHandle', gca, ... + 'cst', cst, 'cubeIdx', 1, 'plane', 3, 'slice', 5, ... + 'thresh', 0.1*max(doseCube(:)), 'alpha', 0.8, ... + 'contourColorMap', white, 'doseColorMap', jet, ... + 'doseWindow', [min(doseCube(:)) 1.1*max(doseCube(:))], ... + 'doseIsoLevels', max(doseCube(:)).*[0.5, 0.6, 0.7, 0.8, 0.9, 0.92, 0.95, 0.97, 0.99], ... + 'voiSelection', boolVOIselection, ... + 'colorBarLabel', 'Absorbed Dose [Gy]', ... + 'boolPlotLegend', 1, 'showCt', 0, 'FontSize', 13); + + assertTrue(isempty(hCt)); + assertTrue(isa(hContour, "cell")); + assertTrue(isa(hIsoDose, "cell")); + if ~moxunit_util_platform_is_octave + assertTrue(isa(hDose, 'matlab.graphics.primitive.Image')); + end + close(hF); + +function test_title_input + load photons_testData.mat + hF = figure(); + hA = axes(hF); + tString = 'Hello'; + matRad_plotSlice(ct,'title','Hello','axesHandle',hA); + assertTrue(isequal('Hello',get(get(hA,'Title'),'String'))); + close(hF);