From 0c7bfec6f2867bd9de14ea9e1dda78501e85401c Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Tue, 19 Nov 2024 13:19:57 +0100 Subject: [PATCH 01/87] Codecov Update and GitLab Pipeline for building matRad --- .github/workflows/test-results.yml | 7 +++- .gitlab-ci.yml | 53 ++++++++++++++++++++++++++++++ matRad_buildStandalone.m | 45 +++++++++++++------------ 3 files changed, 81 insertions(+), 24 deletions(-) create mode 100644 .gitlab-ci.yml diff --git a/.github/workflows/test-results.yml b/.github/workflows/test-results.yml index 8322c983a..4dcd505f9 100644 --- a/.github/workflows/test-results.yml +++ b/.github/workflows/test-results.yml @@ -36,5 +36,10 @@ jobs: with: files: | test-results/*/testresults.xml - + + - name: Publish Test Results on Codecov + uses: codecov/test-results-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: test-results/test-results/matlab-R2022b/coverage.xml,test-results/matlab-latest/coverage.xml,test-results/octave/coverage.xml \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 000000000..d4804e84a --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,53 @@ +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/matRad_buildStandalone.m b/matRad_buildStandalone.m index 6feebb3e2..70cf83189 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,... @@ -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 - - - - - - From c55fa648ad3b2c9f940a62833e1f3bed9a751c30 Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Tue, 19 Nov 2024 13:50:16 +0100 Subject: [PATCH 02/87] improve packaging on gitlab --- .gitlab-ci.yml | 4 +--- matRad_buildStandalone.m | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d4804e84a..5b7b37775 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -41,9 +41,7 @@ package: 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"' + - '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 diff --git a/matRad_buildStandalone.m b/matRad_buildStandalone.m index 70cf83189..55de5a97e 100644 --- a/matRad_buildStandalone.m +++ b/matRad_buildStandalone.m @@ -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.',... From d0ff2813d82a42eef0a4280264fbd3cdb1df5b7e Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Tue, 19 Nov 2024 16:48:27 +0100 Subject: [PATCH 03/87] small bugfix in DVH widget Signed-off-by: Niklas Wahl --- matRad/gui/widgets/matRad_DVHWidget.m | 13 +++++++++++-- matRad/planAnalysis/matRad_showDVH.m | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) 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/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 From 3f50eb7bc033c5458906b703bda3ec1863969806 Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Tue, 19 Nov 2024 16:49:13 +0100 Subject: [PATCH 04/87] extended GUI tests using the testData for photons, protons, helium and carbon Signed-off-by: Niklas Wahl --- test/gui/test_gui_DVHStatsWidget.m | 57 +++++++++++++++++++++++++++ test/gui/test_gui_PlanWidget.m | 63 ++++++++++++++++++++++++++++-- test/gui/test_gui_exportWidget.m | 14 +++++++ test/gui/test_gui_viewingWidget.m | 58 ++++++++++++++++++++++++++- 4 files changed, 188 insertions(+), 4 deletions(-) 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..d397da83d 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,63 @@ 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); + -%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 From 794336615d879bb6edf157e79872d90709eb230f Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Thu, 21 Nov 2024 14:10:09 +0100 Subject: [PATCH 05/87] fix filepaths in workflow github testresults to codecov Signed-off-by: Niklas Wahl --- .github/workflows/test-results.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-results.yml b/.github/workflows/test-results.yml index 4dcd505f9..8507868cf 100644 --- a/.github/workflows/test-results.yml +++ b/.github/workflows/test-results.yml @@ -41,5 +41,5 @@ jobs: uses: codecov/test-results-action@v1 with: token: ${{ secrets.CODECOV_TOKEN }} - files: test-results/test-results/matlab-R2022b/coverage.xml,test-results/matlab-latest/coverage.xml,test-results/octave/coverage.xml + files: test-results/matlab-R2022b/testresults.xml,test-results/matlab-latest/testresults.xml,test-results/octave/testresults.xml \ No newline at end of file From 2f04478aa5bf98bd6dbd5cdde98e9d23efb24837 Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Thu, 28 Nov 2024 17:28:11 +0100 Subject: [PATCH 06/87] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd1ac42ab..39c82e7c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## Development Changes +- Bugfix for DVH widget to not throw a warning in updates, handle scenarios correctly / more robustly and missing xlabel axesHandle parameter. + ## Version 3.1.0 - "Cleve" ### Major Changes and New Features From 2b21c92cf1e2e69d9436b3aadcabbb828edc8fcc Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Fri, 29 Nov 2024 14:46:23 +0100 Subject: [PATCH 07/87] Small DICOM Export / Import fixes (#799) * Writes ReferencedRTPlanSequence always when RTDose is exported. * Check for conditionally required ReferencedRTPlanSequence field during dose cube import * enable export and reimport of additional cubes (BED, LET, alpha, beta) * enable addition fields for dicom export (BED, LET, alpha, beta, RBE, effect) * fix selection bug that only one rt dose file could be selected * consistent usage of the importers "patients" property (always a cell) * update changelog --- CHANGELOG.md | 9 +++- .../matRad_exportDicomRTDoses.m | 50 +++++++++---------- .../matRad_DicomImporter.m | 3 +- .../matRad_importDicomRTDose.m | 19 +++++-- .../matRad_interpDicomDoseCube.m | 4 +- .../matRad_scanDicomImportFolder.m | 4 +- matRad/gui/widgets/matRad_importDicomWidget.m | 22 ++++---- 7 files changed, 66 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39c82e7c5..2c221a221 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,14 @@ # Changelog ## Development Changes -- Bugfix for DVH widget to not throw a warning in updates, handle scenarios correctly / more robustly and missing xlabel axesHandle parameter. + +### Bug Fixes +- 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. + ## Version 3.1.0 - "Cleve" 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_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/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 From 9503d6725fcbb06a2ca40aa9a5008cc076e6c368 Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Wed, 11 Dec 2024 13:22:20 +0100 Subject: [PATCH 08/87] Bugfix for Brachy Therapy Stf Generator (#801) * fix visualization of brachytherapy * fix for nifti writer only allowing 80 characters in description string --- examples/matRad_example15_brachy.m | 10 +--------- matRad/IO/matRad_writeNifTI.m | 3 +++ matRad/steering/matRad_StfGeneratorBrachy.m | 2 +- 3 files changed, 5 insertions(+), 10 deletions(-) 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/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/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 From 69f5ac3b10a2dbc18fa0e961fdc182c8911da754 Mon Sep 17 00:00:00 2001 From: Amit Bennan Date: Thu, 19 Dec 2024 13:02:08 +0100 Subject: [PATCH 09/87] Bug/803 window range (#805) * Fix for the issue with dispWindow update for cases: locksetting, change in cube, vieweroptions also fix for too many viewing widget updates for Plan widget changes * bug fix for iso dose line update failure. * small added fix for showing RBExDose by default --------- Co-authored-by: Niklas Wahl --- .../gui/widgets/matRad_ViewerOptionsWidget.m | 1 + matRad/gui/widgets/matRad_ViewingWidget.m | 76 ++++++++++--------- 2 files changed, 43 insertions(+), 34 deletions(-) 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..5743b21f1 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'},evt) this.updateIsoDoseLineCache(); + 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,27 @@ 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 end + + end \ No newline at end of file From 79966a323d44dc2b847185ae3027a73d45da94f0 Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Fri, 20 Dec 2024 00:14:50 +0100 Subject: [PATCH 10/87] also include external type contours in optimization --- .../@matRad_OptimizationProblem/matRad_constraintFunctions.m | 2 +- .../@matRad_OptimizationProblem/matRad_constraintJacobian.m | 2 +- .../@matRad_OptimizationProblem/matRad_getJacobianStructure.m | 2 +- .../@matRad_OptimizationProblem/matRad_objectiveFunction.m | 2 +- .../@matRad_OptimizationProblem/matRad_objectiveGradient.m | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) 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_getJacobianStructure.m b/matRad/optimization/@matRad_OptimizationProblem/matRad_getJacobianStructure.m index 28aec1666..02b44ce05 100644 --- a/matRad/optimization/@matRad_OptimizationProblem/matRad_getJacobianStructure.m +++ b/matRad/optimization/@matRad_OptimizationProblem/matRad_getJacobianStructure.m @@ -36,7 +36,7 @@ % 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}) 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}) From 6e516ae5183501f2178f62626de8d53dfdb20422 Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Fri, 20 Dec 2024 00:15:15 +0100 Subject: [PATCH 11/87] change interpolation method in dose calculation --- .../+DoseEngines/matRad_ParticlePencilBeamEngineAbstract.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matRad/doseCalc/+DoseEngines/matRad_ParticlePencilBeamEngineAbstract.m b/matRad/doseCalc/+DoseEngines/matRad_ParticlePencilBeamEngineAbstract.m index a4d2530c9..cd0a00d78 100644 --- a/matRad/doseCalc/+DoseEngines/matRad_ParticlePencilBeamEngineAbstract.m +++ b/matRad/doseCalc/+DoseEngines/matRad_ParticlePencilBeamEngineAbstract.m @@ -247,7 +247,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 From b3649852a0f41a3d5fa33adea0f149d7b40fe975 Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Wed, 8 Jan 2025 14:34:59 +0100 Subject: [PATCH 12/87] fix interpolation in LDR 2D computation in Brachytherapy Dose Engine --- .../+DoseEngines/matRad_TG43BrachyEngine.m | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) 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 From c2a3af7c6362813c36550ad27cc7adac729ab8b8 Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Fri, 10 Jan 2025 11:16:26 +0100 Subject: [PATCH 13/87] remove out-of-place options check in OptimizerSimulannealbnd --- .../optimization/optimizer/matRad_OptimizerSimulannealbnd.m | 5 ----- 1 file changed, 5 deletions(-) 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); From 1d6ecc4c6f03e08b23df8fad0929b1f501866291 Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Fri, 10 Jan 2025 11:18:57 +0100 Subject: [PATCH 14/87] add global optimization toolbox to test environment to test the simulated annealing optimization --- .github/actions/test-matlab/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 47995ddb02570c91e15c88562fc6d28b6ec99bcd Mon Sep 17 00:00:00 2001 From: Amit Bennan Date: Fri, 10 Jan 2025 11:20:58 +0100 Subject: [PATCH 15/87] Bug fix for resultGUI field updates with saved results in the GUI (#809) --- matRad/gui/widgets/matRad_WorkflowWidget.m | 23 +++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/matRad/gui/widgets/matRad_WorkflowWidget.m b/matRad/gui/widgets/matRad_WorkflowWidget.m index 9fa2e3489..e48fb7697 100644 --- a/matRad/gui/widgets/matRad_WorkflowWidget.m +++ b/matRad/gui/widgets/matRad_WorkflowWidget.m @@ -20,6 +20,7 @@ % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% properties + savedResultTag = {}; end methods @@ -465,22 +466,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 +728,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'); From cc75426f7cbd844e20e7ac0c7b2b97ed5b1e0d8f Mon Sep 17 00:00:00 2001 From: Amit Bennan Date: Mon, 20 Jan 2025 17:02:16 +0100 Subject: [PATCH 16/87] bugfix for gantry angle dependant couch angle autofill edit --- matRad/gui/widgets/matRad_PlanWidget.m | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/matRad/gui/widgets/matRad_PlanWidget.m b/matRad/gui/widgets/matRad_PlanWidget.m index 493415abc..e8909212d 100644 --- a/matRad/gui/widgets/matRad_PlanWidget.m +++ b/matRad/gui/widgets/matRad_PlanWidget.m @@ -958,17 +958,20 @@ function updatePlnInWorkspace(this,hObject,evtData) 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 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 +% 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); % [°] +% end end pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); From fb4eb9641c7f7b7812b69af671d777f57eb28364 Mon Sep 17 00:00:00 2001 From: Amit Bennan Date: Wed, 22 Jan 2025 17:18:54 +0100 Subject: [PATCH 17/87] bug fix for beam angle visualization and update with gantry and couch angle changes --- matRad/gui/widgets/matRad_PlanWidget.m | 43 ++++++++++++------- matRad/gui/widgets/matRad_ViewingWidget.m | 4 +- .../gui/widgets/matRad_VisualizationWidget.m | 2 +- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/matRad/gui/widgets/matRad_PlanWidget.m b/matRad/gui/widgets/matRad_PlanWidget.m index e8909212d..6b5b7715f 100644 --- a/matRad/gui/widgets/matRad_PlanWidget.m +++ b/matRad/gui/widgets/matRad_PlanWidget.m @@ -28,6 +28,7 @@ hTissueWindow; currentMachine; + plotPlan = false; end properties (Constant) @@ -946,32 +947,39 @@ 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))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); - + 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); % [°] + + if ~isempty(hObject) && (strcmp(hObject.Tag,'editGantryAngle')||strcmp(hObject.Tag,'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 -% 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); % [°] -% end + this.plotPlan = true; end pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles); @@ -1081,6 +1089,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 diff --git a/matRad/gui/widgets/matRad_ViewingWidget.m b/matRad/gui/widgets/matRad_ViewingWidget.m index 5743b21f1..6be1c3491 100644 --- a/matRad/gui/widgets/matRad_ViewingWidget.m +++ b/matRad/gui/widgets/matRad_ViewingWidget.m @@ -517,10 +517,10 @@ function notifyPlotUpdated(obj) end this.updateValues(); %doUpdate = this.checkUpdateNecessary({'pln_display','ct','cst','resultGUI','image_display'},evt); - if this.checkUpdateNecessary({'resultGUI','image_display','viewer_options'},evt) + if this.checkUpdateNecessary({'resultGUI','image_display','viewer_options','pln_angles'},evt) this.updateIsoDoseLineCache(); this.UpdatePlot(); - end + end else this.updateValues(); this.UpdatePlot(); 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,... From af790f4c52b55b93679df34f8b855ad1d66bbbb6 Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Fri, 24 Jan 2025 10:41:43 +0100 Subject: [PATCH 18/87] uses persistent caching for available subclasses (stf generators, scenario models, bio models, dose engines) --- matRad/bioModels/matRad_BiologicalModel.m | 8 +++++++- .../@matRad_DoseEngineBase/getAvailableEngines.m | 8 +++++++- matRad/scenarios/matRad_ScenarioModel.m | 7 ++++++- matRad/steering/matRad_StfGeneratorBase.m | 8 +++++++- 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/matRad/bioModels/matRad_BiologicalModel.m b/matRad/bioModels/matRad_BiologicalModel.m index 0dda39af1..b770dd9b4 100644 --- a/matRad/bioModels/matRad_BiologicalModel.m +++ b/matRad/bioModels/matRad_BiologicalModel.m @@ -126,7 +126,13 @@ %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 + 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 diff --git a/matRad/doseCalc/+DoseEngines/@matRad_DoseEngineBase/getAvailableEngines.m b/matRad/doseCalc/+DoseEngines/@matRad_DoseEngineBase/getAvailableEngines.m index af42a7a51..445b5b79d 100644 --- a/matRad/doseCalc/+DoseEngines/@matRad_DoseEngineBase/getAvailableEngines.m +++ b/matRad/doseCalc/+DoseEngines/@matRad_DoseEngineBase/getAvailableEngines.m @@ -36,7 +36,13 @@ %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 +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/scenarios/matRad_ScenarioModel.m b/matRad/scenarios/matRad_ScenarioModel.m index 7c5542468..640ed52c4 100644 --- a/matRad/scenarios/matRad_ScenarioModel.m +++ b/matRad/scenarios/matRad_ScenarioModel.m @@ -232,7 +232,12 @@ 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 + 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..6dcc99165 100644 --- a/matRad/steering/matRad_StfGeneratorBase.m +++ b/matRad/steering/matRad_StfGeneratorBase.m @@ -439,7 +439,13 @@ 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 + 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 = []; From ab55cfa2c89599093761241ed1b5f40e809b8ca3 Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Fri, 24 Jan 2025 10:42:01 +0100 Subject: [PATCH 19/87] don't show warning when default stf generator is selected --- matRad/gui/widgets/matRad_PlanWidget.m | 2 +- matRad/steering/matRad_StfGeneratorBase.m | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/matRad/gui/widgets/matRad_PlanWidget.m b/matRad/gui/widgets/matRad_PlanWidget.m index 6b5b7715f..03cf3139e 100644 --- a/matRad/gui/widgets/matRad_PlanWidget.m +++ b/matRad/gui/widgets/matRad_PlanWidget.m @@ -804,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)); diff --git a/matRad/steering/matRad_StfGeneratorBase.m b/matRad/steering/matRad_StfGeneratorBase.m index 6dcc99165..1f48daf88 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); From 77e9c0c017f0ea78c627a3fbb1b9cf1fafa4e011 Mon Sep 17 00:00:00 2001 From: Amit Bennan Date: Fri, 24 Jan 2025 15:13:19 +0100 Subject: [PATCH 20/87] bug fix for BED quantity opt extended for all modalities --- matRad/matRad_fluenceOptimization.m | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/matRad/matRad_fluenceOptimization.m b/matRad/matRad_fluenceOptimization.m index ecb213102..735324c82 100644 --- a/matRad/matRad_fluenceOptimization.m +++ b/matRad/matRad_fluenceOptimization.m @@ -281,12 +281,16 @@ wInit = ((doseTarget)/(TolEstBio*maxCurrRBE*max(doseTmp(V))))* wOnes; elseif strcmp(pln.propOpt.quantityOpt, 'BED') - - 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); + meanBED = mean((aTmp(V) + bTmp(V).^2)./cst{ixTarget,5}.alphaX); + BEDTarget = doseTarget.*(1 + doseTarget./abr); + +% 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) +% +% 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)); @@ -295,7 +299,7 @@ % 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 +% end bixelWeight = BEDTarget/meanBED; wInit = wOnes * bixelWeight; From ffe6a512696a355d6b3129822442371f415244f2 Mon Sep 17 00:00:00 2001 From: Amit Bennan Date: Fri, 24 Jan 2025 17:05:54 +0100 Subject: [PATCH 21/87] incomplete fix for conformal 3d --- matRad/gui/widgets/matRad_WorkflowWidget.m | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/matRad/gui/widgets/matRad_WorkflowWidget.m b/matRad/gui/widgets/matRad_WorkflowWidget.m index e48fb7697..7cdd9fbd4 100644 --- a/matRad/gui/widgets/matRad_WorkflowWidget.m +++ b/matRad/gui/widgets/matRad_WorkflowWidget.m @@ -301,7 +301,7 @@ function btnLoadMat_Callback(this, hObject, event) end % check if dij exist - if evalin('base','exist(''dij'')') && plnStfMatch + if evalin('base','exist(''dij'')') && plnStfMatch && ~evalin('base','pln.propOpt.conf3D') [dijStfMatch, msg] = matRad_compareDijStf(evalin('base','dij'),evalin('base','stf')); if dijStfMatch set(handles.txtInfo,'String','ready for optimization'); @@ -414,7 +414,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,'conf3D') && pln.propOpt.conf3D + dij = matRad_collapseDij(dij); + end % assign results to base worksapce assignin('base','dij',dij); From 975b9b29a4503a00da44d829cbbb9197f6b8d10f Mon Sep 17 00:00:00 2001 From: s742o Date: Sat, 25 Jan 2025 01:11:45 +0100 Subject: [PATCH 22/87] New function Plot Slice and Bug fixed in Iso Dose lines plot function The new function Plot Slice allows to plot a single dose slice in a flexible way: ct and dose cube are the only required parameters, but other optional, e.g. colorbar, legend, iso dose lines, line and text properties, can be specified. The bug fixing ensures that a 2D quantity is correctly called within the function. --- matRad/plotting/matRad_plotIsoDoseLines.m | 6 +- matRad/util/matRad_plotSlice.m | 205 ++++++++++++++++++++++ 2 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 matRad/util/matRad_plotSlice.m 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/util/matRad_plotSlice.m b/matRad/util/matRad_plotSlice.m new file mode 100644 index 000000000..ba06f0214 --- /dev/null +++ b/matRad/util/matRad_plotSlice.m @@ -0,0 +1,205 @@ +function [] = matRad_plotSlice(ct, dose, 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 +% dose dose cube +% +% input (optional/empty) to be called as Name-value pair arguments: +% 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 +% varargin Additional MATLAB Line or Text Properties (e.g. 'LineWidth', 'FontSize', etc.) +% +% +% References +% - +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Copyright 2015 the matRad development team. +% +% This file is part of the matRad project. It is subject to the license +% terms in the LICENSE file found in the top-level directory of this +% distribution and at https://github.com/e0404/matRad/LICENSE.md. No part +% of the matRad project, including this file, may be copied, modified, +% propagated, or distributed except according to the terms contained in the +% LICENSE file. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +defaultCst = []; +defaultSlice = floor(min(size(dose))./2); +defaultAxesHandle = gca; +defaultCubeIdx = 1; +defaultPlane = 1; +defaultDoseWindow = []; +defaultThresh = []; +defaultAlpha = []; +defaultDoseColorMap = []; +defaultDoseIsoLevels = []; +defaultVOIselection = []; +defaultContourColorMap = []; +defaultBoolPlotLegend = false; +defaultColorBarLabel = []; + +isSlice = @(x) x>=1 && x<=max(size(dose)) && floor(x)==x; +isAxes = @(x) strcmp(get(gca, 'type'), 'axes'); +isCubeIdx = @(x) isscalar(x); +isPlane = @(x) isscalar(x) && (sum(x==[1, 2, 3])==1); +isDoseWindow = @(x) (length(x) == 2 && isvector(x)); +isThresh = @(x) isscalar(x) && (x>=0) && (x<=1); +isAlpha = @(x) isscalar(x) && (x>=0) && (x<=1); +isDoseColorMap = @(x) isnumeric(x) && (size(x, 2)==3) && all(x(:) >= 0) && all(x(:) <= 1); +isDoseIsoLevels = @(x) isnumeric(x) && isvector(x); +isVOIselection = @(x) all(x(:)==1 | x(:)==0); +isContourColorMap = @(x) isnumeric(x) && (size(x, 2)==3) && size(x, 1)>=2 && all(x(:) >= 0) && all(x(:) <= 1); +isBoolPlotLegend = @(x) x==0 || x ==1; +isColorBarLabel = @(x) isstring(x) || ischar(x); + +p = inputParser; +p.KeepUnmatched = true; +addRequired(p, 'ct') +addRequired(p, 'dose') + +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) + +parse(p, ct, dose, varargin{:}); + +%% Unmatched properties +% General properties +lineFieldNames = fieldnames(set(line)); +textFieldNames = fieldnames(set(text)); +% 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 properites from Unmatched +textFields = unmParamNames(ismember(unmParamNames, textFieldNames)); +textValues = struct2cell(p.Unmatched); +textValues = textValues(ismember(unmParamNames, textFieldNames)); +textVarargin = reshape([textFields, textValues]', 1, []); + +%% Plot ct slice +matRad_cfg = MatRad_Config.instance(); + +% Flip axes direction +set(p.Results.axesHandle,'YDir','Reverse'); +% plot ct slice +hCt = matRad_plotCtSlice(p.Results.axesHandle,p.Results.ct.cubeHU,p.Results.cubeIdx,p.Results.plane,p.Results.slice, [], []); +hold on; + +%% Plot dose +if ~isempty(p.Results.doseWindow) && p.Results.doseWindow(2) - p.Results.doseWindow(1) <= 0 + p.Results.doseWindow = [0 2]; +end + +[hDose,doseColorMap,doseWindow] = matRad_plotDoseSlice(p.Results.axesHandle, p.Results.dose, p.Results.plane, p.Results.slice, p.Results.thresh, p.Results.alpha, p.Results.doseColorMap, p.Results.doseWindow); + +%% Plot iso dose lines +if ~isempty(p.Results.doseIsoLevels) + hIsoDose = matRad_plotIsoDoseLines(p.Results.axesHandle,p.Results.dose,[],p.Results.doseIsoLevels,false,p.Results.plane,p.Results.slice,p.Results.doseColorMap,p.Results.doseWindow, lineVarargin{:}); + hold on; +else + hIsoDose = []; +end + +%% Plot VOI contours & Legend + +if ~isempty(p.Results.cst) + [hContour,~] = matRad_plotVoiContourSlice(p.Results.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)); + ixLegend = find(p.Results.voiSelection); + hContourTmp = cellfun(@(X) X(1),hContour(visibleOnSlice),'UniformOutput',false); + if ~isempty(p.Results.voiSelection) + hLegend = legend(p.Results.axesHandle,[hContourTmp{:}],[p.Results.cst(ixLegend(visibleOnSlice),2)],'AutoUpdate','off','TextColor',matRad_cfg.gui.textColor); + else + hLegend = legend(p.Results.axesHandle,[hContourTmp{:}],[p.Results.cst(visibleOnSlice,2)],'AutoUpdate','off','TextColor',matRad_cfg.gui.textColor); + end + 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(p.Results.axesHandle,'tight'); +set(p.Results.axesHandle,'xtick',[],'ytick',[]); +colormap(p.Results.axesHandle,p.Results.doseColorMap); + +if isfield(p.Unmatched, 'FontSize') + matRad_plotAxisLabels(p.Results.axesHandle,p.Results.ct,p.Results.plane,p.Results.slice, p.Unmatched.FontSize, []) +else + matRad_plotAxisLabels(p.Results.axesHandle,p.Results.ct,p.Results.plane,p.Results.slice, [], []) +end + +% Set axis ratio. +ratios = [1/p.Results.ct.resolution.x 1/p.Results.ct.resolution.y 1/p.Results.ct.resolution.z]; + +set(p.Results.axesHandle,'DataAspectRatioMode','manual'); +if p.Results.plane == 1 + res = [ratios(3) ratios(2)]./max([ratios(3) ratios(2)]); + set(p.Results.axesHandle,'DataAspectRatio',[res 1]) +elseif p.Results.plane == 2 % sagittal plane + res = [ratios(3) ratios(1)]./max([ratios(3) ratios(1)]); + set(p.Results.axesHandle,'DataAspectRatio',[res 1]) +elseif p.Results.plane == 3 % Axial plane + res = [ratios(2) ratios(1)]./max([ratios(2) ratios(1)]); + set(p.Results.axesHandle,'DataAspectRatio',[res 1]) +end + +%% Set Colorbar +hCMap = matRad_plotColorbar(p.Results.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 text properties +if ~isempty(textVarargin) + set(p.Results.axesHandle, textVarargin{:}) + set(p.Results.axesHandle.Title, textVarargin{:}) + set(get(hCMap,'YLabel'),'String', p.Results.colorBarLabel, textVarargin{:}); +end + +end \ No newline at end of file From 8531efe65384aca579214fea2e00cbef1c8ea9ca Mon Sep 17 00:00:00 2001 From: JenHardt Date: Tue, 28 Jan 2025 16:34:27 +0100 Subject: [PATCH 23/87] The ct.cube is not needed for TOPAS dose calc but causes errors here when it is not there, therefore it was removed --- matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m | 3 --- 1 file changed, 3 deletions(-) diff --git a/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m b/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m index 39e86e1d0..486fb9635 100644 --- a/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m +++ b/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m @@ -653,8 +653,6 @@ function writeAllFiles(obj,ct,cst,stf,machine,w) for s = 1:ct.numOfCtScen cubeHUresampled{s} = matRad_interp3(dij.ctGrid.x, dij.ctGrid.y', dij.ctGrid.z,ct.cubeHU{s}, ... dij.doseGrid.x,dij.doseGrid.y',dij.doseGrid.z,'linear'); - 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 @@ -671,7 +669,6 @@ function writeAllFiles(obj,ct,cst,stf,machine,w) % Write resampled cubes this.ctR.cubeHU = cubeHUresampled; - this.ctR.cube = cubeResampled; % Set flag for complete resampling this.ctR.resampled = 1; From 8ff7ebe8725f234f8d93ac6e41e9409bc74ed968 Mon Sep 17 00:00:00 2001 From: Amit Bennan Date: Wed, 29 Jan 2025 11:54:09 +0100 Subject: [PATCH 24/87] temporary fixed for BED and effect optimizations --- matRad/matRad_fluenceOptimization.m | 29 +++++-------------- .../projections/matRad_EffectProjection.m | 2 ++ 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/matRad/matRad_fluenceOptimization.m b/matRad/matRad_fluenceOptimization.m index 735324c82..2cc73168e 100644 --- a/matRad/matRad_fluenceOptimization.m +++ b/matRad/matRad_fluenceOptimization.m @@ -146,9 +146,9 @@ % 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 +% 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,25 +281,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); - - BEDTarget = doseTarget.*(1 + doseTarget./abr); - -% 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) -% -% 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 + abr = cst{ixTarget,5}.alphaX./cst{ixTarget,5}.betaX; + meanBED = mean((aTmp(V) + bTmp(V).^2)./cst{ixTarget,5}.alphaX); + + BEDTarget = doseTarget.*(1 + doseTarget./abr); bixelWeight = BEDTarget/meanBED; wInit = wOnes * bixelWeight; diff --git a/matRad/optimization/projections/matRad_EffectProjection.m b/matRad/optimization/projections/matRad_EffectProjection.m index ea06d6c22..22915b240 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') && dij.RBE == 1.1 + 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 From f5a52e27a48df7ca1a22f30c7c982151d7ad68ae Mon Sep 17 00:00:00 2001 From: Amit Bennan Date: Wed, 29 Jan 2025 12:27:36 +0100 Subject: [PATCH 25/87] clean up --- matRad/matRad_fluenceOptimization.m | 3 --- 1 file changed, 3 deletions(-) diff --git a/matRad/matRad_fluenceOptimization.m b/matRad/matRad_fluenceOptimization.m index 2cc73168e..e11f88cbe 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 From f75e84a140b330a500feb57d587cc88940562525 Mon Sep 17 00:00:00 2001 From: remocristoforetti <115004596+remocristoforetti@users.noreply.github.com> Date: Wed, 5 Feb 2025 16:38:28 +0100 Subject: [PATCH 26/87] FRED engine (#800) * Add FRED engine files * add FRED readme file * Add error message for hlut * Update fred engine * revert additional upload in wrong folder * change to RBExDose * add searchpath check for hlut * update default hlut naming * add correct folder delimiter * Move to SO specific path and fielseparators * remove useWSL and use static information for calls & version * * adapt tests for FRED * add radiationMode check to base engine * correct log level for dij reading in FRED engine * Update isActive function and check for installation. * explicit -nogpu command * Add missing radiationMode in brachy test * Update externalCalculation handling * Additional tests for FRED engine * Fix FRED tests * Fred data for testing --------- Co-authored-by: Niklas Wahl Co-authored-by: Cristoforetti --- examples/matRad_example18_FREDMC.m | 92 +++ .../matRad_DoseEngineBase.m | 5 + .../@matRad_ParticleFREDEngine/calcDose.m | 448 ++++++++++++ .../matRad_ParticleFREDEngine.m | 676 ++++++++++++++++++ .../readSimulationOutput.m | 75 ++ .../writePlanDeliveryFile.m | 162 +++++ .../writePlanFile.m | 183 +++++ .../writeRegionsFile.m | 81 +++ .../@matRad_ParticleFREDEngine/writeRunFile.m | 34 + .../matRad_default_FredMaterialConverter.txt | 9 + test/brachy/test_calcBrachyDose.m | 1 + test/doseCalc/test_FREDEngine.m | 141 ++++ test/testData/FRED_data/MCrun/fred.inp | 2 + .../FRED_data/MCrun/inp/plan/plan.inp | 30 + .../FRED_data/MCrun/inp/plan/planDelivery.inp | 76 ++ .../FRED_data/MCrun/inp/regions/CTpatient.mhd | 11 + .../FRED_data/MCrun/inp/regions/CTpatient.raw | Bin 0 -> 4000 bytes .../FRED_data/MCrun/inp/regions/hLut.inp | 9 + .../FRED_data/MCrun/inp/regions/regions.inp | 14 + .../MCrun/out/score/Phantom.Dose.mhd | Bin 0 -> 8262 bytes .../MCrun/out/score/Phantom.LETd.mhd | Bin 0 -> 8262 bytes .../MCrun/out/scoreij/Phantom.Dose.bin | Bin 0 -> 53424 bytes .../MCrun/out/scoreij/Phantom.LETd.bin | Bin 0 -> 79908 bytes test/testData/Fred.sparseDij.bin | Bin 0 -> 54080 bytes 24 files changed, 2049 insertions(+) create mode 100644 examples/matRad_example18_FREDMC.m create mode 100644 matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/calcDose.m create mode 100644 matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/matRad_ParticleFREDEngine.m create mode 100644 matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/readSimulationOutput.m create mode 100644 matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writePlanDeliveryFile.m create mode 100644 matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writePlanFile.m create mode 100644 matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writeRegionsFile.m create mode 100644 matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writeRunFile.m create mode 100644 matRad/doseCalc/FRED/hluts/matRad_default_FredMaterialConverter.txt create mode 100644 test/doseCalc/test_FREDEngine.m create mode 100644 test/testData/FRED_data/MCrun/fred.inp create mode 100644 test/testData/FRED_data/MCrun/inp/plan/plan.inp create mode 100644 test/testData/FRED_data/MCrun/inp/plan/planDelivery.inp create mode 100644 test/testData/FRED_data/MCrun/inp/regions/CTpatient.mhd create mode 100644 test/testData/FRED_data/MCrun/inp/regions/CTpatient.raw create mode 100644 test/testData/FRED_data/MCrun/inp/regions/hLut.inp create mode 100644 test/testData/FRED_data/MCrun/inp/regions/regions.inp create mode 100644 test/testData/FRED_data/MCrun/out/score/Phantom.Dose.mhd create mode 100644 test/testData/FRED_data/MCrun/out/score/Phantom.LETd.mhd create mode 100644 test/testData/FRED_data/MCrun/out/scoreij/Phantom.Dose.bin create mode 100644 test/testData/FRED_data/MCrun/out/scoreij/Phantom.LETd.bin create mode 100644 test/testData/Fred.sparseDij.bin 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/matRad/doseCalc/+DoseEngines/@matRad_DoseEngineBase/matRad_DoseEngineBase.m b/matRad/doseCalc/+DoseEngines/@matRad_DoseEngineBase/matRad_DoseEngineBase.m index b0fea5001..18aef9c73 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; diff --git a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/calcDose.m b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/calcDose.m new file mode 100644 index 000000000..9bf3e183e --- /dev/null +++ b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/calcDose.m @@ -0,0 +1,448 @@ +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).targetPoints = stfFred(i).energyLayer(j).targetPoints/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, logical(this.calcLET)); + + 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, logical(this.calcLET)); + + 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..6116ecd9d --- /dev/null +++ b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/matRad_ParticleFREDEngine.m @@ -0,0 +1,676 @@ +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'}; + + calcBioDose; + currentVersion; + availableVersions = {'3.70.0'}; % Interface requires latest FRED version + radiationMode; + + end + + properties + 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; + 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; + + 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 + version = cmdOut(1:end-1); + else + matRad_cfg.dispError('Something wrong occured in checking FRED installation. Please check correct FRED installation'); + end + catch + matRad_cfg.dispWarning('Something wrong occured in checking FRED installation. Please check correct FRED installation'); + 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; + % FRED adds 1000000 when new field is added + %bixNum = bixNum - (10^6*(i-1)); + + colIndices(end+1:end+numVox) = bixelCounter; %bixNum + 1; + currVoxelIndices = fread(f,numVox,"uint32") + 1; + tmpValues = fread(f,numVox*nComponents,"float32"); + valuesNom = tmpValues(1:nComponents:end);%tmpValues(nComponents:nComponents:end); + % values(end+1:end+numVox) = tmpValuess(1:nComponents:end);%tmpValues(nComponents: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 + + [doseCubeV, letdCubeV, fileName] = readSimulationOutput(runFolder,calcDoseDirect, calcLET); + + 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 + + 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.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..81633431f --- /dev/null +++ b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/readSimulationOutput.m @@ -0,0 +1,75 @@ +function [doseCube, letCube, loadFileName] = readSimulationOutput(runFolder,calcDoseDirect, varargin) + +matRad_cfg = MatRad_Config.instance(); + +p = inputParser(); +addRequired(p, 'runFolder', @ischar); +addRequired(p, 'calcDoseDirect', @islogical); +addOptional(p, 'calcLET',0,@islogical); + +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); + 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); + 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-ij 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..97dc18d4e --- /dev/null +++ b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writeRegionsFile.m @@ -0,0 +1,81 @@ +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 + +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/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/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_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/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 0000000000000000000000000000000000000000..847594b9e7bec7d1d502a8cb31c8f95b7738c70e GIT binary patch literal 4000 zcmb36BQXjGeF!k10Z^Qw2{DX{F;E-^#O8(3u%}P@8;uY8gfp?}c{Dyo<72d*!ClXc c=8w_*F`7R{^9QIt9SZ4y*z#&r-9Uu^0F}6sZU6uP literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6a58ff0b5d7a2f6e38e4ad7cc0623afe4bb2308d GIT binary patch literal 8262 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 Od7N@(7SbEL4E;ZwASJ~B literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6d103b2571fbbf0e617e3bbc54250136f7da272e GIT binary patch literal 8262 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b3ca89c51b1cb7f8089a791817907ed767a6f61a GIT binary patch literal 79908 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 Jl8f!s{{iU=+;ac` literal 0 HcmV?d00001 diff --git a/test/testData/Fred.sparseDij.bin b/test/testData/Fred.sparseDij.bin new file mode 100644 index 0000000000000000000000000000000000000000..46a6a22ec6f20406b7044cd7f3ddf4019c081e4d GIT binary patch literal 54080 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 B5rqH% literal 0 HcmV?d00001 From 17b3512add9bb50f2cd5ed010a3bd4970e0096d2 Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Wed, 5 Feb 2025 16:43:40 +0100 Subject: [PATCH 27/87] Update coverage-report.yml to not fail on PRs --- .github/workflows/coverage-report.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/coverage-report.yml b/.github/workflows/coverage-report.yml index 4a415cca4..e4b492077 100644 --- a/.github/workflows/coverage-report.yml +++ b/.github/workflows/coverage-report.yml @@ -60,6 +60,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 From 172acc1cb5cfac920a5e330f649f70b2b0bd854d Mon Sep 17 00:00:00 2001 From: JenHardt Date: Fri, 7 Feb 2025 10:54:23 +0100 Subject: [PATCH 28/87] update to ct.cube handling in TOPAS added hlut and use GivenEqDensityCube to allow use to choose if current ct should be used or a new hlut and ct.cube should be loaded --- .../+DoseEngines/matRad_TopasMCEngine.m | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m b/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m index 79251cab6..9bdb3d118 100644 --- a/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m +++ b/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m @@ -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 = []; @@ -176,7 +178,9 @@ 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]; @@ -641,6 +645,25 @@ function writeAllFiles(obj,ct,cst,stf,machine,w) 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... '); @@ -653,6 +676,8 @@ function writeAllFiles(obj,ct,cst,stf,machine,w) for s = 1:ct.numOfCtScen cubeHUresampled{s} = matRad_interp3(dij.ctGrid.x, dij.ctGrid.y', dij.ctGrid.z,ct.cubeHU{s}, ... dij.doseGrid.x,dij.doseGrid.y',dij.doseGrid.z,'linear'); + 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 @@ -669,6 +694,7 @@ function writeAllFiles(obj,ct,cst,stf,machine,w) % Write resampled cubes this.ctR.cubeHU = cubeHUresampled; + this.ctR.cube = cubeResampled; % Set flag for complete resampling this.ctR.resampled = 1; @@ -2159,7 +2185,7 @@ function writePatient(obj,ct,pln) % Write material converter switch obj.materialConverter.mode case 'RSP' % Relative stopping power converter - rspHlut = matRad_loadHLUT(ct,obj.radiationMode); + rspHlut = obj.hlut; min_HU = rspHlut(1,1); max_HU = rspHlut(end,1); @@ -2211,7 +2237,7 @@ function writePatient(obj,ct,pln) case 'HUToWaterSchneider' % Schneider converter - rspHlut = matRad_loadHLUT(ct,obj.radiationMode); + rspHlut = obj.hlut; try % Write Schneider Converter From d303f63755cd3b2348a0dae8ebc7c47c6e34f809 Mon Sep 17 00:00:00 2001 From: Amit Bennan Date: Sun, 9 Feb 2025 11:24:09 +0100 Subject: [PATCH 29/87] added BED optimization to carbon example (also for test) --- examples/matRad_example7_carbon.m | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/examples/matRad_example7_carbon.m b/examples/matRad_example7_carbon.m index 86426f511..dd7f80acd 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 From a81ddfdea5f06a71f6113f4fd26414d53c49470b Mon Sep 17 00:00:00 2001 From: JenHardt Date: Wed, 12 Feb 2025 17:35:16 +0100 Subject: [PATCH 30/87] should be added here since they are later an option in the beamProfile --- matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m b/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m index 9bdb3d118..c74df1012 100644 --- a/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m +++ b/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m @@ -1446,7 +1446,7 @@ function writeStfFields(obj,ct,stf,w,baseData) otherwise % if particles - if ~any(ismember(obj.beamProfile,{'biGaussian','simple'})) + if ~any(ismember(obj.beamProfile,{'biGaussian','simple','phasespace','virtualGaussian','uniform'})) matRad_cfg.dispWarning('beamProfile "%s" not available for particles, switching to "%s" as default.',obj.beamProfile,obj.defaultParticleBeamProfile); obj.beamProfile = obj.defaultParticleBeamProfile; end From f65ec9bf42d2065c3a92fd52e4d526f8be899c87 Mon Sep 17 00:00:00 2001 From: JenHardt Date: Fri, 14 Feb 2025 09:21:47 +0100 Subject: [PATCH 31/87] Revert "should be added here since they are later an option in the beamProfile" This reverts commit a81ddfdea5f06a71f6113f4fd26414d53c49470b. --- matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m b/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m index c74df1012..9bdb3d118 100644 --- a/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m +++ b/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m @@ -1446,7 +1446,7 @@ function writeStfFields(obj,ct,stf,w,baseData) otherwise % if particles - if ~any(ismember(obj.beamProfile,{'biGaussian','simple','phasespace','virtualGaussian','uniform'})) + if ~any(ismember(obj.beamProfile,{'biGaussian','simple'})) matRad_cfg.dispWarning('beamProfile "%s" not available for particles, switching to "%s" as default.',obj.beamProfile,obj.defaultParticleBeamProfile); obj.beamProfile = obj.defaultParticleBeamProfile; end From a1163e195ea27b21187156bebb0bf45ddc02c2a1 Mon Sep 17 00:00:00 2001 From: s742o Date: Fri, 14 Feb 2025 13:08:22 +0100 Subject: [PATCH 32/87] plotSlice function changes Ct is the only required parameter. Dose cube is optional. Option for plotting the ct added. --- matRad/util/matRad_plotSlice.m | 62 ++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/matRad/util/matRad_plotSlice.m b/matRad/util/matRad_plotSlice.m index ba06f0214..a704d88dd 100644 --- a/matRad/util/matRad_plotSlice.m +++ b/matRad/util/matRad_plotSlice.m @@ -1,4 +1,4 @@ -function [] = matRad_plotSlice(ct, dose, varargin) +function [] = matRad_plotSlice(ct, varargin) % matRad tool function to directly plot a complete slice of a ct with dose % optionally including contours and isolines % @@ -45,22 +45,25 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +defaultDose = []; defaultCst = []; -defaultSlice = floor(min(size(dose))./2); +defaultSlice = floor(min(ct.cubeDim)./2); defaultAxesHandle = gca; defaultCubeIdx = 1; defaultPlane = 1; defaultDoseWindow = []; defaultThresh = []; defaultAlpha = []; -defaultDoseColorMap = []; +defaultDoseColorMap = jet; defaultDoseIsoLevels = []; defaultVOIselection = []; defaultContourColorMap = []; defaultBoolPlotLegend = false; defaultColorBarLabel = []; +defaultShowCt = true; -isSlice = @(x) x>=1 && x<=max(size(dose)) && floor(x)==x; +isDose = @(x) isnumeric(x) && all(size(x) == ct.cubeDim); +isSlice = @(x) x>=1 && x<=max(ct.cubeDim) && floor(x)==x; isAxes = @(x) strcmp(get(gca, 'type'), 'axes'); isCubeIdx = @(x) isscalar(x); isPlane = @(x) isscalar(x) && (sum(x==[1, 2, 3])==1); @@ -73,12 +76,13 @@ isContourColorMap = @(x) isnumeric(x) && (size(x, 2)==3) && size(x, 1)>=2 && all(x(:) >= 0) && all(x(:) <= 1); isBoolPlotLegend = @(x) x==0 || x ==1; isColorBarLabel = @(x) isstring(x) || ischar(x); +isShowCt = @(x) isscalar(x) && (x==0) || (x==1); p = inputParser; p.KeepUnmatched = true; addRequired(p, 'ct') -addRequired(p, 'dose') +addParameter(p, 'dose', defaultDose, isDose) addParameter(p, 'cst', defaultCst) addParameter(p, 'slice', defaultSlice, isSlice) addParameter(p, 'axesHandle', defaultAxesHandle, isAxes) @@ -93,8 +97,9 @@ addParameter(p, 'contourColorMap', defaultContourColorMap, isContourColorMap) addParameter(p, 'boolPlotLegend', defaultBoolPlotLegend, isBoolPlotLegend) addParameter(p, 'colorBarLabel', defaultColorBarLabel, isColorBarLabel) +addParameter(p, 'showCt', defaultShowCt, isShowCt) -parse(p, ct, dose, varargin{:}); +parse(p, ct, varargin{:}); %% Unmatched properties % General properties @@ -118,24 +123,37 @@ % Flip axes direction set(p.Results.axesHandle,'YDir','Reverse'); % plot ct slice -hCt = matRad_plotCtSlice(p.Results.axesHandle,p.Results.ct.cubeHU,p.Results.cubeIdx,p.Results.plane,p.Results.slice, [], []); +if p.Results.showCt + hCt = matRad_plotCtSlice(p.Results.axesHandle,p.Results.ct.cubeHU,p.Results.cubeIdx,p.Results.plane,p.Results.slice, [], []); +else + %figure() +end hold on; %% Plot dose -if ~isempty(p.Results.doseWindow) && p.Results.doseWindow(2) - p.Results.doseWindow(1) <= 0 - p.Results.doseWindow = [0 2]; -end +if ~isempty(p.Results.dose) + if ~isempty(p.Results.doseWindow) && p.Results.doseWindow(2) - p.Results.doseWindow(1) <= 0 + p.Results.doseWindow = [0 2]; + end -[hDose,doseColorMap,doseWindow] = matRad_plotDoseSlice(p.Results.axesHandle, p.Results.dose, p.Results.plane, p.Results.slice, p.Results.thresh, p.Results.alpha, p.Results.doseColorMap, p.Results.doseWindow); + [hDose,doseColorMap,doseWindow] = matRad_plotDoseSlice(p.Results.axesHandle, p.Results.dose, p.Results.plane, p.Results.slice, p.Results.thresh, p.Results.alpha, p.Results.doseColorMap, p.Results.doseWindow); -%% Plot iso dose lines -if ~isempty(p.Results.doseIsoLevels) - hIsoDose = matRad_plotIsoDoseLines(p.Results.axesHandle,p.Results.dose,[],p.Results.doseIsoLevels,false,p.Results.plane,p.Results.slice,p.Results.doseColorMap,p.Results.doseWindow, lineVarargin{:}); - hold on; -else - hIsoDose = []; -end + %% Plot iso dose lines + if ~isempty(p.Results.doseIsoLevels) + hIsoDose = matRad_plotIsoDoseLines(p.Results.axesHandle,p.Results.dose,[],p.Results.doseIsoLevels,false,p.Results.plane,p.Results.slice,p.Results.doseColorMap,p.Results.doseWindow, lineVarargin{:}); + hold on; + else + hIsoDose = []; + end + %% Set Colorbar + hCMap = matRad_plotColorbar(p.Results.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) @@ -188,18 +206,10 @@ set(p.Results.axesHandle,'DataAspectRatio',[res 1]) end -%% Set Colorbar -hCMap = matRad_plotColorbar(p.Results.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 text properties if ~isempty(textVarargin) set(p.Results.axesHandle, textVarargin{:}) set(p.Results.axesHandle.Title, textVarargin{:}) - set(get(hCMap,'YLabel'),'String', p.Results.colorBarLabel, textVarargin{:}); end end \ No newline at end of file From 086c4990c462108d4f594219919bb7e273aba7aa Mon Sep 17 00:00:00 2001 From: RemoCristoforetti Date: Fri, 14 Feb 2025 16:31:43 +0100 Subject: [PATCH 33/87] Correct input order in function description --- matRad/matRad_calcDoseForward.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From 67a77d1bb259642a4e478d55007f2f0231d34746 Mon Sep 17 00:00:00 2001 From: RemoCristoforetti Date: Fri, 14 Feb 2025 16:32:10 +0100 Subject: [PATCH 34/87] Add source parametrization for FRED --- matRad/basedata/matRad_MCemittanceBaseData.m | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/matRad/basedata/matRad_MCemittanceBaseData.m b/matRad/basedata/matRad_MCemittanceBaseData.m index d8871bfaf..4dd248348 100644 --- a/matRad/basedata/matRad_MCemittanceBaseData.m +++ b/matRad/basedata/matRad_MCemittanceBaseData.m @@ -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 From 38c8a268e4eff66ece8f3ebbb7403c603a8f4cee Mon Sep 17 00:00:00 2001 From: RemoCristoforetti Date: Fri, 14 Feb 2025 16:32:46 +0100 Subject: [PATCH 35/87] Minor bug fix and cleanup --- .../@matRad_ParticleFREDEngine/calcDose.m | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/calcDose.m b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/calcDose.m index 9bf3e183e..a041d4535 100644 --- a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/calcDose.m +++ b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/calcDose.m @@ -86,12 +86,6 @@ % Use the emittance base data class to recover MC information emittanceBaseData = matRad_MCemittanceBaseData(this.machine,stf); - % if this.calcDoseDirect - % cumulativeWeights = 0; - % for i=1:length(stf) - % cumulativeWeights = cumulativeWeights + sum([stf(i).ray.weight]); - % end - % end % Loop over fields. FRED performs one single simulation for multiple % fields @@ -162,7 +156,7 @@ stfFred(i).emittanceRefPlaneDistance = []; % Need to get the parameters for the model from MCemittance - for eIdx=emittanceBaseData.energyIndex + for eIdx=emittanceBaseData.energyIndex' % Only using first focus index for now tmpOpticsData = emittanceBaseData.fitBeamOpticsForEnergy(eIdx,1); stfFred(i).emittanceX = [stfFred(i).emittanceX, tmpOpticsData.twissEpsilonX]; @@ -176,7 +170,7 @@ stfFred(i).sSQr_b = []; stfFred(i).sSQr_c = []; - for eIdx=emittanceBaseData.energyIndex + 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]; @@ -272,7 +266,6 @@ 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).targetPoints = stfFred(i).energyLayer(j).targetPoints/10; stfFred(i).energyLayer(j).nBixels = numel(stfFred(i).energyLayer(j).bixelNum); if this.calcDoseDirect @@ -330,7 +323,7 @@ systemCall = [this.cmdCall, ' -nogpu -f fred.inp']; end - % printOutput to matLab console + % printOutput to matlab console if this.printOutput [status,~] = system(systemCall,'-echo'); else From 8de5078cac88e116912f37511020cd4d41bc87ba Mon Sep 17 00:00:00 2001 From: RemoCristoforetti Date: Fri, 14 Feb 2025 16:33:44 +0100 Subject: [PATCH 36/87] Update to include readout of multiple ij file version --- .../@matRad_ParticleFREDEngine/calcDose.m | 4 +- .../matRad_ParticleFREDEngine.m | 206 +++++++++++++++++- .../readSimulationOutput.m | 37 +++- .../writeRegionsFile.m | 4 + 4 files changed, 236 insertions(+), 15 deletions(-) diff --git a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/calcDose.m b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/calcDose.m index a041d4535..39a188e23 100644 --- a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/calcDose.m +++ b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/calcDose.m @@ -339,14 +339,14 @@ end % read simulation output - [doseCube, letdCube] = this.readSimulationOutput(this.MCrunFolder,this.calcDoseDirect, logical(this.calcLET)); + [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, logical(this.calcLET)); + [doseCube, letdCube, loadFileName] = this.readSimulationOutput(this.MCrunFolder,this.calcDoseDirect, 'calcLET',logical(this.calcLET),'readFunctionHandle', this.dijReaderHandle); dij.externalCalculationLodPath = loadFileName; diff --git a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/matRad_ParticleFREDEngine.m b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/matRad_ParticleFREDEngine.m index 6116ecd9d..a7ed0a9d2 100644 --- a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/matRad_ParticleFREDEngine.m +++ b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/matRad_ParticleFREDEngine.m @@ -54,15 +54,17 @@ defaultHUtable = 'matRad_default_FredMaterialConverter'; AvailableSourceModels = {'gaussian', 'emittance', 'sigmaSqrModel'}; + defaultDijFormatVersion = '20'; calcBioDose; currentVersion; - availableVersions = {'3.70.0'}; % Interface requires latest FRED version + availableVersions = {'3.70.0'}; % Or higher. radiationMode; end properties + dijFormatVersion; externalCalculation; useGPU; calcLET; @@ -100,6 +102,7 @@ inputFolder; regionsFolder; planFolder; + dijReaderHandle; end methods @@ -316,6 +319,7 @@ function setDefaults(this) this.outputMCvariance = false; this.constantRBE = NaN; this.ignoreOutsideDensities = false; + this.dijFormatVersion = this.defaultDijFormatVersion; end @@ -517,14 +521,70 @@ function writeHlut(this,hLutFile) numVox = fread(f,1,"int32"); bixelCounter = bixelCounter +1; - % FRED adds 1000000 when new field is added - %bixNum = bixNum - (10^6*(i-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; %bixNum + 1; + colIndices(end+1:end+numVox) = bixelCounter; currVoxelIndices = fread(f,numVox,"uint32") + 1; tmpValues = fread(f,numVox*nComponents,"float32"); - valuesNom = tmpValues(1:nComponents:end);%tmpValues(nComponents:nComponents:end); - % values(end+1:end+numVox) = tmpValuess(1:nComponents:end);%tmpValues(nComponents:nComponents:end); + valuesNom = tmpValues(1:nComponents:end); if nComponents == 2 valuesDen = tmpValues(nComponents:nComponents:end); @@ -546,12 +606,78 @@ function writeHlut(this,hLutFile) 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, calcLET); + [doseCubeV, letdCubeV, fileName] = readSimulationOutput(runFolder,calcDoseDirect, varargin); end @@ -595,6 +721,25 @@ function updatePaths(obj, rootFolder) 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 @@ -626,6 +771,53 @@ function updatePaths(obj, rootFolder) 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) diff --git a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/readSimulationOutput.m b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/readSimulationOutput.m index 81633431f..0ae5da683 100644 --- a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/readSimulationOutput.m +++ b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/readSimulationOutput.m @@ -1,11 +1,34 @@ -function [doseCube, letCube, loadFileName] = readSimulationOutput(runFolder,calcDoseDirect, varargin) - +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); -addOptional(p, 'calcLET',0,@islogical); +addParameter(p, 'calcLET',0,@islogical); +addParameter(p, 'readFunctionHandle', @(lFile) DoseEngines.matRad_ParticleFREDEngine.readSparseDijBin(lFile)) parse(p, runFolder,calcDoseDirect, varargin{:}); @@ -27,7 +50,8 @@ % read dij matrix if isfile(loadFileName) - doseCube = DoseEngines.matRad_ParticleFREDEngine.readSparseDijBin(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 @@ -38,7 +62,8 @@ letdDijFileName = fullfile(doseDijFolder,letdDijFile); try - letCube = DoseEngines.matRad_ParticleFREDEngine.readSparseDijBin(letdDijFileName); +% letCube = DoseEngines.matRad_ParticleFREDEngine.readSparseDijBin(letdDijFileName); + letCube = p.Results.readFunctionHandle(letdDijFileName); catch matRad_cfg.dispError('unable to load file: %s',letdDijFileName); end @@ -49,7 +74,7 @@ doseCubeFileName = 'Phantom.Dose.mhd'; loadFileName = fullfile(doseCubeFolder, doseCubeFileName); - matRad_cfg.dispInfo(sprintf('Looking for scorer-ij file in sub folder: %s\n', strrep(doseCubeFolder, '\', '\\'))); + matRad_cfg.dispInfo(sprintf('Looking for scorer file in sub folder: %s\n', strrep(doseCubeFolder, '\', '\\'))); if isfile(loadFileName) doseCube = matRad_readMHD(loadFileName); diff --git a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writeRegionsFile.m b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writeRegionsFile.m index 97dc18d4e..849304e9e 100644 --- a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writeRegionsFile.m +++ b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/writeRegionsFile.m @@ -72,6 +72,10 @@ function writeRegionsFile(this,fName) 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]); From 39f749d00ef22ae5dec21958f91a8ef3af7b9736 Mon Sep 17 00:00:00 2001 From: RemoCristoforetti Date: Fri, 14 Feb 2025 17:22:59 +0100 Subject: [PATCH 37/87] Missing output bug fix --- .../@matRad_ParticleFREDEngine/matRad_ParticleFREDEngine.m | 1 + 1 file changed, 1 insertion(+) diff --git a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/matRad_ParticleFREDEngine.m b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/matRad_ParticleFREDEngine.m index a7ed0a9d2..443ae7adf 100644 --- a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/matRad_ParticleFREDEngine.m +++ b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/matRad_ParticleFREDEngine.m @@ -477,6 +477,7 @@ function writeHlut(this,hLutFile) end catch matRad_cfg.dispWarning('Something wrong occured in checking FRED installation. Please check correct FRED installation'); + version = []; end end From 4d6d96e69d63c16a4dce495fab44c00a5a3a540f Mon Sep 17 00:00:00 2001 From: s742o Date: Fri, 14 Feb 2025 21:11:04 +0100 Subject: [PATCH 38/87] plotSlice function modification to include display ct option and make dose an optional variable. plotSliceWrapper modified to deprecated function and calling plotSlice instead. --- matRad/util/matRad_plotSlice.m | 12 ++++++------ matRad/util/matRad_plotSliceWrapper.m | 9 ++++++++- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/matRad/util/matRad_plotSlice.m b/matRad/util/matRad_plotSlice.m index a704d88dd..676636c73 100644 --- a/matRad/util/matRad_plotSlice.m +++ b/matRad/util/matRad_plotSlice.m @@ -1,4 +1,4 @@ -function [] = matRad_plotSlice(ct, varargin) +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 % @@ -68,14 +68,14 @@ isCubeIdx = @(x) isscalar(x); isPlane = @(x) isscalar(x) && (sum(x==[1, 2, 3])==1); isDoseWindow = @(x) (length(x) == 2 && isvector(x)); -isThresh = @(x) isscalar(x) && (x>=0) && (x<=1); -isAlpha = @(x) isscalar(x) && (x>=0) && (x<=1); +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); -isDoseIsoLevels = @(x) isnumeric(x) && isvector(x); -isVOIselection = @(x) all(x(:)==1 | x(:)==0); +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); isBoolPlotLegend = @(x) x==0 || x ==1; -isColorBarLabel = @(x) isstring(x) || ischar(x); +isColorBarLabel = @(x) isstring(x) || ischar(x) || isempty(x); isShowCt = @(x) isscalar(x) && (x==0) || (x==1); p = inputParser; diff --git a/matRad/util/matRad_plotSliceWrapper.m b/matRad/util/matRad_plotSliceWrapper.m index bcc7d9436..f56d2bac6 100644 --- a/matRad/util/matRad_plotSliceWrapper.m +++ b/matRad/util/matRad_plotSliceWrapper.m @@ -63,6 +63,8 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +warning('Deprecation warning: matRad_plotSliceWrapper is deprecated. Using matRad_plot_Slice instead'); + % Handle the argument list if ~exist('thresh','var') || isempty(thresh) thresh = []; @@ -101,6 +103,11 @@ matRad_cfg = MatRad_Config.instance(); +warning('Deprecation warning: matRad_plotSliceWrapper is deprecated. Using matRad_plot_Slice instead'); + +[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); + +%{ set(axesHandle,'YDir','Reverse'); % plot ct slice hCt = matRad_plotCtSlice(axesHandle,ct.cubeHU,cubeIdx,plane,slice); @@ -171,6 +178,6 @@ if ~isempty(colorBarLabel) set(get(hCMap,'YLabel'),'String', colorBarLabel,'FontSize',matRad_cfg.gui.fontSize); end - +%} end From d87921c030968f6b7b2fd9b6bf3ef6965be93f1c Mon Sep 17 00:00:00 2001 From: s742o Date: Mon, 17 Feb 2025 14:41:23 +0100 Subject: [PATCH 39/87] plot Slice function small fix --- matRad/util/matRad_plotSlice.m | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/matRad/util/matRad_plotSlice.m b/matRad/util/matRad_plotSlice.m index 676636c73..c80af8237 100644 --- a/matRad/util/matRad_plotSlice.m +++ b/matRad/util/matRad_plotSlice.m @@ -133,11 +133,12 @@ %% Plot dose if ~isempty(p.Results.dose) if ~isempty(p.Results.doseWindow) && p.Results.doseWindow(2) - p.Results.doseWindow(1) <= 0 - p.Results.doseWindow = [0 2]; + %p.Results.doseWindow = [0 2]; + doseWindow = [min(min(min(p.Results.dose))) max(max(max(p.Results.dose)))]; + [hDose,doseColorMap,doseWindow] = matRad_plotDoseSlice(p.Results.axesHandle, p.Results.dose, p.Results.plane, p.Results.slice, p.Results.thresh, p.Results.alpha, p.Results.doseColorMap, doseWindow); + else + [hDose,doseColorMap,doseWindow] = matRad_plotDoseSlice(p.Results.axesHandle, p.Results.dose, p.Results.plane, p.Results.slice, p.Results.thresh, p.Results.alpha, p.Results.doseColorMap, p.Results.doseWindow); end - - [hDose,doseColorMap,doseWindow] = matRad_plotDoseSlice(p.Results.axesHandle, p.Results.dose, p.Results.plane, p.Results.slice, p.Results.thresh, p.Results.alpha, p.Results.doseColorMap, p.Results.doseWindow); - %% Plot iso dose lines if ~isempty(p.Results.doseIsoLevels) hIsoDose = matRad_plotIsoDoseLines(p.Results.axesHandle,p.Results.dose,[],p.Results.doseIsoLevels,false,p.Results.plane,p.Results.slice,p.Results.doseColorMap,p.Results.doseWindow, lineVarargin{:}); From 000d3a19be469559096153e864aaae46e4201628 Mon Sep 17 00:00:00 2001 From: JenHardt Date: Fri, 21 Feb 2025 09:51:27 +0100 Subject: [PATCH 40/87] update to include physical dose calc, and beam wise calc --- matRad/4D/matRad_calc4dDose.m | 50 ++++++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/matRad/4D/matRad_calc4dDose.m b/matRad/4D/matRad_calc4dDose.m index 072b7296f..807ab3873 100644 --- a/matRad/4D/matRad_calc4dDose.m +++ b/matRad/4D/matRad_calc4dDose.m @@ -74,12 +74,10 @@ 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.phaseDose{i} = tmpResultGUI.physicalDose; + % compute RBExDose with const RBE + if 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; @@ -90,14 +88,29 @@ else matRad_cfg.dispError('Unsupported biological model %s!',pln.bioModel.model); end + + for beamIx = 1:dij.numOfBeams + resultGUI.(['phaseDose_beam', num2str(beamIx)]){i} = tmpResultGUI.(['physicalDose_beam', num2str(beamIx)]); + % compute RBExD with const RBE + if isa(pln.bioModel,'matRad_ConstantRBE') + resultGUI.(['phaseRBExDose_beam', num2str(beamIx)]){i} = tmpResultGUI.(['RBExDose_beam', num2str(beamIx)]); + % compute all fields + 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 % accumulation -if isa(pln.bioModel,'matRad_EmptyBiologicalModel') - - resultGUI.accPhysicalDose = matRad_doseAcc(ct,resultGUI.phaseDose, cst, accType); - -elseif isa(pln.bioModel,'matRad_ConstantRBE') +resultGUI.accPhysicalDose = matRad_doseAcc(ct,resultGUI.phaseDose, cst, accType); +if isa(pln.bioModel,'matRad_ConstantRBE') resultGUI.accRBExDose = matRad_doseAcc(ct,resultGUI.phaseRBExDose, cst, accType); @@ -116,5 +129,22 @@ else matRad_cfg.dispError('Unsupported biological model %s!',pln.bioModel.model); end + +for beamIx = 1:dij.numOfBeams + resultGUI.(['accPhysicalDose_beam', num2str(beamIx)])= matRad_doseAcc(ct,resultGUI.(['phaseDose_beam', num2str(beamIx)]), cst, accType); + if isa(pln.bioModel,'matRad_ConstantRBE') + resultGUI.(['accRBExDose_beam', num2str(beamIx)]) = matRad_doseAcc(ct,resultGUI.(['phaseRBExDose_beam', num2str(beamIx)]), cst, accType); + elseif isa(pln.bioModel,'matRad_LQBasedModel') + resultGUI.(['accAlphaDose_beam', num2str(beamIx)]) = matRad_doseAcc(ct,resultGUI.(['phaseAlphaDose_beam', num2str(beamIx)]), cst, accType); + resultGUI.(['accSqrtBetaDose_beam', num2str(beamIx)]) = matRad_doseAcc(ct,resultGUI.(['phaseAlphaDose_beam', num2str(beamIx)]), cst, accType); + resultGUI.(['accEffect_beam', num2str(beamIx)]) = resultGUI.(['accAlphaDose_beam', num2str(beamIx)]) + resultGUI.(['accSqrtBetaDose_beam', num2str(beamIx)]).^2; + resultGUI.(['accRBExDose_beam', num2str(beamIx)]){i} = zeros(ct.cubeDim); + resultGUI.(['accRBExDose_beam', num2str(beamIx)]){i} = ((sqrt(ax{i}(ix).^2 + 4 .* bx{i}(ix) .* resultGUI.(['accEffect_beam', num2str(beamIx)]){i}(ix)) - ax{i}(ix))./(2.*bx{i}(ix))); + else + matRad_cfg.dispError('Unsupported biological model %s!',pln.bioModel.model); + end +end + + end From 50bcd8f30b88532dbde86232638e483eef62ada5 Mon Sep 17 00:00:00 2001 From: JenHardt Date: Fri, 21 Feb 2025 09:52:02 +0100 Subject: [PATCH 41/87] added index of phase --- matRad/4D/matRad_makePhaseMatrix.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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; From 4ef2a5c9d113e5dbca97ec2b0e99b09f2a4dabf8 Mon Sep 17 00:00:00 2001 From: JenHardt Date: Fri, 21 Feb 2025 09:53:44 +0100 Subject: [PATCH 42/87] added 4d stuff to Topas engine as well as corresponding test --- .../+DoseEngines/matRad_TopasMCEngine.m | 848 +++++++++++------- test/doseCalc/test_TopasMCEngine.m | 40 + 2 files changed, 588 insertions(+), 300 deletions(-) diff --git a/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m b/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m index 9bdb3d118..ed95eeeed 100644 --- a/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m +++ b/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m @@ -75,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' @@ -370,6 +374,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.calc4DInterplayI || obj.MCparam.numOfCtScen > 1 + for ctScen = 1:dij.numOfScenarios + tmpResultGUI = matRad_calcCubes(ones(dij.numOfBeams,1),dij,ctScen); + resultGUI.phaseDose{ctScen} = tmpResultGUI.phaseDose; + for beamIx = 1:dij.numOfBeams + resultGUI.(['phaseDose_beam', num2str(beamIx)]){ctScen} = tmpResultGUI.(['phaseDose_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 @@ -509,12 +536,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 @@ -762,13 +793,21 @@ function writeAllFiles(obj,ct,cst,stf,machine,w) % 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,'*'); @@ -776,6 +815,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 @@ -789,8 +832,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 @@ -860,8 +905,16 @@ function writeAllFiles(obj,ct,cst,stf,machine,w) end % Tally per field - if isfield(topasSum,'Sum') - topasCube.([tname '_beam' num2str(f)]){ctScen} = topasSum.Sum; + if isfield(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; @@ -1107,6 +1160,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)!) @@ -1182,7 +1238,7 @@ function writeRunHeader(obj,fID,fieldIx,runIx,ctScen) %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 @@ -1203,14 +1259,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); @@ -1220,7 +1283,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 @@ -1232,6 +1295,22 @@ function writeScorers(obj,fID) matRad_cfg.dispDebug('Reading doseToMedium 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 = replace(scorerTxt, '/Patient', ['/Patient' num2str(PhaseNum)]); + scorerTxt = replace(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'}]; @@ -1246,8 +1325,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 @@ -1255,8 +1336,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 @@ -1269,6 +1352,46 @@ 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 = replace(scorerTxt, 'Alpha/', ['Alpha' num2str(PhaseNum) '/']); + scorerTxt = replace(scorerTxt, 'Beta/', ['Beta' num2str(PhaseNum) '/']); + scorerTxt = replace(scorerTxt, '/RBE', ['/RBE' num2str(PhaseNum)]); + if contains(lower(obj.scorer.RBE_model{i}),'mcn') + scorerTxt = replace(scorerTxt, '_alpha_MCN', ['_alpha_MCN-matRad_cube' num2str(PhaseNum)]); + scorerTxt = replace(scorerTxt, '_beta_MCN', ['_beta_MCN-matRad_cube' num2str(PhaseNum)]); + elseif contains(lower(obj.scorer.RBE_model{i}),'wed') + scorerTxt = replace(scorerTxt, '_alpha_WED', ['_alpha_WED-matRad_cube' num2str(PhaseNum)]); + scorerTxt = replace(scorerTxt, '_beta_WED', ['_beta_WED-matRad_cube' num2str(PhaseNum)]); + elseif contains(lower(obj.scorer.RBE_model{i}),'lem') + scorerTxt = replace(scorerTxt, '_alpha_LEM', ['_alpha_LEM-matRad_cube' num2str(PhaseNum)]); + scorerTxt = replace(scorerTxt, '_beta_LEM', ['_beta_LEM-matRad_cube' num2str(PhaseNum)]); + scorerTxt = replace(scorerTxt, '_RBE_LEM', ['_RBE_LEM-matRad_cube' num2str(PhaseNum)]); + elseif contains(lower(obj.scorer.RBE_model{i}),'libamtrack') + scorerTxt = replace(scorerTxt, '_alpha_libamtrack', ['_alpha_libamtrack-matRad_cube' num2str(PhaseNum)]); + scorerTxt = replace(scorerTxt, '_beta_libamtrack', ['_beta_libamtrack-matRad_cube' num2str(PhaseNum)]); + scorerTxt = replace(scorerTxt, '_RBE_libamtrack', ['_RBE_libamtrack-matRad_cube' num2str(PhaseNum)]); + end + %dont allways write cell lines + if contains(scorerTxt,'### HCP Tabulated ###') + scorerTxt = extractBefore(scorerTxt, '### HCP Tabulated ###'); + end + fprintf(fID,'\n%s\n\n',scorerTxt); + scorerInString = {'tabulatedAlpha', 'tabulatedBeta', 'RBE', 'McNamaraAlpha', 'McNamaraBeta', 'WedenbergAlpha', 'WedenbergBeta'}; + for ixWrite = ixToWrite4DInterplay + 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 @@ -1326,6 +1449,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 @@ -1338,6 +1469,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 = replace(scorerTxt, 'Tally_DoseToWater', ['Tally_DoseToWater' num2str(PhaseNum)]); + scorerTxt = replace(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 @@ -1350,6 +1498,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 = replace(scorerTxt, '/ProtonLET', ['/ProtonLET' num2str(PhaseNum)]); + scorerTxt = replace(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 @@ -1512,7 +1676,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) @@ -1573,96 +1736,100 @@ 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'} + [~,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) @@ -1672,8 +1839,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 @@ -1687,19 +1859,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; @@ -1708,11 +1882,11 @@ 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 @@ -2058,6 +2232,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)); @@ -2092,7 +2313,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 @@ -2112,8 +2333,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 @@ -2170,239 +2391,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 = 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); - - 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 = 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]; - + 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 diff --git a/test/doseCalc/test_TopasMCEngine.m b/test/doseCalc/test_TopasMCEngine.m index bee074bd4..dd10d5b92 100644 --- a/test/doseCalc/test_TopasMCEngine.m +++ b/test/doseCalc/test_TopasMCEngine.m @@ -129,7 +129,47 @@ 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 +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 From 48582d4c90a697582bb4a90f75e1d92b677b4fa4 Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Wed, 26 Feb 2025 15:48:01 +0100 Subject: [PATCH 43/87] add attributes for line ending management --- .gitattributes | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..a3a14089c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +# 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 \ No newline at end of file From 68137774109433ff57a3aac3c6546da649b15a42 Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Wed, 26 Feb 2025 15:54:48 +0100 Subject: [PATCH 44/87] fix in git attributes --- .gitattributes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index a3a14089c..c127814df 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,5 @@ # Auto-detect text files and normalize (LF in repo). -* text-auto +* text=auto # Force files to be binary *.mat binary From ca05aed22762deed3b0d54289bda5c2ab8d96660 Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Wed, 26 Feb 2025 16:16:37 +0100 Subject: [PATCH 45/87] Update .gitattributes such that m files are never committed as executable --- .gitattributes | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index c127814df..46879b073 100644 --- a/.gitattributes +++ b/.gitattributes @@ -7,4 +7,7 @@ *.mex* binary *.zip binary *.dll binary -*.exe binary \ No newline at end of file +*.exe binary + +# Ensure .m files are never marked as executable +*.m -x \ No newline at end of file From 1c49003257fd426e15e3fc2c0ad1ab914868dd5d Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Wed, 26 Feb 2025 18:31:12 +0100 Subject: [PATCH 46/87] code cleanup --- matRad/util/matRad_plotSlice.m | 34 ++++++------ matRad/util/matRad_plotSliceWrapper.m | 74 +-------------------------- 2 files changed, 16 insertions(+), 92 deletions(-) diff --git a/matRad/util/matRad_plotSlice.m b/matRad/util/matRad_plotSlice.m index c80af8237..8857d5e05 100644 --- a/matRad/util/matRad_plotSlice.m +++ b/matRad/util/matRad_plotSlice.m @@ -34,7 +34,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % -% Copyright 2015 the matRad development team. +% Copyright 2025 the matRad development team. % % This file is part of the matRad project. It is subject to the license % terms in the LICENSE file found in the top-level directory of this @@ -132,19 +132,17 @@ %% Plot dose if ~isempty(p.Results.dose) + doseWindow = [min(min(min(p.Results.dose))) max(max(max(p.Results.dose)))]; if ~isempty(p.Results.doseWindow) && p.Results.doseWindow(2) - p.Results.doseWindow(1) <= 0 - %p.Results.doseWindow = [0 2]; - doseWindow = [min(min(min(p.Results.dose))) max(max(max(p.Results.dose)))]; - [hDose,doseColorMap,doseWindow] = matRad_plotDoseSlice(p.Results.axesHandle, p.Results.dose, p.Results.plane, p.Results.slice, p.Results.thresh, p.Results.alpha, p.Results.doseColorMap, doseWindow); - else - [hDose,doseColorMap,doseWindow] = matRad_plotDoseSlice(p.Results.axesHandle, p.Results.dose, p.Results.plane, p.Results.slice, p.Results.thresh, p.Results.alpha, p.Results.doseColorMap, p.Results.doseWindow); - end + doseWindow = p.Results.doseWindow; + end + [hDose,doseColorMap,doseWindow] = matRad_plotDoseSlice(p.Results.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(p.Results.axesHandle,p.Results.dose,[],p.Results.doseIsoLevels,false,p.Results.plane,p.Results.slice,p.Results.doseColorMap,p.Results.doseWindow, lineVarargin{:}); - hold on; - else - hIsoDose = []; end %% Set Colorbar @@ -161,14 +159,13 @@ [hContour,~] = matRad_plotVoiContourSlice(p.Results.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)); - ixLegend = find(p.Results.voiSelection); + visibleOnSlice = (~cellfun(@isempty,hContour)); hContourTmp = cellfun(@(X) X(1),hContour(visibleOnSlice),'UniformOutput',false); + voiSelection = visibleOnSlice; if ~isempty(p.Results.voiSelection) - hLegend = legend(p.Results.axesHandle,[hContourTmp{:}],[p.Results.cst(ixLegend(visibleOnSlice),2)],'AutoUpdate','off','TextColor',matRad_cfg.gui.textColor); - else - hLegend = legend(p.Results.axesHandle,[hContourTmp{:}],[p.Results.cst(visibleOnSlice,2)],'AutoUpdate','off','TextColor',matRad_cfg.gui.textColor); + voiSelection = visibleOnSlice(find(p.Results.voiSelection)); end + hLegend = legend(p.Results.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) @@ -185,12 +182,11 @@ axis(p.Results.axesHandle,'tight'); set(p.Results.axesHandle,'xtick',[],'ytick',[]); colormap(p.Results.axesHandle,p.Results.doseColorMap); - +fontSize = []; if isfield(p.Unmatched, 'FontSize') - matRad_plotAxisLabels(p.Results.axesHandle,p.Results.ct,p.Results.plane,p.Results.slice, p.Unmatched.FontSize, []) -else - matRad_plotAxisLabels(p.Results.axesHandle,p.Results.ct,p.Results.plane,p.Results.slice, [], []) + fontSize = p.Unmatched.FontSize; end +matRad_plotAxisLabels(p.Results.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]; diff --git a/matRad/util/matRad_plotSliceWrapper.m b/matRad/util/matRad_plotSliceWrapper.m index f56d2bac6..81d60f9bf 100644 --- a/matRad/util/matRad_plotSliceWrapper.m +++ b/matRad/util/matRad_plotSliceWrapper.m @@ -103,81 +103,9 @@ matRad_cfg = MatRad_Config.instance(); -warning('Deprecation warning: matRad_plotSliceWrapper is deprecated. Using matRad_plot_Slice instead'); +matRad_cfg.dispDeprecationWarning('Deprecation warning: matRad_plotSliceWrapper is deprecated. Using matRad_plot_Slice instead'); [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); -%{ -set(axesHandle,'YDir','Reverse'); -% plot ct slice -hCt = matRad_plotCtSlice(axesHandle,ct.cubeHU,cubeIdx,plane,slice); -hold on; - -% 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 -%} end From aecbc90cf3cba439a3968eb0cf0e63de51ff848a Mon Sep 17 00:00:00 2001 From: mlapaeva <34272754+mlapaeva@users.noreply.github.com> Date: Wed, 26 Mar 2025 22:51:40 +0100 Subject: [PATCH 47/87] Synthetic CT example (#827) * [synthetic CT example]: create dicoms for ct and sCT from liver phantom * [synthetic CT example]: fix bug with non-closing modal window, when dicom is loaded withour structure file * [synthetic CT example]: creating synthetic CT example for DVH diff calcs * Revert "[synthetic CT example]: create dicoms for ct and sCT from liver phantom" This reverts commit 9a925059af7d50c10511011f66cb1ead843bc66a. * [synthetic CT example]: DICOM import included, and changes from review are applied * [Synthetic CT example]: name is added to author list and citation * [synthetic CT example]: example is added to autotests --------- Co-authored-by: mlapaeva <34272754+MariyaLapaeva@users.noreply.github.com> Co-authored-by: Niklas Wahl --- AUTHORS.txt | 3 +- CITATION.cff | 2 + ..._example18_CT_sCT_DVH_difference_photons.m | 321 ++++++++++++++++++ .../matRad_importDicom.m | 4 +- test/autoExampleTest/test_examples.m | 1 + 5 files changed, 329 insertions(+), 2 deletions(-) create mode 100644 examples/matRad_example18_CT_sCT_DVH_difference_photons.m diff --git a/AUTHORS.txt b/AUTHORS.txt index 8d2dc3189..690d7fc31 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 @@ -26,6 +26,7 @@ * Navid Khaledi * Thomas Klinge * Jeremias Kunz +* Mariia Lapaeva * Paul Anton Meder * Henning Mescher * Lucas-Raphael Müller diff --git a/CITATION.cff b/CITATION.cff index 0019001d3..41b30e17b 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -61,6 +61,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" diff --git a/examples/matRad_example18_CT_sCT_DVH_difference_photons.m b/examples/matRad_example18_CT_sCT_DVH_difference_photons.m new file mode 100644 index 000000000..4b106deb9 --- /dev/null +++ b/examples/matRad_example18_CT_sCT_DVH_difference_photons.m @@ -0,0 +1,321 @@ +%% Example: Photon 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. +% +% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + +%% 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. +clear; +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= "userdata\syntheticCT\"; % If you want to export your data, use "userdata" folder as it is ignored by git +name="pat001"; +patDirRealCT= fullfile(patDir,name,"realCT"); +patDirFakeCT= fullfile(patDir,name,"fakeCT"); + +try + if ~exist(patDirRealCT, 'dir') + mkdir(patDirRealCT); + end +catch ME + fprintf('Error creating realCT directory: %s\n', patDirRealCT); + fprintf('Error message: %s\n', ME.message); +end + +try + if ~exist(patDirFakeCT, 'dir') + mkdir(patDirFakeCT); + end +catch ME + fprintf('Error creating fakeCT directory: %s\n', patDirFakeCT); + fprintf('Error message: %s\n', ME.message); +end + +% 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'); +realCTct=ct; +%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; +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(matRadGUI); +clearvars -except 'patDir' 'patDirFakeCT' 'patDirRealCT' 'matRad_cfg' 'name'; + + +%% 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); +% 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 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 = 4; + +% 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 = 4; % [mm] +pln.propDoseCalc.doseGrid.resolution.y = 4; % [mm] +pln.propDoseCalc.doseGrid.resolution.z = 4; % [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. +dvhTableReal=struct2table(qi); +% Select only DVH parameters from QI table you are interested in comparison +dvhTableReal=dvhTableReal(:,1:9); +dvhTableReal.patient= repmat(char(name),length(qi),1); +dvhTableReal.ct_type = repmat(char("real"),length(qi),1); +% Check DVH table for real CT +disp(dvhTableReal); + +%% 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 +dvhTableFake=struct2table(qi); +% Select the same DVH parameters for further comparison +dvhTableFake=dvhTableFake(:,1:9); +dvhTableFake.patient= repmat(char(name),length(qi),1); +dvhTableFake.ct_type = repmat(char("fake"),length(qi),1); + +%% Calculate the difference in between of DVH calculated on real CT (dvh_table_real) and synthetic CT (dvh_table_fake) +dvhTableDiff=dvhTableReal; +for i =1:height(dvhTableReal) + for j=["mean","std","max", "min", "D_2","D_5", "D_95", "D_98"] + 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 + +%% Check the difference +disp(dvhTableDiff) + +%%% Save the results to CSV file +%file_path_dvh_diff = "YOUR PATH" +%writetable(dvh_table_diff, file_path_dvh_diff); +delete(matRadGUI); +close all; +clear; \ No newline at end of file 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/test/autoExampleTest/test_examples.m b/test/autoExampleTest/test_examples.m index d533ed34c..49251bdbf 100644 --- a/test/autoExampleTest/test_examples.m +++ b/test/autoExampleTest/test_examples.m @@ -23,6 +23,7 @@ 'examples/matRad_example12_simpleParticleMonteCarlo.m',... 'examples/matRad_example15_brachy.m',... 'examples/matRad_example17_biologicalModels.m',... + 'examples/matRad_example18_CT_sCT_DVH_difference_photons.m',... 'matRad.m',... }; From 2f9668554eae08db1013bb9b6233c603d85d60fa Mon Sep 17 00:00:00 2001 From: JenHardt Date: Fri, 4 Apr 2025 12:22:53 +0200 Subject: [PATCH 48/87] so that topas output files and analytically calculated have the same name in resultGUI --- matRad/4D/matRad_calc4dDose.m | 2 +- .../matRad_DoseEngineBase.m | 28 +++++++++++++++++-- .../+DoseEngines/matRad_TopasMCEngine.m | 6 ++-- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/matRad/4D/matRad_calc4dDose.m b/matRad/4D/matRad_calc4dDose.m index 807ab3873..2ca072606 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 diff --git a/matRad/doseCalc/+DoseEngines/@matRad_DoseEngineBase/matRad_DoseEngineBase.m b/matRad/doseCalc/+DoseEngines/@matRad_DoseEngineBase/matRad_DoseEngineBase.m index 18aef9c73..1041224de 100644 --- a/matRad/doseCalc/+DoseEngines/@matRad_DoseEngineBase/matRad_DoseEngineBase.m +++ b/matRad/doseCalc/+DoseEngines/@matRad_DoseEngineBase/matRad_DoseEngineBase.m @@ -290,17 +290,41 @@ 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 + resultGUI = matRad_appendResultGUI(resultGUI,resultGUItmp,false,sprintf('scen%d',i)); + end end + resultGUI.w = w; + end function dij = calcDoseInfluence(this,ct,cst,stf) diff --git a/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m b/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m index ed95eeeed..c4ef4e001 100644 --- a/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m +++ b/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m @@ -374,12 +374,12 @@ 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.calc4DInterplayI || obj.MCparam.numOfCtScen > 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.phaseDose; + resultGUI.phaseDose{ctScen} = tmpResultGUI.physicalDose; for beamIx = 1:dij.numOfBeams - resultGUI.(['phaseDose_beam', num2str(beamIx)]){ctScen} = tmpResultGUI.(['phaseDose_beam', num2str(beamIx)]); + 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; From 806f3617cefdeea4a601e0c90d2ad284a0ccbd27 Mon Sep 17 00:00:00 2001 From: JenHardt Date: Fri, 4 Apr 2025 12:39:36 +0200 Subject: [PATCH 49/87] update to new dose enging --- examples/matRad_example10_4DphotonRobust.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/matRad_example10_4DphotonRobust.m b/examples/matRad_example10_4DphotonRobust.m index 363a3797a..c18fff650 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 From 77e19abd85f7263dbe59e4af6bce08136e94e590 Mon Sep 17 00:00:00 2001 From: s742o Date: Fri, 4 Apr 2025 16:44:36 +0200 Subject: [PATCH 50/87] plotSlice fixing and testing --- matRad/util/matRad_plotSlice.m | 48 ++++++++++++------- test/util/test_plotSlice.m | 88 ++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 17 deletions(-) create mode 100644 test/util/test_plotSlice.m diff --git a/matRad/util/matRad_plotSlice.m b/matRad/util/matRad_plotSlice.m index 8857d5e05..8e328d59d 100644 --- a/matRad/util/matRad_plotSlice.m +++ b/matRad/util/matRad_plotSlice.m @@ -7,9 +7,9 @@ % % input (required) % ct matRad ct struct -% dose dose cube % % 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 @@ -26,9 +26,9 @@ % 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 % - % @@ -103,19 +103,19 @@ %% Unmatched properties % General properties -lineFieldNames = fieldnames(set(line)); -textFieldNames = fieldnames(set(text)); +lineFieldNames = fieldnames(set(line)); +textFieldNames = fieldnames(set(text)); % 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 properites from Unmatched -textFields = unmParamNames(ismember(unmParamNames, textFieldNames)); -textValues = struct2cell(p.Unmatched); -textValues = textValues(ismember(unmParamNames, textFieldNames)); -textVarargin = reshape([textFields, textValues]', 1, []); +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, []); %% Plot ct slice matRad_cfg = MatRad_Config.instance(); @@ -125,14 +125,12 @@ % plot ct slice if p.Results.showCt hCt = matRad_plotCtSlice(p.Results.axesHandle,p.Results.ct.cubeHU,p.Results.cubeIdx,p.Results.plane,p.Results.slice, [], []); -else - %figure() end hold on; %% Plot dose if ~isempty(p.Results.dose) - doseWindow = [min(min(min(p.Results.dose))) max(max(max(p.Results.dose)))]; + doseWindow = [min(p.Results.dose(:)) max(p.Results.dose(:))]; if ~isempty(p.Results.doseWindow) && p.Results.doseWindow(2) - p.Results.doseWindow(1) <= 0 doseWindow = p.Results.doseWindow; end @@ -209,4 +207,20 @@ set(p.Results.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 \ No newline at end of file diff --git a/test/util/test_plotSlice.m b/test/util/test_plotSlice.m new file mode 100644 index 000000000..2a8998444 --- /dev/null +++ b/test/util/test_plotSlice.m @@ -0,0 +1,88 @@ +function test_suite = test_plotSlice + +test_functions=localfunctions(); + +initTestSuite; + +function test_plot_ct_only + + load BOXPHANTOM.mat + figure() + [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSlice(ct); + assertTrue(isempty(hCMap)); + assertTrue(isempty(hDose)); + assertTrue(~isempty(hCt)&&isa(hCt, 'matlab.graphics.primitive.Image')); + assertTrue(isempty(hContour)); + assertTrue(isempty(hIsoDose)); + + load PROSTATE.mat + figure() + [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSlice(ct, 'slice', 91, 'cst', cst); + assertTrue(isempty(hCMap)); + assertTrue(isempty(hDose)); + assertTrue(~isempty(hCt)&&isa(hCt, 'matlab.graphics.primitive.Image')); + assertTrue(isa(hContour, 'cell')); + assertTrue(isempty(hIsoDose)); + + +function test_plot_dose_slice + + load protons_testData.mat + figure(); + [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSlice(ct, 'dose', resultGUI.physicalDose); + assertTrue(isa(hCMap, 'matlab.graphics.illustration.ColorBar')); + assertTrue(isa(hDose, 'matlab.graphics.primitive.Image')); + assertTrue(~isempty(hCt)&&isa(hCt, 'matlab.graphics.primitive.Image')); + assertTrue(isempty(hContour)); + assertTrue(isempty(hIsoDose)); + + load helium_testData.mat + figure(); + [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSlice(ct, 'dose', resultGUI.physicalDose); + assertTrue(isa(hCMap, 'matlab.graphics.illustration.ColorBar')); + assertTrue(isa(hDose, 'matlab.graphics.primitive.Image')); + assertTrue(~isempty(hCt)&&isa(hCt, 'matlab.graphics.primitive.Image')); + assertTrue(isempty(hContour)); + assertTrue(isempty(hIsoDose)); + + load carbon_testData.mat + figure(); + [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSlice(ct, 'dose', resultGUI.physicalDose); + assertTrue(isa(hCMap, 'matlab.graphics.illustration.ColorBar')); + assertTrue(isa(hDose, 'matlab.graphics.primitive.Image')); + assertTrue(~isempty(hCt)&&isa(hCt, 'matlab.graphics.primitive.Image')); + assertTrue(isempty(hContour)); + assertTrue(isempty(hIsoDose)); + + matRad; + figure(); + [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSlice(ct, 'dose', resultGUI.physicalDose, 'plane', 3); + assertTrue(isa(hCMap, 'matlab.graphics.illustration.ColorBar')); + assertTrue(isa(hDose, 'matlab.graphics.primitive.Image')); + assertTrue(~isempty(hCt)&&isa(hCt, 'matlab.graphics.primitive.Image')); + assertTrue(isempty(hContour)); + assertTrue(isempty(hIsoDose)); + +function test_optional_input + + matRad; + 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', 65, ... + '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(isa(hCMap, 'matlab.graphics.illustration.ColorBar')); + assertTrue(isa(hDose, 'matlab.graphics.primitive.Image')); + assertTrue(isempty(hCt)); + assertTrue(isa(hContour, "cell")); + assertTrue(isa(hIsoDose, "cell")); + From 2931519b8d274f22cdac363884683ddced45b697 Mon Sep 17 00:00:00 2001 From: s742o Date: Sat, 5 Apr 2025 05:05:27 +0200 Subject: [PATCH 51/87] First substitution of plotSlice to deprecated plotSliceWrapper into compareDose: works. --- matRad/planAnalysis/matRad_compareDose.m | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) 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); From 625ec6aeebb474d7552334758c53b9710b8789d0 Mon Sep 17 00:00:00 2001 From: s742o Date: Sat, 5 Apr 2025 05:58:35 +0200 Subject: [PATCH 52/87] Examples 1,2,5,7 substitution of plotSliceWrapper with new plotSlice --- examples/matRad_example1_phantom.m | 3 ++- examples/matRad_example2_photons.m | 9 ++++++--- examples/matRad_example5_protons.m | 9 ++++++--- examples/matRad_example7_carbon.m | 9 ++++++--- 4 files changed, 20 insertions(+), 10 deletions(-) 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_example2_photons.m b/examples/matRad_example2_photons.m index e1b6536a4..7df0c7b73 100644 --- a/examples/matRad_example2_photons.m +++ b/examples/matRad_example2_photons.m @@ -198,9 +198,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 +210,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 dd7f80acd..d57cf9a93 100644 --- a/examples/matRad_example7_carbon.m +++ b/examples/matRad_example7_carbon.m @@ -165,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 From 825413729e83a58b111576263c552fbef33019f6 Mon Sep 17 00:00:00 2001 From: s742o Date: Mon, 7 Apr 2025 10:54:40 +0200 Subject: [PATCH 53/87] Substitution of plotSliceWrapper with plotSlice --- examples/matRad_example10_4DphotonRobust.m | 30 ++++++++++++------- examples/matRad_example8_protonsRobust.m | 18 +++++++---- .../matRad_createAnimationForLatexReport.m | 3 +- .../samplingAnalysis/matRad_latexReport.m | 9 ++++-- 4 files changed, 40 insertions(+), 20 deletions(-) diff --git a/examples/matRad_example10_4DphotonRobust.m b/examples/matRad_example10_4DphotonRobust.m index 363a3797a..6bbd53ca8 100644 --- a/examples/matRad_example10_4DphotonRobust.m +++ b/examples/matRad_example10_4DphotonRobust.m @@ -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_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/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(); From 6589fa50084ad4956a6c29a4b133a8664d048bb8 Mon Sep 17 00:00:00 2001 From: s742o Date: Mon, 7 Apr 2025 10:56:18 +0200 Subject: [PATCH 54/87] Warning fix in plotSlice --- matRad/util/matRad_plotSliceWrapper.m | 2 -- 1 file changed, 2 deletions(-) diff --git a/matRad/util/matRad_plotSliceWrapper.m b/matRad/util/matRad_plotSliceWrapper.m index 81d60f9bf..4e73d3796 100644 --- a/matRad/util/matRad_plotSliceWrapper.m +++ b/matRad/util/matRad_plotSliceWrapper.m @@ -63,8 +63,6 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -warning('Deprecation warning: matRad_plotSliceWrapper is deprecated. Using matRad_plot_Slice instead'); - % Handle the argument list if ~exist('thresh','var') || isempty(thresh) thresh = []; From e5c40a7a89215ffdc4d08be7d9a195f44989aafa Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Mon, 14 Apr 2025 15:55:16 +0200 Subject: [PATCH 55/87] fix and rename sythetic ct example --- ...otons.m => matRad_example19_CT_sCT_DVH_difference_photons.m} | 2 +- test/autoExampleTest/test_examples.m | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename examples/{matRad_example18_CT_sCT_DVH_difference_photons.m => matRad_example19_CT_sCT_DVH_difference_photons.m} (99%) diff --git a/examples/matRad_example18_CT_sCT_DVH_difference_photons.m b/examples/matRad_example19_CT_sCT_DVH_difference_photons.m similarity index 99% rename from examples/matRad_example18_CT_sCT_DVH_difference_photons.m rename to examples/matRad_example19_CT_sCT_DVH_difference_photons.m index 4b106deb9..623437bb0 100644 --- a/examples/matRad_example18_CT_sCT_DVH_difference_photons.m +++ b/examples/matRad_example19_CT_sCT_DVH_difference_photons.m @@ -57,7 +57,7 @@ % 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'); +load('LIVER.mat'); realCTct=ct; %review real CT volume matRadGUI; diff --git a/test/autoExampleTest/test_examples.m b/test/autoExampleTest/test_examples.m index 49251bdbf..0aa2507a9 100644 --- a/test/autoExampleTest/test_examples.m +++ b/test/autoExampleTest/test_examples.m @@ -23,7 +23,7 @@ 'examples/matRad_example12_simpleParticleMonteCarlo.m',... 'examples/matRad_example15_brachy.m',... 'examples/matRad_example17_biologicalModels.m',... - 'examples/matRad_example18_CT_sCT_DVH_difference_photons.m',... + 'examples/matRad_example19_CT_sCT_DVH_difference_photons.m',... 'matRad.m',... }; From 696e9c4efff0c143be1df72112165d3021b6aaa8 Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Mon, 14 Apr 2025 16:41:07 +0200 Subject: [PATCH 56/87] fix matRadGUI return value if GUI is disabled in matRad_Config --- matRadGUI.m | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 From 0c533551abd41f5c588e4e1b718ba0753ba0d3cc Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Mon, 14 Apr 2025 16:41:45 +0200 Subject: [PATCH 57/87] some string adaptations in synthetic CT example --- ..._example19_CT_sCT_DVH_difference_photons.m | 124 +++++++++--------- 1 file changed, 61 insertions(+), 63 deletions(-) diff --git a/examples/matRad_example19_CT_sCT_DVH_difference_photons.m b/examples/matRad_example19_CT_sCT_DVH_difference_photons.m index 623437bb0..0502f88c2 100644 --- a/examples/matRad_example19_CT_sCT_DVH_difference_photons.m +++ b/examples/matRad_example19_CT_sCT_DVH_difference_photons.m @@ -1,4 +1,4 @@ -%% Example: Photon Treatment Plan +%% Example: Comparison of a CT and (fake) synthetic CT dose calculation % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % @@ -28,41 +28,32 @@ % 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. -clear; 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= "userdata\syntheticCT\"; % If you want to export your data, use "userdata" folder as it is ignored by git -name="pat001"; -patDirRealCT= fullfile(patDir,name,"realCT"); -patDirFakeCT= fullfile(patDir,name,"fakeCT"); - -try - if ~exist(patDirRealCT, 'dir') - mkdir(patDirRealCT); - end -catch ME - fprintf('Error creating realCT directory: %s\n', patDirRealCT); - fprintf('Error message: %s\n', ME.message); -end +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']; -try - if ~exist(patDirFakeCT, 'dir') - mkdir(patDirFakeCT); - end -catch ME - fprintf('Error creating fakeCT directory: %s\n', patDirFakeCT); - fprintf('Error message: %s\n', ME.message); +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'); -realCTct=ct; +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 = 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 @@ -71,7 +62,7 @@ % 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; +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 @@ -85,8 +76,8 @@ fakeCTct.cubeHU{1} = fakeCTcubeHU; %review fake CT volume -ct= fakeCTct; -matRadGUI; +ct = fakeCTct; +hGUI = matRadGUI; %saving as DICOMs dcmExpFakeCT= matRad_DicomExporter; % create instance of matRad_DicomExporter @@ -96,8 +87,7 @@ dcmExpFakeCT.matRad_exportDicom(); %clear all except of paths, close windows to start from clean space -delete(matRadGUI); -clearvars -except 'patDir' 'patDirFakeCT' 'patDirRealCT' 'matRad_cfg' 'name'; +delete(hGUI); %% Patient Data Import from DICOM @@ -113,6 +103,12 @@ 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; @@ -133,7 +129,7 @@ % This is the superclass for all objectives and constraints to enable easy % identification. - if cst{i,3} == "OAR" + if strcmp(cst{i,3},'OAR') if ~isempty(regexpi(cst{i,2},'spinal', 'once')) objective = DoseObjectives.matRad_MaxDVH; objective.penalty = 1; @@ -205,7 +201,7 @@ pln.propStf.gantryAngles = [0 50 100 150 200 250 300]; pln.propStf.couchAngles = zeros(1,numel(pln.propStf.gantryAngles)); -pln.propStf.bixelWidth = 4; +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 @@ -215,9 +211,9 @@ %% Dose calculation settings % set resolution of dose calculation and optimization -pln.propDoseCalc.doseGrid.resolution.x = 4; % [mm] -pln.propDoseCalc.doseGrid.resolution.y = 4; % [mm] -pln.propDoseCalc.doseGrid.resolution.z = 4; % [mm] +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. @@ -257,14 +253,16 @@ % 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. -dvhTableReal=struct2table(qi); -% Select only DVH parameters from QI table you are interested in comparison -dvhTableReal=dvhTableReal(:,1:9); -dvhTableReal.patient= repmat(char(name),length(qi),1); -dvhTableReal.ct_type = repmat(char("real"),length(qi),1); -% Check DVH table for real CT -disp(dvhTableReal); +% 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. @@ -292,30 +290,30 @@ qi = resultGUI.qi; % In the similar fashion, save DVH parameters calculated on the synthetic CT to the table -dvhTableFake=struct2table(qi); -% Select the same DVH parameters for further comparison -dvhTableFake=dvhTableFake(:,1:9); -dvhTableFake.patient= repmat(char(name),length(qi),1); -dvhTableFake.ct_type = repmat(char("fake"),length(qi),1); +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) -dvhTableDiff=dvhTableReal; -for i =1:height(dvhTableReal) - for j=["mean","std","max", "min", "D_2","D_5", "D_95", "D_98"] - 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"; +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 -end -%% Check the difference -disp(dvhTableDiff) + disp(dvhTableDiff) -%%% Save the results to CSV file -%file_path_dvh_diff = "YOUR PATH" -%writetable(dvh_table_diff, file_path_dvh_diff); -delete(matRadGUI); -close all; -clear; \ No newline at end of file + %%% 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 From e1e507c491f2e6d3c2cc6ce12eaaa73dc4785c40 Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Fri, 4 Apr 2025 10:50:46 +0200 Subject: [PATCH 58/87] Caches for Classes now check if the cached classes are valid, and recaches otherwise --- matRad/bioModels/matRad_BiologicalModel.m | 7 +++++++ .../@matRad_DoseEngineBase/getAvailableEngines.m | 8 ++++++++ matRad/scenarios/matRad_ScenarioModel.m | 7 +++++++ matRad/steering/matRad_StfGeneratorBase.m | 7 +++++++ 4 files changed, 29 insertions(+) diff --git a/matRad/bioModels/matRad_BiologicalModel.m b/matRad/bioModels/matRad_BiologicalModel.m index b770dd9b4..cfd18ddec 100644 --- a/matRad/bioModels/matRad_BiologicalModel.m +++ b/matRad/bioModels/matRad_BiologicalModel.m @@ -128,6 +128,13 @@ folders = [folders matRad_cfg.userfolders]; 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); diff --git a/matRad/doseCalc/+DoseEngines/@matRad_DoseEngineBase/getAvailableEngines.m b/matRad/doseCalc/+DoseEngines/@matRad_DoseEngineBase/getAvailableEngines.m index 445b5b79d..44582f171 100644 --- a/matRad/doseCalc/+DoseEngines/@matRad_DoseEngineBase/getAvailableEngines.m +++ b/matRad/doseCalc/+DoseEngines/@matRad_DoseEngineBase/getAvailableEngines.m @@ -37,6 +37,14 @@ %Get available, valid classes through call to matRad helper function %for finding subclasses 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); diff --git a/matRad/scenarios/matRad_ScenarioModel.m b/matRad/scenarios/matRad_ScenarioModel.m index 640ed52c4..f84a3dfc3 100644 --- a/matRad/scenarios/matRad_ScenarioModel.m +++ b/matRad/scenarios/matRad_ScenarioModel.m @@ -234,6 +234,13 @@ function listAllScenarios(this) folders = [folders matRad_cfg.userfolders]; 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); diff --git a/matRad/steering/matRad_StfGeneratorBase.m b/matRad/steering/matRad_StfGeneratorBase.m index 1f48daf88..31c5a11ee 100644 --- a/matRad/steering/matRad_StfGeneratorBase.m +++ b/matRad/steering/matRad_StfGeneratorBase.m @@ -446,6 +446,13 @@ function createPatientGeometry(this) %Get available, valid classes through call to matRad helper function %for finding subclasses 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); From 3696549c70e7e1e6961a607b40d41009765a7add Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Thu, 8 May 2025 19:23:04 +0200 Subject: [PATCH 59/87] bugfix for EXTERNAL contours and speedub of jacobian structure for constrained optimization --- .../matRad_getConstraintBounds.m | 2 +- .../matRad_getJacobianStructure.m | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) 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 02b44ce05..ec5eabbff 100644 --- a/matRad/optimization/@matRad_OptimizationProblem/matRad_getJacobianStructure.m +++ b/matRad/optimization/@matRad_OptimizationProblem/matRad_getJacobianStructure.m @@ -32,7 +32,9 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % 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. @@ -43,12 +45,15 @@ 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 From c595fe76b1fe5fddbc592d0864c9ce4ece0ace45 Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Thu, 8 May 2025 21:10:54 +0200 Subject: [PATCH 60/87] viewing widget export function --- matRad/gui/widgets/matRad_ViewingWidget.m | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/matRad/gui/widgets/matRad_ViewingWidget.m b/matRad/gui/widgets/matRad_ViewingWidget.m index 6be1c3491..2f8920e94 100644 --- a/matRad/gui/widgets/matRad_ViewingWidget.m +++ b/matRad/gui/widgets/matRad_ViewingWidget.m @@ -1360,6 +1360,11 @@ function updateDisplaySelection(this,visSelection) end end end + + function exportSlice(this,filename,varargin) + exportgraphics(this.handles.figure1,filename,varargin{:}); + end + end From fe64f8dae79a7bf6f06b16e8dc8feaf83489947c Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Thu, 22 May 2025 14:49:19 +0200 Subject: [PATCH 61/87] streamlined (using test data only) and octave compatible tests --- test/util/test_plotSlice.m | 61 +++++++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/test/util/test_plotSlice.m b/test/util/test_plotSlice.m index 2a8998444..fed895e01 100644 --- a/test/util/test_plotSlice.m +++ b/test/util/test_plotSlice.m @@ -11,16 +11,22 @@ [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSlice(ct); assertTrue(isempty(hCMap)); assertTrue(isempty(hDose)); - assertTrue(~isempty(hCt)&&isa(hCt, 'matlab.graphics.primitive.Image')); + assertFalse(isempty(hCt)); + if ~moxunit_util_platform_is_octave + assertTrue(isa(hCt, 'matlab.graphics.primitive.Image')) + end assertTrue(isempty(hContour)); assertTrue(isempty(hIsoDose)); - + load PROSTATE.mat figure() [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSlice(ct, 'slice', 91, 'cst', cst); assertTrue(isempty(hCMap)); assertTrue(isempty(hDose)); - assertTrue(~isempty(hCt)&&isa(hCt, 'matlab.graphics.primitive.Image')); + assertFalse(isempty(hCt)); + if ~moxunit_util_platform_is_octave + assertTrue(isa(hCt, 'matlab.graphics.primitive.Image')) + end assertTrue(isa(hContour, 'cell')); assertTrue(isempty(hIsoDose)); @@ -30,48 +36,60 @@ load protons_testData.mat figure(); [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSlice(ct, 'dose', resultGUI.physicalDose); - assertTrue(isa(hCMap, 'matlab.graphics.illustration.ColorBar')); - assertTrue(isa(hDose, 'matlab.graphics.primitive.Image')); - assertTrue(~isempty(hCt)&&isa(hCt, 'matlab.graphics.primitive.Image')); + 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 load helium_testData.mat figure(); [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSlice(ct, 'dose', resultGUI.physicalDose); - assertTrue(isa(hCMap, 'matlab.graphics.illustration.ColorBar')); - assertTrue(isa(hDose, 'matlab.graphics.primitive.Image')); - assertTrue(~isempty(hCt)&&isa(hCt, 'matlab.graphics.primitive.Image')); + 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 load carbon_testData.mat figure(); [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSlice(ct, 'dose', resultGUI.physicalDose); - assertTrue(isa(hCMap, 'matlab.graphics.illustration.ColorBar')); - assertTrue(isa(hDose, 'matlab.graphics.primitive.Image')); - assertTrue(~isempty(hCt)&&isa(hCt, 'matlab.graphics.primitive.Image')); + 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 - matRad; + load photons_testData.mat figure(); [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSlice(ct, 'dose', resultGUI.physicalDose, 'plane', 3); - assertTrue(isa(hCMap, 'matlab.graphics.illustration.ColorBar')); - assertTrue(isa(hDose, 'matlab.graphics.primitive.Image')); - assertTrue(~isempty(hCt)&&isa(hCt, 'matlab.graphics.primitive.Image')); + 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 function test_optional_input - matRad; + load photons_testData.mat 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', 65, ... + '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(:))], ... @@ -80,9 +98,10 @@ 'colorBarLabel', 'Absorbed Dose [Gy]', ... 'boolPlotLegend', 1, 'showCt', 0, 'FontSize', 13); - assertTrue(isa(hCMap, 'matlab.graphics.illustration.ColorBar')); - assertTrue(isa(hDose, 'matlab.graphics.primitive.Image')); assertTrue(isempty(hCt)); assertTrue(isa(hContour, "cell")); assertTrue(isa(hIsoDose, "cell")); - + if ~moxunit_util_platform_is_octave + assertTrue(isa(hDose, 'matlab.graphics.primitive.Image')); + assertTrue(isa(hCt, 'matlab.graphics.primitive.Image')) + end From 7f1e98e3263cde8affa2e883475d8c091656612c Mon Sep 17 00:00:00 2001 From: JenHardt <84377305+JenHardt@users.noreply.github.com> Date: Mon, 16 Jun 2025 12:55:19 +0200 Subject: [PATCH 62/87] updates to solve error for photon 4d calc --- matRad/4D/matRad_calc4dDose.m | 112 ++++++++++++++++++---------------- 1 file changed, 60 insertions(+), 52 deletions(-) diff --git a/matRad/4D/matRad_calc4dDose.m b/matRad/4D/matRad_calc4dDose.m index 2ca072606..5aea56558 100644 --- a/matRad/4D/matRad_calc4dDose.m +++ b/matRad/4D/matRad_calc4dDose.m @@ -75,73 +75,81 @@ % compute physical dose for physical opt resultGUI.phaseDose{i} = tmpResultGUI.physicalDose; - % compute RBExDose with const RBE - 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 - - for beamIx = 1:dij.numOfBeams - resultGUI.(['phaseDose_beam', num2str(beamIx)]){i} = tmpResultGUI.(['physicalDose_beam', num2str(beamIx)]); - % compute RBExD with const RBE + if ~isa(pln.bioModel,'matRad_EmptyBiologicalModel') + % compute RBExDose if isa(pln.bioModel,'matRad_ConstantRBE') - resultGUI.(['phaseRBExDose_beam', num2str(beamIx)]){i} = tmpResultGUI.(['RBExDose_beam', num2str(beamIx)]); - % compute all fields + resultGUI.phaseRBExDose{i} = tmpResultGUI.RBExDose; 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))); + 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 resultGUI.accPhysicalDose = matRad_doseAcc(ct,resultGUI.phaseDose, cst, accType); -if isa(pln.bioModel,'matRad_ConstantRBE') - - resultGUI.accRBExDose = matRad_doseAcc(ct,resultGUI.phaseRBExDose, cst, accType); -elseif isa(pln.bioModel,'matRad_LQBasedModel') - - resultGUI.accAlphaDose = matRad_doseAcc(ct,resultGUI.phaseAlphaDose, cst,accType); - resultGUI.accSqrtBetaDose = matRad_doseAcc(ct,resultGUI.phaseSqrtBetaDose, cst, accType); - - % only compute where we have biologically defined tissue - ix = (ax{1} ~= 0); - - resultGUI.accEffect = resultGUI.accAlphaDose + resultGUI.accSqrtBetaDose.^2; - - resultGUI.accRBExDose = zeros(ct.cubeDim); - resultGUI.accRBExDose(ix) = ((sqrt(ax{1}(ix).^2 + 4 .* bx{1}(ix) .* resultGUI.accEffect(ix)) - ax{1}(ix))./(2.*bx{1}(ix))); -else - matRad_cfg.dispError('Unsupported biological model %s!',pln.bioModel.model); +if ~isa(pln.bioModel,'matRad_EmptyBiologicalModel') + if isa(pln.bioModel,'matRad_ConstantRBE') + + resultGUI.accRBExDose = matRad_doseAcc(ct,resultGUI.phaseRBExDose, cst, accType); + + elseif isa(pln.bioModel,'matRad_LQBasedModel') + + resultGUI.accAlphaDose = matRad_doseAcc(ct,resultGUI.phaseAlphaDose, cst,accType); + resultGUI.accSqrtBetaDose = matRad_doseAcc(ct,resultGUI.phaseSqrtBetaDose, cst, accType); + + % only compute where we have biologically defined tissue + ix = (ax{1} ~= 0); + + resultGUI.accEffect = resultGUI.accAlphaDose + resultGUI.accSqrtBetaDose.^2; + + resultGUI.accRBExDose = zeros(ct.cubeDim); + resultGUI.accRBExDose(ix) = ((sqrt(ax{1}(ix).^2 + 4 .* bx{1}(ix) .* resultGUI.accEffect(ix)) - ax{1}(ix))./(2.*bx{1}(ix))); + else + matRad_cfg.dispError('Unsupported biological model %s!',pln.bioModel.model); + end end for beamIx = 1:dij.numOfBeams resultGUI.(['accPhysicalDose_beam', num2str(beamIx)])= matRad_doseAcc(ct,resultGUI.(['phaseDose_beam', num2str(beamIx)]), cst, accType); - if isa(pln.bioModel,'matRad_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); + 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 From 9a0c1112498042301b655ec6cd0a07393716ae4e Mon Sep 17 00:00:00 2001 From: JenHardt <84377305+JenHardt@users.noreply.github.com> Date: Mon, 16 Jun 2025 17:12:34 +0200 Subject: [PATCH 63/87] octave fix --- matRad/4D/matRad_addMovement.m | 38 +++++++++++++++++----------------- 1 file changed, 19 insertions(+), 19 deletions(-) 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); From ad938d312ec8c0f8fac32a9706c1e5754485146f Mon Sep 17 00:00:00 2001 From: JenHardt <84377305+JenHardt@users.noreply.github.com> Date: Tue, 17 Jun 2025 11:32:22 +0200 Subject: [PATCH 64/87] replace replace function as it causes porblems with octave --- .../+DoseEngines/matRad_TopasMCEngine.m | 106 +++++++++--------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m b/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m index c4ef4e001..f11033a75 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,7 +18,7 @@ % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - properties (Constant) + properties (Constant) possibleRadiationModes = {'photons','protons','helium','carbon'}; name = 'TOPAS'; shortName = 'TOPAS'; @@ -38,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 @@ -157,7 +157,7 @@ 'Scorer_RBE_MCN','TOPAS_scorer_doseRBE_McNamara.txt.in', ... ... %PhaseSpace Source 'phaseSpaceSourcePhotons' ,'VarianClinaciX_6MV_20x20_aboveMLC_w2' ); - + end @@ -182,9 +182,9 @@ 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]; @@ -193,7 +193,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 @@ -465,7 +465,7 @@ function writeAllFiles(obj,ct,cst,stf,machine,w) return; else end - + %% Initialize dose grid and dij @@ -496,12 +496,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 @@ -645,7 +645,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; @@ -671,7 +671,7 @@ 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(); @@ -702,7 +702,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}, ... @@ -710,27 +710,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'); @@ -905,7 +905,7 @@ function writeAllFiles(obj,ct,cst,stf,machine,w) end % Tally per field - if isfield(topasSum,'Sum') + if isfield(topasSum,'Sum') if obj.calc4DInterplay || obj.MCparam.numOfCtScen > 1 if strcmp(tname, 'physicalDose') topasCube.(['phaseDose_beam' num2str(f)]){ctScen} = topasSum.Sum; @@ -1071,7 +1071,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 @@ -1232,7 +1232,7 @@ 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); @@ -1295,12 +1295,12 @@ function writeScorers(obj,fID,beamIx) matRad_cfg.dispDebug('Reading doseToMedium 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 = replace(scorerTxt, '/Patient', ['/Patient' num2str(PhaseNum)]); - scorerTxt = replace(scorerTxt, '_physicalDose', ['_physicalDose-matRad_cube' num2str(PhaseNum)]); + 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); @@ -1354,25 +1354,25 @@ function writeScorers(obj,fID,beamIx) fprintf(fID,'\n%s\n\n',scorerName); if obj.calc4DInterplay - for PhaseNum = obj.MCparam.Phases{beamIx}' + for PhaseNum = obj.MCparam.Phases{beamIx}' scorerTxt = fileread(fname); - scorerTxt = replace(scorerTxt, 'Alpha/', ['Alpha' num2str(PhaseNum) '/']); - scorerTxt = replace(scorerTxt, 'Beta/', ['Beta' num2str(PhaseNum) '/']); - scorerTxt = replace(scorerTxt, '/RBE', ['/RBE' num2str(PhaseNum)]); + scorerTxt = strrep(scorerTxt, 'Alpha/', ['Alpha' num2str(PhaseNum) '/']); + scorerTxt = strrep(scorerTxt, 'Beta/', ['Beta' num2str(PhaseNum) '/']); + scorerTxt = strrep(scorerTxt, '/RBE', ['/RBE' num2str(PhaseNum)]); if contains(lower(obj.scorer.RBE_model{i}),'mcn') - scorerTxt = replace(scorerTxt, '_alpha_MCN', ['_alpha_MCN-matRad_cube' num2str(PhaseNum)]); - scorerTxt = replace(scorerTxt, '_beta_MCN', ['_beta_MCN-matRad_cube' num2str(PhaseNum)]); + scorerTxt = strrep(scorerTxt, '_alpha_MCN', ['_alpha_MCN-matRad_cube' num2str(PhaseNum)]); + scorerTxt = strrep(scorerTxt, '_beta_MCN', ['_beta_MCN-matRad_cube' num2str(PhaseNum)]); elseif contains(lower(obj.scorer.RBE_model{i}),'wed') - scorerTxt = replace(scorerTxt, '_alpha_WED', ['_alpha_WED-matRad_cube' num2str(PhaseNum)]); - scorerTxt = replace(scorerTxt, '_beta_WED', ['_beta_WED-matRad_cube' num2str(PhaseNum)]); + scorerTxt = strrep(scorerTxt, '_alpha_WED', ['_alpha_WED-matRad_cube' num2str(PhaseNum)]); + scorerTxt = strrep(scorerTxt, '_beta_WED', ['_beta_WED-matRad_cube' num2str(PhaseNum)]); elseif contains(lower(obj.scorer.RBE_model{i}),'lem') - scorerTxt = replace(scorerTxt, '_alpha_LEM', ['_alpha_LEM-matRad_cube' num2str(PhaseNum)]); - scorerTxt = replace(scorerTxt, '_beta_LEM', ['_beta_LEM-matRad_cube' num2str(PhaseNum)]); - scorerTxt = replace(scorerTxt, '_RBE_LEM', ['_RBE_LEM-matRad_cube' num2str(PhaseNum)]); + 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 contains(lower(obj.scorer.RBE_model{i}),'libamtrack') - scorerTxt = replace(scorerTxt, '_alpha_libamtrack', ['_alpha_libamtrack-matRad_cube' num2str(PhaseNum)]); - scorerTxt = replace(scorerTxt, '_beta_libamtrack', ['_beta_libamtrack-matRad_cube' num2str(PhaseNum)]); - scorerTxt = replace(scorerTxt, '_RBE_libamtrack', ['_RBE_libamtrack-matRad_cube' num2str(PhaseNum)]); + 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 if contains(scorerTxt,'### HCP Tabulated ###') @@ -1380,7 +1380,7 @@ function writeScorers(obj,fID,beamIx) end fprintf(fID,'\n%s\n\n',scorerTxt); scorerInString = {'tabulatedAlpha', 'tabulatedBeta', 'RBE', 'McNamaraAlpha', 'McNamaraBeta', 'WedenbergAlpha', 'WedenbergBeta'}; - for ixWrite = ixToWrite4DInterplay + for ixWrite = ixToWrite4DInterplay 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)); @@ -1450,7 +1450,7 @@ function writeScorers(obj,fID,beamIx) end fprintf(fID,'s:Sc/%s%s/ReferencedSubScorer_Dose = "Tally_DoseToWater"\n',scorerPrefix,scorerNames{s}); if obj.calc4DInterplay - for PhaseNum = obj.MCparam.Phases{beamIx}' + 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 @@ -1473,8 +1473,8 @@ function writeScorers(obj,fID,beamIx) if obj.calc4DInterplay for PhaseNum = obj.MCparam.Phases{beamIx}' scorerTxt = fileread(fname); - scorerTxt = replace(scorerTxt, 'Tally_DoseToWater', ['Tally_DoseToWater' num2str(PhaseNum)]); - scorerTxt = replace(scorerTxt, '_doseToWater', ['_doseToWater-matRad_cube' num2str(PhaseNum)]); + 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); @@ -1502,8 +1502,8 @@ function writeScorers(obj,fID,beamIx) if obj.calc4DInterplay for PhaseNum = obj.MCparam.Phases{beamIx}' scorerTxt = fileread(fname); - scorerTxt = replace(scorerTxt, '/ProtonLET', ['/ProtonLET' num2str(PhaseNum)]); - scorerTxt = replace(scorerTxt, '_LET', ['_LET-matRad_cube' num2str(PhaseNum)]); + 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); @@ -1700,8 +1700,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}); @@ -1821,7 +1821,7 @@ function writeStfFields(obj,ct,stf,w,baseData) nBeamParticlesTotal(beamIx) = nBeamParticlesTotal(beamIx) + nCurrentParticles; currentBixel = currentBixel + 1; - + end end @@ -1890,7 +1890,7 @@ function writeStfFields(obj,ct,stf,w,baseData) 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); @@ -2082,7 +2082,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 @@ -2091,7 +2091,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 @@ -2238,7 +2238,7 @@ function writeStfFields(obj,ct,stf,w,baseData) 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,'.'); @@ -2261,7 +2261,7 @@ function writeStfFields(obj,ct,stf,w,baseData) 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); @@ -2714,7 +2714,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 From ced961e86adac6908972023d0bcaf0520b01639c Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Mon, 23 Jun 2025 12:53:35 +0200 Subject: [PATCH 65/87] fix test for optional inputs of new plotSlice --- test/util/test_plotSlice.m | 1 - 1 file changed, 1 deletion(-) diff --git a/test/util/test_plotSlice.m b/test/util/test_plotSlice.m index fed895e01..50e6b66be 100644 --- a/test/util/test_plotSlice.m +++ b/test/util/test_plotSlice.m @@ -103,5 +103,4 @@ assertTrue(isa(hIsoDose, "cell")); if ~moxunit_util_platform_is_octave assertTrue(isa(hDose, 'matlab.graphics.primitive.Image')); - assertTrue(isa(hCt, 'matlab.graphics.primitive.Image')) end From bb8ec019e15a330a12ef010bc1d8ad68d2e4e4e0 Mon Sep 17 00:00:00 2001 From: JenHardt <84377305+JenHardt@users.noreply.github.com> Date: Fri, 27 Jun 2025 12:58:33 +0200 Subject: [PATCH 66/87] added RBE test for regular and 4d plus a small bug fix --- .../+DoseEngines/matRad_TopasMCEngine.m | 21 +++-- test/doseCalc/test_TopasMCEngine.m | 90 +++++++++++++++++++ 2 files changed, 102 insertions(+), 9 deletions(-) diff --git a/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m b/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m index f11033a75..da41aacee 100644 --- a/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m +++ b/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m @@ -246,13 +246,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 @@ -294,8 +294,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 @@ -1380,7 +1383,7 @@ function writeScorers(obj,fID,beamIx) end fprintf(fID,'\n%s\n\n',scorerTxt); scorerInString = {'tabulatedAlpha', 'tabulatedBeta', 'RBE', 'McNamaraAlpha', 'McNamaraBeta', 'WedenbergAlpha', 'WedenbergBeta'}; - for ixWrite = ixToWrite4DInterplay + 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)); diff --git a/test/doseCalc/test_TopasMCEngine.m b/test/doseCalc/test_TopasMCEngine.m index dd10d5b92..a3dff06ff 100644 --- a/test/doseCalc/test_TopasMCEngine.m +++ b/test/doseCalc/test_TopasMCEngine.m @@ -74,6 +74,54 @@ 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 = 2:numel(radModes) + switch radModes{i} + case 'protons' + RBEmodel = {'mcn', 'wed'}; + case {'helium', 'carbon'} + RBEmodel ={'libamtrack','lem'}; + 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 + 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_TopasMCdoseCalcMultRuns numOfRuns = 5; radModes = DoseEngines.matRad_TopasMCEngine.possibleRadiationModes; @@ -138,7 +186,47 @@ 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'}; + end if ~strcmp(radModes{i},'photons') load([radModes{i} '_testData.mat']); [ct,cst] = matRad_addMovement(ct, cst,5, numOfPhases,[0 3 0],'dvfType','pull'); @@ -151,6 +239,8 @@ 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]; From 8c08414dfc716b38a02e64499a3819d4b75e0861 Mon Sep 17 00:00:00 2001 From: JenHardt <84377305+JenHardt@users.noreply.github.com> Date: Fri, 27 Jun 2025 14:47:30 +0200 Subject: [PATCH 67/87] octave fix --- matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m b/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m index da41aacee..19ef5ddf2 100644 --- a/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m +++ b/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m @@ -1362,24 +1362,25 @@ function writeScorers(obj,fID,beamIx) scorerTxt = strrep(scorerTxt, 'Alpha/', ['Alpha' num2str(PhaseNum) '/']); scorerTxt = strrep(scorerTxt, 'Beta/', ['Beta' num2str(PhaseNum) '/']); scorerTxt = strrep(scorerTxt, '/RBE', ['/RBE' num2str(PhaseNum)]); - if contains(lower(obj.scorer.RBE_model{i}),'mcn') + 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 contains(lower(obj.scorer.RBE_model{i}),'wed') + 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 contains(lower(obj.scorer.RBE_model{i}),'lem') + 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 contains(lower(obj.scorer.RBE_model{i}),'libamtrack') + 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 - if contains(scorerTxt,'### HCP Tabulated ###') - scorerTxt = extractBefore(scorerTxt, '### HCP Tabulated ###'); + 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'}; From c8c5f5fcafc2f554d5353b7c4ca49e6ab93b69cf Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Wed, 23 Jul 2025 15:25:08 +0900 Subject: [PATCH 68/87] Fix GUI warnings and add missing GUI update in example 2 (#857) * fix conf3D warnings in GUI * fix issue in pln and stf comparison * typo in conf3D check * Add GUI update in second example --- examples/matRad_example2_photons.m | 8 ++++++-- matRad/gui/widgets/matRad_WorkflowWidget.m | 16 ++++++++++------ matRad/util/matRad_comparePlnStf.m | 19 ++++++++++++------- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/examples/matRad_example2_photons.m b/examples/matRad_example2_photons.m index 7df0c7b73..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. diff --git a/matRad/gui/widgets/matRad_WorkflowWidget.m b/matRad/gui/widgets/matRad_WorkflowWidget.m index 7cdd9fbd4..e7416a803 100644 --- a/matRad/gui/widgets/matRad_WorkflowWidget.m +++ b/matRad/gui/widgets/matRad_WorkflowWidget.m @@ -278,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'); @@ -287,9 +287,10 @@ 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'); @@ -301,8 +302,11 @@ function btnLoadMat_Callback(this, hObject, event) end % check if dij exist - if evalin('base','exist(''dij'')') && plnStfMatch && ~evalin('base','pln.propOpt.conf3D') - [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'); @@ -416,7 +420,7 @@ function btnCalcDose_Callback(this, hObject, eventdata) dij = matRad_calcDoseInfluence(evalin('base','ct'),evalin('base','cst'),stf,pln); % prepare dij for 3d conformal - if isfield(pln.propOpt,'conf3D') && pln.propOpt.conf3D + if isfield(pln,'propOpt') && isfield(pln.propOpt,'conf3D') && pln.propOpt.conf3D dij = matRad_collapseDij(dij); end % assign results to base worksapce 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 From 57817d719ae7086f3f82916d95e3a81aa2e1f689 Mon Sep 17 00:00:00 2001 From: SimonaFa <138221228+SimonaFa@users.noreply.github.com> Date: Thu, 24 Jul 2025 07:30:06 +0200 Subject: [PATCH 69/87] Plot Slice function: title implemented (#854) * Plot Slice function: title implemented * Plot Slice function bug fixed * Some changes from branch update * Fix title usage and some plotSlice cleanup --------- Co-authored-by: Niklas Wahl Co-authored-by: Ahmad Neishabouri --- matRad/util/matRad_plotSlice.m | 114 +++++++++++++++++++++------------ test/util/test_plotSlice.m | 25 +++++--- 2 files changed, 90 insertions(+), 49 deletions(-) diff --git a/matRad/util/matRad_plotSlice.m b/matRad/util/matRad_plotSlice.m index 8e328d59d..8dc9b96a0 100644 --- a/matRad/util/matRad_plotSlice.m +++ b/matRad/util/matRad_plotSlice.m @@ -48,7 +48,7 @@ defaultDose = []; defaultCst = []; defaultSlice = floor(min(ct.cubeDim)./2); -defaultAxesHandle = gca; +defaultAxesHandle = []; defaultCubeIdx = 1; defaultPlane = 1; defaultDoseWindow = []; @@ -61,10 +61,11 @@ 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(gca, 'type'), 'axes'); +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)); @@ -77,55 +78,81 @@ 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) - -parse(p, ct, varargin{:}); + +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 -lineFieldNames = fieldnames(set(line)); -textFieldNames = fieldnames(set(text)); +% 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, []); +% +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(p.Results.axesHandle,'YDir','Reverse'); +set(axesHandle,'XTick',[],'YTick',[]); +set(axesHandle,'YDir','Reverse'); % plot ct slice if p.Results.showCt - hCt = matRad_plotCtSlice(p.Results.axesHandle,p.Results.ct.cubeHU,p.Results.cubeIdx,p.Results.plane,p.Results.slice, [], []); + 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 @@ -134,17 +161,17 @@ if ~isempty(p.Results.doseWindow) && p.Results.doseWindow(2) - p.Results.doseWindow(1) <= 0 doseWindow = p.Results.doseWindow; end - [hDose,doseColorMap,doseWindow] = matRad_plotDoseSlice(p.Results.axesHandle, p.Results.dose, p.Results.plane, p.Results.slice, p.Results.thresh, p.Results.alpha, p.Results.doseColorMap, doseWindow); + [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(p.Results.axesHandle,p.Results.dose,[],p.Results.doseIsoLevels,false,p.Results.plane,p.Results.slice,p.Results.doseColorMap,p.Results.doseWindow, lineVarargin{:}); + 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(p.Results.axesHandle,doseColorMap,doseWindow,'Location','EastOutside'); + 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); @@ -154,7 +181,7 @@ %% Plot VOI contours & Legend if ~isempty(p.Results.cst) - [hContour,~] = matRad_plotVoiContourSlice(p.Results.axesHandle, p.Results.cst, p.Results.ct, p.Results.cubeIdx, p.Results.voiSelection, p.Results.plane, p.Results.slice, p.Results.contourColorMap, lineVarargin{:}); + [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)); @@ -163,7 +190,7 @@ if ~isempty(p.Results.voiSelection) voiSelection = visibleOnSlice(find(p.Results.voiSelection)); end - hLegend = legend(p.Results.axesHandle,[hContourTmp{:}],[p.Results.cst(voiSelection,2)],'AutoUpdate','off','TextColor',matRad_cfg.gui.textColor); + 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) @@ -177,34 +204,39 @@ end %% Adjust axes -axis(p.Results.axesHandle,'tight'); -set(p.Results.axesHandle,'xtick',[],'ytick',[]); -colormap(p.Results.axesHandle,p.Results.doseColorMap); +axis(axesHandle,'tight'); +set(axesHandle,'xtick',[],'ytick',[]); +colormap(axesHandle,p.Results.doseColorMap); fontSize = []; if isfield(p.Unmatched, 'FontSize') fontSize = p.Unmatched.FontSize; end -matRad_plotAxisLabels(p.Results.axesHandle,p.Results.ct,p.Results.plane,p.Results.slice, fontSize, []) +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(p.Results.axesHandle,'DataAspectRatioMode','manual'); +set(axesHandle,'DataAspectRatioMode','manual'); if p.Results.plane == 1 res = [ratios(3) ratios(2)]./max([ratios(3) ratios(2)]); - set(p.Results.axesHandle,'DataAspectRatio',[res 1]) + set(axesHandle,'DataAspectRatio',[res 1]) elseif p.Results.plane == 2 % sagittal plane res = [ratios(3) ratios(1)]./max([ratios(3) ratios(1)]); - set(p.Results.axesHandle,'DataAspectRatio',[res 1]) + set(axesHandle,'DataAspectRatio',[res 1]) elseif p.Results.plane == 3 % Axial plane res = [ratios(2) ratios(1)]./max([ratios(2) ratios(1)]); - set(p.Results.axesHandle,'DataAspectRatio',[res 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(p.Results.axesHandle, textVarargin{:}) - set(p.Results.axesHandle.Title, textVarargin{:}) + set(axesHandle, textVarargin{:}) + set(axesHandle.Title, textVarargin{:}) end if ~exist('hCMap', 'var') diff --git a/test/util/test_plotSlice.m b/test/util/test_plotSlice.m index 50e6b66be..9aa1e82dc 100644 --- a/test/util/test_plotSlice.m +++ b/test/util/test_plotSlice.m @@ -7,7 +7,6 @@ function test_plot_ct_only load BOXPHANTOM.mat - figure() [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSlice(ct); assertTrue(isempty(hCMap)); assertTrue(isempty(hDose)); @@ -17,9 +16,9 @@ end assertTrue(isempty(hContour)); assertTrue(isempty(hIsoDose)); + close(gcf); load PROSTATE.mat - figure() [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSlice(ct, 'slice', 91, 'cst', cst); assertTrue(isempty(hCMap)); assertTrue(isempty(hDose)); @@ -29,12 +28,11 @@ end assertTrue(isa(hContour, 'cell')); assertTrue(isempty(hIsoDose)); - + close(gcf); function test_plot_dose_slice load protons_testData.mat - figure(); [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSlice(ct, 'dose', resultGUI.physicalDose); assertFalse(isempty(hCt)); assertTrue(isempty(hContour)); @@ -44,9 +42,9 @@ assertTrue(isa(hDose, 'matlab.graphics.primitive.Image')); assertTrue(isa(hCt, 'matlab.graphics.primitive.Image')) end + close(gcf); load helium_testData.mat - figure(); [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSlice(ct, 'dose', resultGUI.physicalDose); assertFalse(isempty(hCt)); assertTrue(isempty(hContour)); @@ -56,9 +54,9 @@ assertTrue(isa(hDose, 'matlab.graphics.primitive.Image')); assertTrue(isa(hCt, 'matlab.graphics.primitive.Image')) end + close(gcf); load carbon_testData.mat - figure(); [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSlice(ct, 'dose', resultGUI.physicalDose); assertFalse(isempty(hCt)); assertTrue(isempty(hContour)); @@ -68,9 +66,9 @@ assertTrue(isa(hDose, 'matlab.graphics.primitive.Image')); assertTrue(isa(hCt, 'matlab.graphics.primitive.Image')) end + close(gcf); load photons_testData.mat - figure(); [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSlice(ct, 'dose', resultGUI.physicalDose, 'plane', 3); assertFalse(isempty(hCt)); assertTrue(isempty(hContour)); @@ -80,11 +78,12 @@ 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 - figure(); + hF = figure(); doseCube = resultGUI.physicalDose; boolVOIselection = ones(1, size(cst, 1)); [hCMap,hDose,hCt,hContour,hIsoDose] = matRad_plotSlice(ct, ... @@ -104,3 +103,13 @@ 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); From 089ebe28a1481b05db5e30e9dd4df773f137c2e3 Mon Sep 17 00:00:00 2001 From: SimonaFa <138221228+SimonaFa@users.noreply.github.com> Date: Thu, 31 Jul 2025 13:52:22 +0200 Subject: [PATCH 70/87] plotSlice function: minor fix for empty variables option (#859) * Plot Slice function: title implemented * Plot Slice function bug fixed * Some changes from branch update * Fix title usage and some plotSlice cleanup * PlotSlice function fix for empty variables for consistency * small fix regarding dose window in plot slice * revert changes on axis call (use the sanitized axesHandle) * Update matRad_plotSlice.m dose window check in validation --------- Co-authored-by: Niklas Wahl --- matRad/util/matRad_plotSlice.m | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/matRad/util/matRad_plotSlice.m b/matRad/util/matRad_plotSlice.m index 8dc9b96a0..0ff7e4a56 100644 --- a/matRad/util/matRad_plotSlice.m +++ b/matRad/util/matRad_plotSlice.m @@ -68,13 +68,13 @@ 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)); +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); +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); +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); @@ -128,7 +128,8 @@ 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)); @@ -158,7 +159,7 @@ %% Plot dose if ~isempty(p.Results.dose) doseWindow = [min(p.Results.dose(:)) max(p.Results.dose(:))]; - if ~isempty(p.Results.doseWindow) && p.Results.doseWindow(2) - p.Results.doseWindow(1) <= 0 + 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); @@ -206,7 +207,7 @@ %% Adjust axes axis(axesHandle,'tight'); set(axesHandle,'xtick',[],'ytick',[]); -colormap(axesHandle,p.Results.doseColorMap); +%colormap(p.Results.axesHandle,p.Results.doseColorMap); fontSize = []; if isfield(p.Unmatched, 'FontSize') fontSize = p.Unmatched.FontSize; @@ -255,4 +256,4 @@ hIsoDose = []; end -end \ No newline at end of file +end From 103bbce9ba45d4925096067352b806e11cb174e7 Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Wed, 6 Aug 2025 11:32:24 +0900 Subject: [PATCH 71/87] make the analytical functions in the Bortfeld engine public --- .../matRad_ParticleAnalyticalBortfeldEngine.m | 204 +++++++++--------- 1 file changed, 103 insertions(+), 101 deletions(-) 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; From 0f89330f579eccf5308cbd2881a44c74c7fffb19 Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Wed, 6 Aug 2025 13:20:45 +0900 Subject: [PATCH 72/87] avoid cutting of rad depth on beam for fine sampling engine --- .../matRad_ParticleFineSamplingPencilBeamEngine.m | 2 ++ .../matRad_ParticlePencilBeamEngineAbstract.m | 14 ++++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/matRad/doseCalc/+DoseEngines/matRad_ParticleFineSamplingPencilBeamEngine.m b/matRad/doseCalc/+DoseEngines/matRad_ParticleFineSamplingPencilBeamEngine.m index a3cfc87dd..46933b543 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) diff --git a/matRad/doseCalc/+DoseEngines/matRad_ParticlePencilBeamEngineAbstract.m b/matRad/doseCalc/+DoseEngines/matRad_ParticlePencilBeamEngineAbstract.m index cd0a00d78..7c16593c3 100644 --- a/matRad/doseCalc/+DoseEngines/matRad_ParticlePencilBeamEngineAbstract.m +++ b/matRad/doseCalc/+DoseEngines/matRad_ParticlePencilBeamEngineAbstract.m @@ -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 @@ -418,12 +420,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); From 228f95b8360fefa1634dc525fa2660dba23e5cf6 Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Tue, 12 Aug 2025 11:07:56 +0900 Subject: [PATCH 73/87] small sanity check in weights initialization with warning --- matRad/matRad_fluenceOptimization.m | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/matRad/matRad_fluenceOptimization.m b/matRad/matRad_fluenceOptimization.m index e11f88cbe..640b43e7c 100644 --- a/matRad/matRad_fluenceOptimization.m +++ b/matRad/matRad_fluenceOptimization.m @@ -297,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 From c67b2a9ef7a791c761ecb2d050fbbcd9648f6310 Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Tue, 12 Aug 2025 11:08:52 +0900 Subject: [PATCH 74/87] we need to allow more index computations in fine sampling engine --- .../matRad_ParticleFineSamplingPencilBeamEngine.m | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/matRad/doseCalc/+DoseEngines/matRad_ParticleFineSamplingPencilBeamEngine.m b/matRad/doseCalc/+DoseEngines/matRad_ParticleFineSamplingPencilBeamEngine.m index 46933b543..fbe852d5a 100644 --- a/matRad/doseCalc/+DoseEngines/matRad_ParticleFineSamplingPencilBeamEngine.m +++ b/matRad/doseCalc/+DoseEngines/matRad_ParticleFineSamplingPencilBeamEngine.m @@ -68,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); From 0d68a2da48529edfdaca46cb01742d609c36400a Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Wed, 3 Sep 2025 15:49:05 +0200 Subject: [PATCH 75/87] add test to capture PlanWidget behavior overwriting the isocenter in the workspace with nan if multiple isocenters are defined --- test/gui/test_gui_PlanWidget.m | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/gui/test_gui_PlanWidget.m b/test/gui/test_gui_PlanWidget.m index d397da83d..6fbe44a0b 100644 --- a/test/gui/test_gui_PlanWidget.m +++ b/test/gui/test_gui_PlanWidget.m @@ -110,5 +110,34 @@ 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); + + %TODO: Test Buttons \ No newline at end of file From 8c2bc04008db69af0bad3bd8897e917da10fe636 Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Wed, 3 Sep 2025 15:58:23 +0200 Subject: [PATCH 76/87] isocenter will only be updated from the GUI field if not set to "multiple isoCenter" --- matRad/gui/widgets/matRad_PlanWidget.m | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/matRad/gui/widgets/matRad_PlanWidget.m b/matRad/gui/widgets/matRad_PlanWidget.m index 03cf3139e..c92133aff 100644 --- a/matRad/gui/widgets/matRad_PlanWidget.m +++ b/matRad/gui/widgets/matRad_PlanWidget.m @@ -982,8 +982,12 @@ function updatePlnInWorkspace(this,hObject,evtData) 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'); From a44bbc18f916ad7792608b98a7bd300185e66aad Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Wed, 3 Sep 2025 16:30:52 +0200 Subject: [PATCH 77/87] fix octave compatibility issue in changing gantry or couch angles --- matRad/gui/widgets/matRad_PlanWidget.m | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/matRad/gui/widgets/matRad_PlanWidget.m b/matRad/gui/widgets/matRad_PlanWidget.m index c92133aff..939daa5c5 100644 --- a/matRad/gui/widgets/matRad_PlanWidget.m +++ b/matRad/gui/widgets/matRad_PlanWidget.m @@ -970,7 +970,9 @@ function updatePlnInWorkspace(this,hObject,evtData) 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')||strcmp(hObject.Tag,'editCouchAngle')) + objectTag = get(hObject,'Tag'); + + if ~isempty(hObject) && (strcmp(objectTag,'editGantryAngle')||strcmp(objectTag,'editCouchAngle')) if numel(this.parseStringAsNum(get(handles.editCouchAngle,'String'),true)) Date: Wed, 3 Sep 2025 18:29:09 +0200 Subject: [PATCH 78/87] more consistent check for empty hObject / objectTag in PlanWidget --- matRad/gui/widgets/matRad_PlanWidget.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/matRad/gui/widgets/matRad_PlanWidget.m b/matRad/gui/widgets/matRad_PlanWidget.m index 939daa5c5..18321f695 100644 --- a/matRad/gui/widgets/matRad_PlanWidget.m +++ b/matRad/gui/widgets/matRad_PlanWidget.m @@ -970,9 +970,9 @@ function updatePlnInWorkspace(this,hObject,evtData) pln.propStf.gantryAngles = this.parseStringAsNum(get(handles.editGantryAngle,'String'),true); % [°] pln.propStf.couchAngles = this.parseStringAsNum(get(handles.editCouchAngle,'String'),true); % [°] - objectTag = get(hObject,'Tag'); + objectTag = get(hObject,'Tag'); % Returns empty if hObject is empty, no check required - if ~isempty(hObject) && (strcmp(objectTag,'editGantryAngle')||strcmp(objectTag,'editCouchAngle')) + if ~isempty(objectTag) && (strcmp(objectTag,'editGantryAngle')||strcmp(objectTag,'editCouchAngle')) if numel(this.parseStringAsNum(get(handles.editCouchAngle,'String'),true)) Date: Thu, 4 Sep 2025 00:52:25 +0200 Subject: [PATCH 79/87] Tissue selection from GUI bug fixing (#852) * Add getTissueParameters function to bioModel class * Update PlanWidget to allow biological tissue definiiton * clean up matRad_bioModel by calling the validate function from the model itself * add biological model tests for getting available tissue parameters * sanitize tissue btn callback * update the tissue table * test tissue selection button --------- Co-authored-by: Niklas Wahl --- .../matRad_LQKernelBasedModel.m | 18 ++++ matRad/bioModels/matRad_BiologicalModel.m | 7 ++ matRad/bioModels/matRad_bioModel.m | 58 +++---------- matRad/gui/widgets/matRad_PlanWidget.m | 82 ++++++++++++++++--- test/bioModel/test_biologicalModel.m | 29 ++++++- test/gui/test_gui_PlanWidget.m | 15 ++++ 6 files changed, 148 insertions(+), 61 deletions(-) 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 cfd18ddec..283648ce4 100644 --- a/matRad/bioModels/matRad_BiologicalModel.m +++ b/matRad/bioModels/matRad_BiologicalModel.m @@ -251,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_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/gui/widgets/matRad_PlanWidget.m b/matRad/gui/widgets/matRad_PlanWidget.m index 18321f695..912668689 100644 --- a/matRad/gui/widgets/matRad_PlanWidget.m +++ b/matRad/gui/widgets/matRad_PlanWidget.m @@ -1468,6 +1468,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'); @@ -1476,9 +1477,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 @@ -1507,16 +1526,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'); @@ -1525,14 +1570,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/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/gui/test_gui_PlanWidget.m b/test/gui/test_gui_PlanWidget.m index 6fbe44a0b..ec0fd963e 100644 --- a/test/gui/test_gui_PlanWidget.m +++ b/test/gui/test_gui_PlanWidget.m @@ -138,6 +138,21 @@ 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 \ No newline at end of file From 6a7a48c0cd08e9c6a3ceb2b7c282c4d4cf3b0856 Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Mon, 8 Sep 2025 17:18:40 +0200 Subject: [PATCH 80/87] More consistent forward dose calculation and new dose engine tests (#869) * consistent treatment of weights in pencil beam engine for particles * stf generator does now consider airOffset * updated test dataset * add HongPB dose calculation tests * add photon dose calc test and adapt dose engine for tracking weights in bixel struct * add tests for fine sampling dose engine * disable dij sampling for octave in photon engine tests --- .../matRad_DoseEngineBase.m | 16 ++- .../matRad_ParticlePencilBeamEngineAbstract.m | 15 ++- .../matRad_PencilBeamEngineAbstract.m | 33 +---- .../matRad_PhotonPencilBeamSVDEngine.m | 4 + .../matRad_StfGeneratorParticleIMPT.m | 32 ++++- test/doseCalc/test_FSPB.m | 124 ++++++++++++++++++ test/doseCalc/test_HongPB.m | 79 +++++++++++ test/doseCalc/test_SVDPB.m | 58 ++++++++ test/testData/carbon_testData.mat | Bin 19663 -> 16440 bytes test/testData/helium_testData.mat | Bin 78217 -> 76186 bytes test/testData/helper_testDataCreater.m | 56 ++++---- test/testData/protons_testData.mat | Bin 12362 -> 24713 bytes 12 files changed, 348 insertions(+), 69 deletions(-) create mode 100644 test/doseCalc/test_FSPB.m create mode 100644 test/doseCalc/test_HongPB.m create mode 100644 test/doseCalc/test_SVDPB.m diff --git a/matRad/doseCalc/+DoseEngines/@matRad_DoseEngineBase/matRad_DoseEngineBase.m b/matRad/doseCalc/+DoseEngines/@matRad_DoseEngineBase/matRad_DoseEngineBase.m index 1041224de..1fcd1da6c 100644 --- a/matRad/doseCalc/+DoseEngines/@matRad_DoseEngineBase/matRad_DoseEngineBase.m +++ b/matRad/doseCalc/+DoseEngines/@matRad_DoseEngineBase/matRad_DoseEngineBase.m @@ -318,13 +318,23 @@ function assignBioModelPropertiesFromPln(this, plnModel, warnWhenPropertyChanged end end else - resultGUI = matRad_appendResultGUI(resultGUI,resultGUItmp,false,sprintf('scen%d',i)); + 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_ParticlePencilBeamEngineAbstract.m b/matRad/doseCalc/+DoseEngines/matRad_ParticlePencilBeamEngineAbstract.m index 7c16593c3..cef7bd772 100644 --- a/matRad/doseCalc/+DoseEngines/matRad_ParticlePencilBeamEngineAbstract.m +++ b/matRad/doseCalc/+DoseEngines/matRad_ParticlePencilBeamEngineAbstract.m @@ -50,8 +50,6 @@ this = this@DoseEngines.matRad_PencilBeamEngineAbstract(pln); end - - end % Should be abstract methods but in order to satisfy the compatibility @@ -164,6 +162,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; @@ -942,7 +949,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; 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/steering/matRad_StfGeneratorParticleIMPT.m b/matRad/steering/matRad_StfGeneratorParticleIMPT.m index 432011f58..b32cd9194 100644 --- a/matRad/steering/matRad_StfGeneratorParticleIMPT.m +++ b/matRad/steering/matRad_StfGeneratorParticleIMPT.m @@ -19,6 +19,7 @@ name = 'Particle IMPT stf Generator'; shortName = 'ParticleIMPT'; possibleRadiationModes = {'protons','helium','carbon'}; + airOffsetCorrection = true; end properties @@ -44,6 +45,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 +54,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 +82,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 +112,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 +176,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))]; 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..9024d2661 --- /dev/null +++ b/test/doseCalc/test_HongPB.m @@ -0,0 +1,79 @@ +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); + + 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); + + 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); + + + 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/testData/carbon_testData.mat b/test/testData/carbon_testData.mat index 097b548286c5237e6b5137d92a673c86dc30d536..f9224109fc1cb23496443cb443a977a5e2e8683e 100644 GIT binary patch delta 12889 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 Gf&V{0rF~)m diff --git a/test/testData/helium_testData.mat b/test/testData/helium_testData.mat index 3f3aba6c6651dc2832853f03b4db6d1c0e14b87a..8d70fa274dd088cde7b53d9451236c3da09d818e 100644 GIT binary patch delta 73050 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 ALI3~& delta 75092 zcmYJab9^4(6ZhRTwr$&KY&L8f+iGLuO0tb@Hb%q7wrw@GeZ|Jpet*w>-~aCJ>zvt{ zGw;uwIlF1IP+>bzv0QkX=4NDHOkK#x_{cao`Pq2*KXH?BuyJt3eWi>ug06NkK?I1N zK78=VI9-5OQ&T4zyc}dIrect4(k&@I`@XatPE%0eZ*9>W$+n9ZF@c1T_^n7~%*LIO zvHY|yS4+F0q`2<4RdXzJ;kHWrvdPxWjSt&)rpQp+&kGm+!GlM(YZmaj=e5UmdbTT< z;;5F{jJRhRe1b-UcKb8=-k46FDHE`R!#pE^(;EYIuc#!Gw_i(L*nH~5^+|}Dr6go6 zc^ktN67#f#yNGhUAQHLCyk+}L>D9q!EOWns`kqn0E6(_f+hg<*Y|1!s`Fk?^t!rk3 z0(*~R86&JJQHc-iGf82=A_qfcQkG%c_>mhG)yi$cZm6Rvhki7auG5!w_eWq=xH@3Y z2du}gI(uvci@WxIE5c_t{T*}j2T>tNLWCh|+cYZ~Lrmfe_V?+wbI!-gmjf z?gC<^cLXmw<(Sg36&}908<3^ocurMELKW3+;ji{u%;c-`o zx=onGSgxuFtgCPcm48Gn23B5t;x~V0 zl%yaipCYiGLeHGSKT7=(Hq`a%@d4F@p^%(#_7crv(|D}MW(?@o8|$6*fVsY8%Idw= z`k{GAU@%Ddzxk5472ARut3NVUBY0{>ebS2X_Ky5=L62~W+_0mIwS!C4N1pmXz~1Z6 zau{y6Ut{Nn_>{x?M2>QK7z2S9Dux%0ju-Z|%9sly7EU1Efp)-1FvFoc%0;0|(u@pK zyNU4Lh@lYpzsB@YdSJyElN)qJc)*Wdpse>hc}KHeP?zBdgNyyrtU{%&B5ACG=B)xE z9N}5|&6|icH|6T2WL(58UBon9B#>J2VeLe}APu0chUYpMF`bKZ&}I8YVtvjf(kgWfv{Rnu`ENm@R%jtP{s3D&=ND@hHu z5sb2ABaNtrYW~k<%!}c+5BRX+KF;=?L+zMp_UBTes}VTO?(IYE9Td+tfZSa{#HlVr z4eN&JIX1yin@OA}ImPt^?oapXp>`IEdk$o?^)WIhdtse5DSNW+)&%S+YV)MHc9PuR3maQu;`q-LkLI!miPSo&YOlS=w-vDqMY1Bc9_B z-oAVh7$Y(mb!YqKX!}dzjzQE#Vb0In>`>dgMqBxMJx=UAfB8gTevGN)4{ho9A*=!K8Zn8wbqLhSI zApO*|#u|G0cj>WOjR^jcy!6voS9fv%e~~Mc$OcyuXMkX+>-bRZjiZV#l!ytQ08uA^w*FmR~LD$eiE^y?hb7=X*CRlJ+C4EYZ6;lum;*wVed zx+~GZtWn9{fo2^yMt ziUTWYsoSAuWa^%^59~vB#+R~wj9MwONkt3x&hqAD8XXxxUX^tCVU8`~m?&zlCZ?_? zp~}^Z33y=q#lz&L_RZjjKuUysHiGI<3BmZ-$jrqcwO?Cs(G-~4yfKJPK`Rf0$)h!L zc~~S}0mab-#bG*dVu*E;TqbNVrtFxtY=q8LNTdH)fpSs}@ox-_*wDY6I$QLkqB8xL zc6XbM-chE!(WDoYEZZ2q{q}GU*Rl=(CE8y|<-$quxX7U1^!>HrW86P>M3Uo$8{>u1 zMfO-pQo@+B;q0)HV6vgBvLny3Lvpes*0RGov!mMY5HoeFF!ef>b;*_WeD@BFsm9m6 zz!x4gIPK+mz{wIFMG%L;lGIL=@Kh63oD;X56VsoQ_}@x#4$Ohp$pf%;D6x#K0vfDB z?hgGcJWwX{3ICGBD-YLe2f8iP4==DA&p#J0cni-@^MZlKm}9*wJv9c}0~c&8g0d;# z9h1NtG|L+>+T{Pc38pdR{UZrf?gxZCG=zM(@NIIz$QrtktKuKw#ep&xbOibEB)vO_ zKcYPlzT~4mZBw^J1_2KrNViqX`uWQSHEep_PDK&^Tjxv@oOl|Iy6N5lVXo=` zRQ*%a{IE({%%abj8!0q}pD`!pFe_pA*L#f!nah}%Z5WtOshCe`h4iR|R>_1E;sKIE zIaJPqkVe6EAtrjoGCMAI@fJL0{S112JsN$5SeEl-ZW7LAkFQUu)IM#tp7+&>H`T#6 z?2=E5>Q8v|QIT=dG;);ppDAPHDOqJH^=F1SW`_7@hURC6E@y^3XNLaF$kayZ{)(`` zyqCoU;%VNtuIx-=xvI;#svB>nY=A8|QV{RbAPq|_NgDHuq|QS@-NlcU)}Ww*prl^` z7Le%=i`~(}4ONvcQ#P9?GhL0p{16NLU<>>Xe)%o=t-o@vXezW=a#x0PRsQ6vtmUfg zAC3 z=X3#RSVy>!xbS~rUsy{QB*q_-#F^OfXS^@ZnpIkOp3TwUG#kX*{`|?o~@-wSE@wV|RQ$lERzHHRTw+?8 z{!jLzfIk(1i9~jAGy3Q=27ZE8oq=suy#Z{!{!fJN275U#o`kYVcjhC+9tSX7U=g4_ zB9+I`7w8D#ZQi;uxV|S0Sy%HCdA#DjME&qT6yLi4as9g`01q_n!W6s2@XGMZ(9w-6 z(Isj->#U&;J;kVJ-L*l^hP~Hl=r@n^g?TVd%tPS{(+TANpk-2CW|kbR33EvSIbI5mB;X z{Dt^mVCJSq@uf!a5)HyNOqQ(Zhm`#j<;k0A1mtu+eVllITp$PAxn2&U! z;&hP|bm1m+(HL}LJYP}vzCvJsg-`klt^O7H&sWG#(;twh;i9IY6sM8Kw-7}hC_nh6 zhCFKh2!b2@Ue>K6Nrv$?bl|Vw@%wf-FdU&Z;{vUlq6t3_2(1o6{vszvy@ZRogfhTU z91U6>g0xQ*|CZQI`+${+4*35=Y--?es(%SwF9p*8#y^$rlI}BNy)zcUaKJfmAfayx zQ(Nu9j><-s_4vW{i!y`|F!ks`?Em_adMoPqZ*h1b#>o}Js?$XZ0VhfOSID3!p?+7x zSfhv0{nd^lN0`gcU9PL594Z(8jQDbil-63<+)ey*(@$>*h!Xs-$lH|?-j#v)XP21v z|8xSWj|^$ROrcv%;XkvO`u~|_(_>-N{U4~ilurAFPX{6X7QsXV=zZIC(c~zWjwlD0 zoW9+a56HL<=qV2PVgFeUet`VD6juj`SNl1wZl;jWJrM2lNpx~QE$1);EqVX+MtBhS zpWgjfG2=zC<3+GuVs{LT7z6fWpoWK|4j+4VS?>uit5|rn9(Jxt7%E z`#O4l=M5PV2BU$AY{NisNK1f6MS#abfJe^n0m!*rq#bTbKoZ|@Oo`f0i;zxtqD^XXQS(lBVnHH%GdyW(-c%M?8R1TMk;4%1j)&bbyXFDuNL&{`4zpVa zuU|)L3AB{Bl+O)iR^}(Wl96YYYG$Rbqv^(5$Go6ld@B3kIK@l+0p(t4x%x#4?7aJ%YR*g}Vsvrbdn$M5pm_pN>p(+sZbgqvCp(@pR`CZaKwt=bqJAbNlh^ zv|TpO(>TxbMDp51@*1NLM&YYlP7B1J*MpI>qH|%_>z`{Nrejut!_tJY2t!$9WV zv7)(|2>z116KS&-;?bw+A>nvtu9_4xRN}Rd<%)HUC1pZft-o*54iE2&7Q|&5McPUD zTcCB)t?UQ2LB)1_)|lhxeC;&@u7ygh z>boy+K2`YqwAJ3|>NpKEIE)eo4kLW|TW8nX?J|b-zwcXETBPU*64|TXzH|LiCjNwU zqrFzfoMBZzt(4w;(C^t~mrh_?{rmjK#HpO+xgUNr`a3F*xaJ0|p#^KlNUNc=y?i@2 zLG#uyi^ZJctK}g{F?s^^c z_3Y+3)9^s>?KorN{`vgcGi%}Cx!wBL?3#qDX=VkC!4LTFc{mEj!OK?ug&yeRQ$);0 zLCICpux-H$#t|KdK*$^3kFO5hR!t_v{%Dc6p^{h)#sV0Z*zpc%f628$PTaZ4P!kvM z?D69xR&ra+aT{uC57OUGfreGsUracrhG}2)+59bny+dQr{E-(!l_gDK-6&u}Vk3J$ zcWY<#dg_U?RBZ9rxU{+HNpYWV36tb}pn+*~oAakn%jMGnx)2%;LzO%rA=%LEyHP6e z0;=&s@c$dt?cYe-WGLkLA#u^Qydl>^V*f@$xI~LlhT@c{K>u4z*9Ct1%Q60Z#C1u{ zu>nwOK=T9Kn^tfPdbr|ZBy*@3lN&jTAq5F2)}Qio|JPqzyd4A3dVTjpU4Iej+2m&; z2n=8zmL9pRO%V%CVM=WMFvdgSQRt&;3!wMFRb@vt@F4w5mi3En@dp$1mkS6SQ}|@l ze_YZqMgE8uTCWvC24yNFegQAsZ@Cygi5?AiI*<}NNJ1n*5WQoK=P*f_M-XWgEp3H_ z*cPcdMN*juY0!Ie2n~VUJ`L+Ljrt0(e}KHiIc0;pbLfNPHKxj7M_|E2){N}diu{|R zU4QxCnu>s`(CfnVgU7oH?XRpr!1cj<2P8b;{A}{izTmvN6nN{>$0K~N4|0NKPJ$VE zt^K;ZAoDG|cBZ3V?zuyG*cfLY#BSEeja)D);>P5p(TgRVghz2H)3hO5E8) zle|*=DOJEMn-m=~Ff3cJFQKC-cDU8KifUsua zXaO_I?0r*RCn%2W_Fl5S=wJt_vVZ+vYcsWK+GoF;mfUvZAxB zV#pM))1q;FNS~3+pB7~JD>~U`c?O9$?mVTsn{&TWI;W2OU(dP0soSJwGlosWKXGn9SbU9&7a?MNOuKPfxy5F0rs ztUML=yaxK|(pjaJyeGfsv_WS75%DBUuKThce4A*yD7_TCmT1fSWhr>_IJ*s(aaq_? zRih5Ln)lX#+#A=P5%zTc_iP5t$o^@8&4lIcm-Sq1+YOPWBJ8EYD->kah64k0(7#8r z21f4c)EW+IpETo;OV1TOWm*_lrZtzMt&@N=54oji?GZPo6Fwaui;A)9%lE4#t@Ysd za!V6W)MPuc<^U(iy|>}BgjQOf%CdG^_lma6qPtViR>gA-^SiWVntFlns#%Sh47KSD zv!Nq(Vz%PycG>@jHR{8Pa|Q9OLmiQ}H*J=)rK^gXaZyCO+XNMauRdRdL;;BZ@;T5% zkelJ1v+Z9aveD#Y0y&7eY^vi1AqXkVN-%3xrN>_hey)mx&Caw7#5Bwg>N@&#c z6i`~TIVMqU)G&8b2RzeDDV|FxK(cEx>g_=z$Hr23amzix)lYpX;mY5~is~Aue=^nM za5Q!Wkn2Om|rePJ>4#OkGvI~q88dzn5Q#W>%>I!C7TH68>* zT3@UiZoaE0Sl8ez#L)wT3Psk+A=ee7&GY&2w~w8lc73@I0Q^dH%W175&~QAc72a~~ zX0p@PfBmPPd?o=?b``4vl8{$kZqN>Q^?sNxBPfFI(tnnX*+??5>W!D$%DK}c?#yxJ z^sN7Kdn^N9^{Jpg4EL27r*$$CyPXq~c*Fe*hQRal8Qbs%t2fTM_0z8}{+}DuUC;Ea z{HqR$e6GS}fcPe`O?|~|<}1aYvxt@uNq>*oC1-<9SNQE5%Zc%CkURa2kLSzMe`i{7 zn-JWgbljeD*YsM9iNWsfutX5=K)^Koxm=gZ>ynPgZ#|Z!=$hYWvy#$kQy+m=t~2T4 z`+Sc~@om53jWERy=Mw^}RA-%cLaOHV*mC!leBkpO;I4RPqKOdl1qQ_~u}NAh>Dm`# z+lSZ&AtpR9#<^8+HJ+lhpu7%gLKZs(Hbg4RO&-(t?GZFdxF0Ufd49f9F^aiNYQKnj z=BaB1o{aY9qc#-8Tc2X2c=v<5XCz@88B|78;8{Bp=FwNMJ>a@Z`x1P?hIg{WeHLD9 zTzl^ZwCesekze3E^K~A_t}hBIC*4Y;WhryuOK3W7IOOeEVRfzCkD#P|Wp%e#CJVM; z9;q?-R@}@|It%jCY|-+Vu~7b@8O)X`uzRuwsth$CRq(AnFHnfH6fP{%o&I|HYalF7 zDwm#uZeDPB$ENhc5MOn)RWr?@;^3ugn#F(%NEb2t`JCr5o$GwNL!7YF_mU-M*GOzC zb$DRW=zn5^#Y3XQ!hhk_v`E5pb)N6u6Qo7pTQ|k3Kt(d+#`XBE9`5s-qu1Cr8T~$1DAKb^)YQcztU_S=P)A5Bfgxu&*TY4j47(< z*G=URuhoK-<_KKj>}Sp$CCa-*YM%ZEvL5w-)!JOwEJs_Z9Xy+F;rF1K?lZk^T^sb6 zwfu%&xFD%^++e(3Zy$;;j8e3syNd2RAM-@h_^RLMYMWrSuR<1Wo355fJbqgTSW6uj ztsnI)Yeuy5GfU{53rufmTwohJ^Lnc9=>P1o);`gd7fR?ub+=nIY_B%t4fblyYEVS` zBbg%;(+s~|&zp{3NrHeGWmr8{hu@~yq(!|%m?{kqjvP2MeUc89jZ4sjF)*a{ZE(Fu zux}NE@?A1D)3`?N(QA%TO?++wY8EaJoho*;ULWu0vjO+lM}ebBpv38I>**Sm{@{iA z=HOj*dm9?|D%h&GpYhgOo7d_c=3jr{G&PLp&*S^xU=)_7YziG_oZB+xjaNkc?Q%^$ z8~t&iq%66vBuBzpy(0RW^>Bs!Lh_j$W4Lk6I2DZ!$HBx@a! zz%1ncch<9q)90-zf=nPT?Nx%UO=a(%EE7E^bUyv0*=b95uOZlu8(xuQ%peIUC;2QT zraJ95j>hhLXYFq&V3nPk$I#!zaOTB6q`yN^vy^eQ7 zE`}YHIb6%xP8X^Z^f$X)eX_L9K9QfRkYgiQkd4%@o102L&D!2@ZRQ=tDdnVoHq?9# zv3$6ts!B?(1#y7#=lSvx0lQnW6F-iPB_4_2Ey%89dBnS%OzHML(42RziZ)_0OZ?Y~!DUeSBgQewDQ}N8az?aM^>&{f z$QHFHd=ueuV%8g_3#QM`4QOn&j}|6Om`@ z9u)A>^9Ek7T|(&LC&U^@b#G9r7O2E0f<4AbDLg@d1eByl+qN_|wK%%A!HBd7^bIS~ za16GE|Lk292%|2(+vVp`V4Ma?Sz9Tt==IB8Yb);jDcvZH<(~`80O_Bb7*%dv3)OgO zAy2OBKC{3E-N5+Oo*T~CDI(wR?Xk33QtCz0-0QKu_O+I$GoJTEZ$5^R86`iscK3T> zW|N*MRK4%`RK2&0-sNQw{HauJ3|=#5zP%X%d&dXo0B(>do74sQ8Ea=yTeWiRoeVD2v( z8q$$ttb~kz>u;5{y_~BE$)>Z; zx>UsWF*v>9jJlI1*H&iUBL$>Qo9PwclfVzl zLXZ_xo>U5jNQ|#5VvaIY0Vk(D{0x;55Yh0xrIsvmK!0U17Ih%;LKppOvvQTfi&l~{ z%23BAszGvEMk?I)R?d-E9i&tjuE(^*^7$9`jt=pxcf;TtzU?ygn*fXe7QDGH@-j*Phb)ZTuXmJ>?Y5B zD=Ml(7f^Q)8>S?7+0E4~J!co1%WHi~oaSS2Qg-L|#J3FXOI87Y7W^bFa;msPZ(tRD zj)F6nM7Kzba6U^X#MbZ2Ut=8vJziy`8hyH0wz zex1m_p&HKgIuvkD33fkvvhv#G>(nWRK=?A-;Rp&scPi?$F&<7Q3+d9g#YGyM4w_9? z8ojZdHoL2RaQK1%9B&ZN6?p2Pwd1^&Y?Ma8PM`IwK0r2i$;jF?x}N)yLavHbd4c@q z`*FWI+kdcAXgXD?CW7GoqSNz~8JI>!{W#YPvqY21Nw z7$}r)NjZ|!dw(1LCERB5tg*SCw-XH|*N7wuHz+5$YE&f$h==gQWx|0v{T#A-uM3P3 z%R%@0zm4~BC^VTDem?}LKKLs;+T}wNH({vE<`BTn_(yBI2|7AN#`Z1;5@n6!(q^z) z@EV#qbUeJI2(Jr-#1y5x51-)dyAoVqb+@UpSMsDT@Q+y&TyJ-`m5MxB){&!pPE~+md z&85r{!#jBEx3=PiK^V7+a5cu`v(1?eGg71>bxQy z@RuwvG=nGNU4nOi((mPr(Rja*PTq|PiBIYc2&qaBl^}>KjUQZVulbT4a!0zRXGTb* zj>1d=uA_Uj&4m{$RfW-9Z%tH0FKPD(yP8rzW<3s*;df#9`6zV-N?*Dhc8>=$9eLhc zSm!a$BOafexa|kwJ#}NQ-P{cAG49kcz$e~s>3&k?`zP@G%8BLheg83C0S#W;gGJrh zwIsxPa<@J}Kh5v7?4IhtSm5-!K^@EPTYy6P_l_Wpmd}c0ff=sSsPw8&lPa33abbYQ zvevkT#O{f7C000(`02NwN4P;Wbi>MG{l_b3k#XSrAtEfm(X34zEu>>S(4Yflxs<{r2rAqu{Pkam!}1`Gj@U!Y4iZv93MS%`i9h$**k_bqA(Bbf$crR;9O zf4)c)w%S(`@4nq7N^=&huE$23AX#4X# zlM^sk%weC0=k>X4^yE&=0J4rp>0-*wfYR*sO{?xz3A3*4x^E(AVE=PChI&3wy9iUh ztoPf+HPxSKPS#sBG%_#kGpXrA=bBGU5prMWfjG)t49wl6?_~DDydc<2&M9Lzwhyihe9`{^?1ODk%gJtH3D0wvvd*Y(%5KK5E_kB^5BUY? z%+3;ED_OLczB$y0&14@-I1d3(8dvqhSfq(Ue9O~r#EsPFW;<6Nd47zhH9aLADg@pYlk~X zuct|hymxF0ixXg(j&_tqP8*?4?31(G8#6d5yB^d&Dce}CzUR+;c`dv56rQ~HS-AGy zNJK4&!X{w(?60*wMMC4pMZCWM^vEo}94T7*h=)p$ClZhJnKUFK`Ggqq(=(Z=;M7cq z%ZEt(k%;XH)b3%|R}c1C^hKhLbl*D6kdGI09qTGM#VbIqh401`gq(_LMC}V7n$5#c zZiah|hPI~h(un+k+ogWRxMj5*YTK6}MP|sl%t%f<)_~*l7-I?~jHYC-&O+O9$ik}t z-jr#gSbC)66pf*ewB4&km$2eS81J9+da~eP#(wvT)1m9M@s-AThvmqptBJpv9hRJ4 z-&k(I`n~~(Ue!lZn>BJO?w#J`b)P&_8wfNc5Yj6X%e9~nVA~U=h=2R>6?RY0mj+ez zPKVj%wzI7jcLq~(e(1}j(CBEBKr6#Kdbc}+n^WcYI?mdRb-YSLTOYlD<%I6 zKSb@+{8c2Ww#$(eR5MctnZ+{`kwxYBW7Anvvipb{Z~x0(-B}q7DdT-0|4)WRONs-G zU;4;bqZJmFyT5}}bu2$n9@dboR|I7UeMLT$VQ>1VfU= zPQHF2Fc^3uzq0r&6JfivKa=E%H~)Cwl>hu^`02h4^lrK?^Zv=6QQ(sCKK{^w1~fx$ ziHps|0v34P^u#wKFJ%t>YE)_QIo*?jIB-xANJbz~A%~=Opl)hL!ix0w8GN8~5EVDZ z)2EWCI1Y!HZFsH{NGIx7VDf*l=_W)`3F_K~+AR3~8LhH%!P@~u0u|1A2c@tl>TzL? zZo0<=_c%Pd)WwVd151y9nL+g+YMs(PnvQTQ>>gpWl0e=eD;IXdi?wT#Hx8981EIPI zsL<(%5RBQNxP7Av>`GolK>Z4)Ya+J#O_wPCVcd|CASDVz>}7G-@KO`QxaQICFz7eD zkxs3+mcb1SUR;qGukK8)jKJyPrv(=}o~8x^b~fbFqBiP$t8W+1>CsEZ*EWk-SXtRr z$~DIKzvr!bvivEBRRnLTSXs5j?ip> z{3uZl3nE4loQ=lg0Ci12!?ju0Gze7$xkPy$((VBnEFZz&2rnRQ%gH0P-XwupzyPDa zy^9g-`eJ-Z*4BLQm?E*SrU>5=fl>j0fQYFjL~6ycnjl(wD4Pz*+0 zgJU@~!-aZZ5~EITsCqQ+k?cGjP_SZMi*C+}vDM0Bq-`D|ZfOQb>P{qPe;Eyn-6=lOpz!8$chfHgE7UWdbyp}?M~jK$ zABe&&F-ysT_cHdnxBKUulkbV-!Oh3l?W@*F%D>l&PmYcYiz)^}`IyOQRqgyWSf{?| zo?8X4V&a%lwdj(qSHGd3Q8D&mQhWb7za(*C0TjQEiVH}_pBA%BIfz|m?L%f7V_4-~ zVsg&dqa&QIdkg`Nk+Ar|%oe{2W8Q6D_rJ4fU1#5Br7hgI&b#}PPMit9Om8fZPJEFP zCU5kF5)%Pt-6U@DF3P9R`El@3dl^|aw-G~g(%$B21Y+QpDC_c0GD{o`s(c%>`4@_W zZ6j4;zd-gQDgD5shGe3_SzZkv3VsDo9>$1JcK;@Vth@$3Qf=n!ag!+x(6k1tTU z64T5bD(n)%9n&8w$NbKh`d@5o&shDp)1R|OQ$BXuYW3`?Vs?`2_U%2_SX?fAWg*Q~ zYH&|2LbB8RjyDx9pQESZIVjO#GIiO8X1V^PVPFC}l9rCn((-k{&l5kF?iJ7xwuQ{qbIoV=## zQ-_+0JmJfU{JP2HQFgm}Y&=Eoa9m%)T*Y<#)`u+!n`J9CdRuBJVd3HX?4~)vlij=B zeQeoqk1s!Ie_n0JSCYE^5V!sWLTOz{@f1^s0h;vf{ z(YnQ(rb@QYFE_ppwkMklo#=y~%Pd8n?XAn6@1MOZC-2ihzD@GHs(I=kTiP-&XI^cJ zJ20g8z@`#fYs>4#Srkp0C1vXI?`VKrKlUHe>;2x22$HX6A%ac%r)h7)!jKqSb1>C? z3d%Y-wcKEF{;@KHCtC%2WVgZ>mIFjCGLzW{(s5g1p7TX+6zqu82L_1pQieixs#>n%J zj1ptO(-TxdqaA@NB2K@fI+_A<$1Y!KL1*3`+~QK~>yHvjFydM&ffGs3jLHvuO+|wF<4PsF zqtLv zlCQfipS*F4)9=E|e}pmtmov`e>S$;g^rsCmknjx1gwN$(#$i5AiD7?ateVZIyE=rR z1Y*Fi`ZsHEYV)vlzgZ6ayX_4+hhZ>dlNMBvHB)FViZ!Q@9lt^5)xIUHhGbSv4r*xM zD8hQcMhbdM9YTj8W+Tq}KEPSo;4X>!z)h*s4itu?r*cs=Psz{#t=<#aRXdCGJs5bLv&4=Kal=LmZT8UshbEr-4~`YV_=O#BU~L8_D4BF)hgP$_m=emxS<+ka#a&?$P4j>qVQ1 zX9YhWp9Z$3q2ZEb#PFei{~eKPzkUqZ(oiCYpZ>NA7LGr>>~M}c11h6fl#zOSyDZ|_$?r?<+vG>SB`5~(yS(7!ij z!DQ}03+m5s4TYXn>gi=6rR#j^WsOrMXVqg{RS$&+9BGa_cfU;;cNY9Oi&dpBYb1&` z>>Wc0>aM_ymiTfI;{Ynv_|PG zi2@09yhlP%2P1Blnyvl0Ip)+{cZNDNiJ~YSd{%U|b-+-V^G5uOfY|*mmlSmcWXs}$ zeuM~cOi8)OM8hO2rajV^&wdn4=P}!ea*IM)CuTN%_p6=8%KKZ!VK`*SbnNztsvC&u z>p3i5f_sfvu14gl0l8ycc<(}1v8bf?@LFYhuxG67A_K(Wu z%fg9(D5B+-((A{f;}hrr%?@MX>AN?JjSAt5XY8Hvoa{{sFQl;*4wm80W^^jmI+G2L zX7+T7Ao8~0-|uv2O1w|F(+1`<8pT96b_ZQC7|~9<*nM`)`LF}v?bt_NF7!R18ovNi z>~DYY-dXY9?N3D>LyTWgO1>cdcz47)R2~!Vnk2`I@bBt@q zcEqAl;V?Oi;GG-hrQ!-TL3TJo^DhN(u@x{%LbvuytkRr84re)T*7mmSpPhOq`3Nyg z%qRwZe*JrfgvhZq0LxN9bcoi_^*Rag0OpG?g(wJ1yEn*Pp&qUJcQ1o1|;!lJN* z{QWhIWjD8(gksCPxHPSPNjZnYWB8_<-cAR9%xZq{vvDmb&Cak?k{L4Z(|xC4MKXl%XsM!iXJL2w zj}u9KDH^`iYuF~G3!H$s{u)9)_`S_|e)Dvb;kR*4GqWTZGZ z8h83Pp1L2ry-A)aRj<12lA3~(^jOp~-;xj}@p7J1&zuwmAU_V^qfh`5vwr-$1j%(V z@CUlTRNE4~bLM3QE#5;M=zR9}YNUU{Fl>B+&91g0yPpq5t73%5U={rA_H(V0u~7zo zHidI@27|ph``6JXkxGr?N+$G{+IdUz{zo=)snzhviGUvS_`y;sr!AxXx#xLEq9!vJ z@(QIw1g7DdhPP6pSY31=qb2?xen^KCfmHp@|95fkk@1c%+N7odPn##&00Yb~5r%00 z>HRZM7%RNhb6mafPQkcC&()b)fq_8VH>OEY))caFN=?|+y#ACYhhWF81*-zj_loo6 zBiLn#yttD*F*s$oOMER(gxmfz@JSYCEZX&wd*zh{nmXF0HQzdbg0ugpZurJ7r*A$k zQ>Znkepsw>j|>)L;|ix^O$XYJlo(q|$sxx=yjj;3oX~mxEOg#sTm9UCqhKE@OaP}k zhoG{APGI#13YAs*@?xX}WC>?4=-?&oANmK0SM~4)G+cpH{M8%FTp$^Sqxp^W*WS;n zodwXi>(@OaZ`W^tYA6Gt90s>7Xq4sSuK|j~25i5gttU)h1|QLbCwvn6Q`oidQd@M5 zVoh!5km_poSPhc*g^R9ev8+jJ3%ZL)tub5xP@PMRMMaa$;nPCCxoI|>YJ(eTh}QHb zX9(L(9_Lq7M@+e~t4OEMMjXOVMj#T^ox(I_QXjRoFzRDAXSoVigF8$VGl%x4Lk?YSXv?y;;@HaeGsH_fGwm=G>=u8#4wv z%1|N7a52l*6q}UP?#ul5-DjfrsAX@yId>OQ^Xr6xgb6IIb7j&HENID9zO*?Sp9`{m z7m?Bnl!Qod?L)v7@=zSCu8^!z6=Wj|&|^A;>d|!gmz*PnC!V*;c8=2Fb@9Sa-8}`5 z`oR!D8ooD84!T7s#f+={s^EmXtW; zioaRiBwT*Vwg_s*;g%Q|XjsjUj3v^9Pi4@lugA$Oc&`{gg%Tr~(wa%8bJOXU;FoBe zD=2f4FKq*Y=>DE26OA8vH!AwQ1*E}`JAogS}dXhk|XspOenD515I7 z30WQ_w)7KfaKXJr9t{tz<5@yiAP%CpT%*lo+a+h;sSl~LI!B#3C-SlFgWRjSaQ)hF zfb~;Me8*cX;-~D790F*tzeeb!Y)#bBZsr>LeW76|()HUDW|a{+N8fh=#2o-;2OtLi z6%#WRe(RCx!KG?L$ceejlcPCZ4;Cd%Lc9X)$ ztBoCtX`8^HT@yaUGTS+Pbs|vQd_sRFIvEOWt%!k3C=PC3cJ4P!Mr&;2({jTR0*nW0 zAbM1z<(QZ2NG!mH9IR#&8EBSz4XoD^x=m&_@vZo!b3$iWMmS`(N1l9=(5qygdsf0U z)}@UjjCGc2bX9*2m_DrkB6rENl{`9mRgUg%WJ_th?Whg)(0GQ;P#pboBFx+LT-AMU z%Hl4v<;3H^XGacB-0>>HSP*a)0#FyOHLJ2vb&OEApJrxBI-5UEs`lO)0IQquC{5Rd zMhDrW5)>?`h~sM+?)KYlj8UHM74EbS?JI$|=i;D=)wf2J#`o8v^M9u}`0utGgzuiD zm+#lY+u8SC0QlbPy)ES()S2>ra9{SGzES(O;*0z04z7KN{I}O!{rSlA=lOoH5))mf z1m&xp+_yq5@@3WnGeMwG;s0ao9fLcG+IHWWcw$WakCTaQ+xEn^?R0ETY)ow1nRsH` zwkO_s-e;d(b!wjvr>Z~QRd+S2SJ&#buIu{AN+~Mk7sOGBI@%3_W@(9ym|Twt(_&ci zf7uRqhG!W6jcf7IJneF`^4YY~=*pS2%b5hOd@nooCf~gInxZ9E>cg_$8IBZ2sWy83 zX_NeUl_lWk+8QblQKS4x=uOZ%cD-{)MVTCDLlr1I9Tse|VWCyJEnIjH90~K)04c`f zZL*(O7?y-LTv$}1(M3U)!2x6X!x2O~Up<$7s5jNLzwCC+<}Cct4j8lTrkV@Y@qc2C z>U|k7u+I~eWvM0x;R6#?R~%UIuN77_`zf`AwGM$tM_Vp6(M}vD!`)D`LP|Mx>O;L7 zqT|_0QKT?Bk9m3VX%L~T9U1nhiIr_s z$sEkzw^o?{<%xf0zirsZXrUQj1F`^t3YvS=3SCS&N88{ctN3YA&m+ zEzxG;Fupaz$~qA0Y}0eNN#LPq%Y820mO^yLT8n&nR%T=Ae}3=ZCc9dAe5#nF;!)_T z>8Pcs{?Zgr`1PI~GWXw0G|+ZZPNwf@scAC>h5!8zoz7E;`WKO`u_+r0KQzH$>PFsH zWR^(+HxuLHcgleQai&UMbmtE~m)dm&UY z*-L}p=0aYd?^AOPtI?j%n<-xl7D04XGBq?DuSYu^?mt3)Y6g?0E%2oQr6VF;Z3!*f z**v`*=r+o=ty{N}qe8DnD<9ty*oT(g{)ONv6JRyCr=TKO#xS8VhkR z`LKQmvCIm!M=ssm=5`!nTya`kV;Y9?O?tM|-6Z+jwXtaB7F9Y*v0tO?P!!EbZQugk zEW-J)#KP~T8lawLrj53A7`2>rbo$ zRIWh-t@Y(ZexfR_Z%GH|v|?;3|8C%+Atlr$OZT3F41l<1$wQ6D;eYv?w_vH&=ZAb0?vF1o@8Wji=AJJBGg^GI#Z(qtg)&}~ z!3~dR_ki8Rb(_P1yI&c%g3Fxn0nCXDU|fpx zo>jV=FIJpyhHBQ|S_-3<3T=1&r#Z_L9(6GVn6@?i{;Yp{B)dpEZQymvTw$(a;bkj4 zW&}>g9HjIrD10;oj>kltta&Ct-9-l+LFv1uWxBsrIyqE2UrRiB^H0d6=A{G5Qpi;4 z*;K2aHOb#nw4UtWqV?Z1(D%2lr!^$KkqD@uRW;d4c)yJIp@{DE2S1ve4|#9w`NvJA z$!>q+)Mpo0n)MWXKmv&5Z!zD|&UV8e767?)#M3Eftj$`Ab3AH=%cWE{XzLZw7SK5T zK9>DleFNUj-Lp$N)j8?JU73aG3oa?Qqde1-x5`UuYs6Fo%r2zt=zI+ohEC&Z2H!3? zsz-hcx+Q$wcUj5zGpBGI-9$ac1&71nN?W~i=n?fee&)y$Vc=;Zq1PnFhE0ob1e!u9 zkuUD8KW+8+7xOITr{`S=1eiG-lq$)Pv0iu*1#WcTxw}91O0wXn4kpMOg<2K5E{p!~ zZuR9pbxg7HLK1e;oh#mM6PL_$F1+(Zz-61=9D1+*3D(l;m@U4;@H17e8KDd%j=ML? zak6hin3OxKUEDDfJl(c+$Z%>U1b|W&8Zl~2k-t$AaTdhdPfd3I+0IN@YAW>wvY+yt z@xOk&PuKbmJ2DAPh8z${9h|=$=-}NSC7#ckyTShbQ1QNw37xAr&*{dO@S8wMt9Mf; z6PQD|uDY^_x7Q@G)@vTIQP5;>wRDlhc=cZC+1$3KOOk8I9h!NxBCSm_rTjZ5#Q?Htzez#*!!9yPf-0hW?coEf5f?-7o(_JY_>x>zM5< zDF22Ccjm17*Dgd@`fpRS7r-W9Ps_a8+lYbMG1R2#&NLjyI7VCR1~W{VQczgfDe$~_ zf^F&ix!^qcRLbuIVb{IEO^;$!t0vhTL4mdyJIiuX#aC56Q=Lw@toJ9#uwXnxOVTXNP%nL5&C+jj9iC$+IH#= z?7hz6bGMky59V9PC)QZa5}9TGmGnVEjWSpshcSbzCP=9#HHN)DZADPf*@v+MvaL1@ONq`GWl*o&IXin;=0+_E-0YJyImN z(9V3mhHPZw_b=s9+HZC;zGsr3=EtL6O3YYRqGT=$aMiKsksoS@1`pb7)2Ilhrua;R z%S}l_2_|cWB-_(*u#iwZvt2MN7ZZXkQj& z*cwUMF5N`Asu)Qu84XtxA!Wl<${&*|1H?oUXg`RFNBl^;eT}Xf;qiogSF(nT=^|W@P0G$q!}hKd;DENQy7zp~EyU-V(=IG_XVA zU=Xh=|F_}tZQYp6dhuKdqU}X&xolb%na};-WeePR4+T6Zy1~vbzEZ>KW@4@{@J7|e zh6l<(azPWP1xfhQ`ko*B{#k$3yR)lImRG;@X-;>z7C`$;K;YGRs=&R+Y*LO0-wltgqSdr(%3{ z#PC|Od}mHZa%-kLgC#v`#0LFfVwi=*5Pr-wOtOANYlHK#od6;14&oL~a9keD#@D;_ ziShS=834}8pFa1xR&-#-(^>P3bPwjzoLJQUEAe9D`oZ8E{kKrXNbU(%T>}M`Qx@^+ zYxN^7_nl>oNp}h!mKxjRE(m)MeJzHH#4iK4o+n~vB0_5#@(+8mn=Xv=_oo!}r!Q`Q z;UxJI-~_8+W5({B{4s~7TPgZO@3}TY#HB~`jDY)gUPF|^o<-Si?6*p=CZ#u|yys7K zSk60;_pB#)uNQCvu+IauWgkCOKAM%^CVxN6FGfS69fcqj;>w{N4T?$~3R7f#WE9<^ zop)ypEn`47R|#16L3p$F zffNDaAl#1jYYkHDi0+HTy^|up*gDz$jLS=_j^|K?d;09HH=osizqxZuuL|u;v@e^S#al8ZFA8E6 zx3M4Nxo>t{m6ui}U9pcB3U<}AcNmY>Xf~5EhgMO8*KS94)0g=lqI!{hm_GE+`ge^- zPScP1NMis03&3CT)D#AOgX}))|g!V%JPazYjmB3M8FJSmvJT9>~ zAW{{UiNcBR$h+q~{1yjJj42`)wua0}>?pbyHH;T8JaZ@!%qBkkH6E_c+$jiz(n{ynjDU z(3f}t$WJ~T@T+DI4D#K`rDxZrS=K8H&({T4HnN5fb3e2sZh?aXv}kJn^Zw$UNMXp>pE z9iL+TX+`bgT{;=-HYMN>HKY`s9Hn)AksmPjx%8*u0h=fp%}QGl9=wOE`)5D2PLQwP z#n(R@+)G|sv_NMvltEqN<-F}lK>ml-I;v6MS6TueXfZPQi#KyhWMxE|^NOKsX;u3< zPydqdo8M4oUG);c+qS5Aorxa2alJXyNp$TyQOc;?*zac@BqUmC!3JV^K$Tp3}{sGS0P;bctG{ttHEIMH~Tx{u2mu~D2t?HsBG#`!4`cJ zeX4h~%y`dT6yx2JV;DL#9BbhTvaJ**UbXmS{QXG(RPC60-Wkqex9`Ip)(u2 zO{-&4F){Q;RDwcDnd4`CGzkM%aS!;a*bsR3?h(ryAsou z7{E5jh_f3;{RBP48MR9tMe=o*BgTOz#DS%sBi#@L#~3q+vePla%-oYOb8EDVg^6l* z<4iiTt=Iy|+MJ_@wU5|SP!WlMlk{E1~oL`~^RJSh;KS zImyp3zfQ$--WqwpL(pBc5Idh;PpVgh#5g~RQ?vIIrQNB^WcBo_DDJRGMw-Vv6U4_!4y%hlQmSguygBKx; z?&6O}Wo4%Etokl`w9CU)|i%1@Ebr8gf|AlQ0ul!qx0 zXrWv>>|-QKSP`eOXrd0g?NK%aYzTy8^OCZUVlZVEX0y(|g1%+joVxPxX2O~(6>@3+ zWBJ*vQ=u%!j_0~Rj163R$AgU#uF1_OL5VqJV_ZH=Z57uVWW`oIw?W$AT)WY|*?A@( z(+w^at&K7QsJ|2lc>rb0V+pJX*~O+k7z$H66MKuXo~}3_z1$ zeI(a-jZzo85Tue}rPCS)feQ~mFSTr~Ln@x-239Va1xLSu0YeY=xJ2=~IbPze2ytAB zZDb*3p)-69r{nC$x#TH^wEZ@XD9vlP)0HGy_SvVC^y?es=yTT#ogaVU zNu)fi@gC^I<{bnXa`?yg`(=^98cA>bk!tN$6^03~<)0hL;C)svY|fhO@f>hQz3@J7 zlkjo%cZnekKxwE0i=8`aRWwSAV=y|vA)~j2Cpe`?`gik_ei(FoBJ zEp@w*kwCN;ly0y!LTK)Myo@`T^+mi)ct|Z5BK1cS+Nj*G-U#}7zJpLg1Qe{7MLM?oUkD2OEL<+kuHUCyBcC85>z%((UWT*n9x))6wZ^=RsG@QP`G4q z0U(135L9Kcw(gPbrilzdZf>uuRDA6Dk>)I$^Mo9PSawA(L;jWdkjP;2Ma2D9OD_H} z*dEKSaZ8sZ6E+?e+y>8`Dsy>lpDf^aUs+g`+n>%6{hu5i!V+8vH_xu?Be-LJ`sBAJ zf?o9kydK?<02w-|6F3y9@ocWp`+(2t2ms%raC9R*@=l|F7X!UpUSjXEkCVoXTB=a{ z(j{=BNyX#n{%dDY8ZymQSbOW;xTO7+=2B!;7>m|o^`Er{6FdU?nD4P<&{opKVa!tK zM?GJ-^?j}3Sa|?#wIUg=uA9QMp(7QuOB44QN%s z$-T2$?c_06MWB)P()v=MeSib*h655d&@Qu!)R*sxiJ1u?K6R^5FQ#5mtX)*&?K=l0B|-0p1||#m%IC z6t)Y>8GEXx+aXcpSZjJV7lPaVI?8#5%g`5>;otWrN7Pwn5kQ^E`f^U?;s26N632CF zXJ2QJ{0h&e^-S#pcI=XOyy?^(fW|e`^p#4+p3yn1I;kS)xBTI=CKB_u1()6#WxV#3 zTyd~E`JUi`Zmr<@0vLyA9Dpah!4hY9x~S0PyI*i?=c%ASD~wbGZ5y?SI%vf`SdP96 ziB+wIMUOgoiZ~b^1GYv87>-(Zy}OJQf_{1}SZ+i!9*8=@PEU4l{fl5Ikn- zqqT?C(LFoT1Mh*d<2th^qo7jsQ0a9VHMKL0t9uUvewUS|H9JJ(10-r?ipDP$hbAjy zBe<-!QB)3!1ydFTiec^du~XLPD(%ZJ<{St`6!V!q|AQd}dwMEu(g*Wo&|Fd|GLNiC zWNwv8$sp>Z+7Yf@92~x`DIaNv5S}Y&-_D#SBWCisNXYY&LYiiRNKQgOC2TYb=&zo% z`27u9w;O|(_Pgs7HlVvuhU;sszt<^y_Eq}!p%r!=ammN=_oa#j8glcc+93Pxl<`x| zpSV{Dm_uq?oK-$Be4&`b!6+!fl?cs$`S}EFmQbEDPME?}F^_8cPAFFsCl&kLQqX9H zxoU3^Pz^Ot=b(&D-`i44wj4u4zWd4?>7{26q=m^dF13G{F2GszacWjOD()j`)C+c0 zI}Gk464qk^mK)a~9>aw)mYdSLNZ6l94GH&fW#*lJB0Sn~^>O2`q-+W4neI{;_{|2MNiv#!q~XyYKRcdiAx zNrAh?OHTKz>30{7H6ghw6f_c!-6MQ8$4i^?Y$upOeuIEP5A@G@n#Uyg>E-d9X*0;E z<}hg}mJbzm^)>R?r$SPbB!@2hWd?$OYjj|3ICUi3X=B~7gymAt%D zpji(D&(j(^`iCAq=b204->G^#-q_H8x&Otk)x>x%auJ<(Wy5@N&X}3V@Tdwn~Tz{Kn z@X%*Ez0_R_iNW=%98iKl<`2k z_cn@Nga+FclXC(=$6x<2&3bk13Ljl}LCgD6qoYw56tBAgsOU^U!GB*}N3ez=te`t7vCv zIgyQ&P$duJb<85-y8v6s0>`+at7xCPxiBp8BB4;?;{LJg5SDZD@>9qF<3!H$C1)qi zFP3w62-GfSkQP3ZCHdLXONvF6FB9^(P2>s|VoRO4Y~*QJ2V8_{c`~C$D>Mzs zX&=i8?wjUt?L$r6>mBN`D+i>x?kS9GoZ=ef%%+w<;{=v`?pwr;N9!( z`eU`@t-FigCZ?tAk2?W^PYV6Z-+56%si5Ner(Lh9C!Qdl&39y)-$7MrH>80=O|z`O zW5XFZO<8Y(8!?sBpL~gJWrEe^+1@J%r64iiR>8f&R0tioU3`=zLPYe^gT7kXHEGsz zafLOqZw~Dzzjd*f-;5ns8=jA60$u-o?rhaa8bw~f=A&11i`;YVj%MYV{a^%oO2C$O z0tYo+_M{d;l}-X&C$84zWz1M0YmSF9i6(6BFQuYN-z=yVXM_$~2VLp6E`iu+}h^gf>7q<)GHrQ)=B5*Jd;QknlT7ft- z18FN-^MIKdr4Q|ZLlJ4e7l|$zer!G<7Q1uqzo@%Rlx_g(+2Vofs$|Nhz1?oB*kcxS zcZCLJTz|#C3~(GHxdKpV$f_-YX_e-YYdN;VRvNY;EqzbICJzCfc-a(#Dit-Q%uerMx%&;LROF!A#(Km zllXKARZ}7_#F|XB4nwm0OsvG5rRfYFO16*CMnx%Eg;*L0gQ0AG;n}=!%Jz^m%RXGZ zTs!1Bbo+SY-JCpZ)4hDW@9JXCDcG>-G|rD~=!dXW9#%$PtKY5}C&f+om62g6fkpAL+8^o4__ko=%Xb_C3+ZnXG}6i+xyi*NIMejr$#p=g}zgqj3Amz<7_9*_^+O_85vE0o>J8bq_}&xdfUxHjTqJvl&^xw?L#+^|hStAwl) ziCgLF{FBERL)=|$X;hQdWu@4<%m9i8*wWn%z7X#RRFHH%25e|QmL6?0xdW0k7DhhU zFzeYCK7Qr`pjUy8Oo7n7FJ&#Y+FVO?ny9$Xv}nMNJ)~-=O1)r`_<}@kcHa$W`Rzv; z-@DG*hwbyP9Ea{75=Dajcq7db#NV$N+U6$5rN0t~YN_WK!rJ?414hd;U`O%R(W{=7 z$jQEYQjwmz@T_}Oye@iF9BJH{aR=spb&cco{#5WKEqor`y7^E$`J9HUtk^)>=P=}~ zH~~!N>8W+I$WkOXSM2;`qqeZ&CcZ2Db>TL8=EEKow+SuIBH31L3hvnGz~?QpegQwc z|5-P?Uvu7pAMwgeYk2Ef)0UI8QITEGm80rY@vMdGqM7zijKP=5bId>c9lTVL??SD) zy3RWmeQ|TVbaVb(-6uNYd1uE-a!aRr#s%OZ#((C_P;Epv(&yPhX`H_Yqu}?zp(M$I zke`@Rvff!vh_erAoS=50S=`jk2URB`gM{@zuM`j3%MF9+!R z1u@e1^laNV?IL}6+OBkwb#KwtrDNOW@LUzFKuKEc8v7f@c3pi%M^Ob#G5S~MZ)+0; zFRqA|nph}LkdEXIILl9aaJLFWH%RZ*UVet3@@D`0Bsy$feO4UI*l};o9ThWhEn{~kd)Q8f-Qb;a@6G zgn9h#adEW)LnZ*v3X4g%$KL+828(jfjffkclD$rS!o^%lOS`Gg_*1|j3TNwS#pgXl zqHE8mUObsEf*pAmE^b~p{cc3@H>TIr!xG+~JT<(6tv{5L8@bW%>yK|5bQSI&w(qav zlOKAX@8py3b{-FIo4}_sh``p#=(|BOl=|UW!M*5AZ%Gphy~9mo{M7lmeU)!+Ij*HA1f#*dF2!CFnAs|@>(6&(ea)U@{FZdo4|F%?t7i1(j8&k7zsl&B zg+hn(k_T0i;_3F6R2{tWh)2-14Wq?aT5JKyEdXux>!V8)9Y@!Ox7}tdTbT6$HK8XP z`0_{lLzffB-8=wsne^~Poy&}$h%le?HQH{GT?zNAK?;3g8XQzF|DMx`lQ#Vsti1xN z_xv~S><|Pf;c&tFeijx7H)LqNW`*t;iK%tlUGgGVW z3eBkXFble2O1g@a;=}&Y`j9-4ZsMAshS&2d%`k+i+T7P4Ce5Pxt`r~b~8?C@%GNG0CJ|0rJp4N7DY-t%M|hRx_T;`&-mi@V8@ zE{Sj*5l!zV9IAK}ZS^fH9NW}en^OhVUG*aJka`uGNHK;ipk17T+<-tCZ&=Nj-Kbx0JhG!vjAK#Je3jvs0 zRDEV>pjCY_ZoP`7W^5eGrlz|Ydl>pTYl|aZ$Q<>OF1SU4K;B=^M)x?s^-JHE;%7Zz zXz<%Qx!gODq@Mnqo|Aj#rnAwkxCg*`Y&Kqza#B?^;>e<=E~@uOj2Zm-jfy%xSA|(t zENH;u*1#^$Wo$0U;-=X>u`y0W}$j&yLuE?NZc+3?o1eSx)947_T&xRzYcH;r5%r|-de0jS*$g-(Q^Lkz0m0#0E_aKjQ6bDrsa%tzY zEf-p52OU)6iH%${-c;qY0q}?8IJDI3l;r*_hL-oT4E0vG`Woc|q z{Loa~K*;)g$>onR4-^u?->ot##B|@`cf02P;kAA7s5J;+7X0exT@N%!J?22{>Uo>J zTFSrwU6d>Ge&bAGPdm6#=NmQMsr0*k*Dym=&hqJS1L-A9_}Mi)E@m3L5yrTS7oJIl z#(j&R{>Z19Y8SudDo;RQeW(tSTS1Z2kN(UbG2BU-z@NrUq(zT}M$UtLfAzw_X9(vcf%o z3g0wwCf}a`a~(Hi#g=R^S@K)h$HsNP_z1QUo`>&PAexl43qUSpYT)9bJ&PnSX2@N! zL5)RXKm^yZJ0j?xse#65p!8PH+#+;Q*U68hHdR+PzscNoP&Wd&ZIe@ClUEj)Zn!D4wm^GbO}ORuhU~QC@zEj zz-_+R^9m8(ofqB-bTlGVDQGZt-equyYg>s;NAydiExfyU(~Z-`&#n#l{k7t*5JXqP zuvHK)ZTMRR!Tj*AxB0xWz%gokVB;&Ken@O92v-kg8u(MnO0OBw%baf*Il)PlsokOS zT;q$!YTwn-fyD7F6&bX%yBUVoJ|HAbN+Zxh^xOmAYU!EIFUKTU4X?JG zkxiKUM)sZt1&x40n~>63rcKa`|DFf^;A!3$OnudwM+ZCouE|Ju4`m!o4mktfCg=c+ zt?jztA&V5;-zPWq6#dh!Ga^UK(wvq5&VmKrsmZy)kz*i{0n4_b$MxkfdT|AU`thq$ zA+U3|GMY>5(Vy(9iJh8_VjiQA*W0w%tckOGM5t(=r>Fe`?_}>{kTc4xuU0A64SluV zfBKd)syZ2LlDBQ2gyWaKo6f%@;zS7MKbGF}zNkx%X1wA2$gsa;oc<25456gP>b>_3 z1$`98rnOQtI&{Z4G!L}wrnNNduhBAGr~pyzID?-BTDf z^vsm+`!u0YQB3}br2Y)s`2IZm0}(t?7Z3?1Dv9Ngu&4{jw9}vPVpN|Pv0b9@yaP6Z z6rdY~#L|5v;r!ri8Sn8BbzV;zb>= z$|FFk-L@Ta){*t{|2HsZ$%6%0uxf|4&kmC*MK&pxU>-+lvsGf^E0RGFQN$4;5*DTW zHJa*y8P8ac|BJ{eU54zO!pn?eM1@rG^1kZLX=0N8&!iu5m9Saj60o8-T!f6(v4rL3ko}a`#3z?rp7_M z|B*aWYpcRWP~A)yoCfJUs@}pRZWnHI8P*aZKrEQ%Qngt(jNp1^J$@0v%o#JyYsBrV z*Uye9!b2-8H}owj?gc0y^3THtE~>46aoqRrOIL6TJOh}Tcl|i#oUBF4bcDWl*NM0j z+)n%Hp*OtpcHBX;fv!5&vw(gv@+#v6wZKd8Q?pQ0b^o6z%-tTQoGQLoa#m!jTlqSm z+ay{z*vS#Vyz3W&Y{Z6Wc&t+|Te)RX6&&1i#$OIo*dgJmXSZ>H*85wH*EF>oiWry- zl8kSkHX69&2m(9rO#fC}$Lz>-lEKtx5}CP(7qWMh9q=;7D&KPC6Cy9nhSFiGf|dD} z4xd*>72S@moL6^VQ7Pd9wl|KvuG*Ue*(TJBDJ)b^H0)};T_2*&aRIm$rgOuE5l7}t zXCggKORIO$3_}BNXtSqp=&%9@&A14x>eBkS*JkS8nMmc6w04w#$3qu&qwD!c(0izt zuCDbZzO~5Sc>CO_dGer!>-X}OaW}GK^LT?XrAh#C|yBe06Zsp-OB(`gfyB>Zhwa=_oiNGJNsVgR-3;+dDR0 z?ph-Oc9an~{p&u8rtg=$imz#_)^J#s&%4-q5Y5wyPE?Z{!{}X2fknrqmuw5$1?Cw@ zN3bBK^JToEH>sL>!oS~sD`N5w0+okzF=Q!JZ{HT7%?0LHWKDYFgsUyEVL<)d=MQXP zUvY8P;aIR>W1xM@XgeXts|?Qs$spPZ-pay1Cy`ztS*rWpf)qBy=5xm78f1V^*_g-e zBapes*B`F=sbz6zi{X&3MR7WP8qKH`_J)kcyhsv;v|F!f&Tx=>;G zzOXtzZ`}%CI+Qy2^eJ^~xRl`!&+x5LQ_SzBIiNAudsFfCeB_jXyG94`o_^aijZ<>Or2eI!Gd`4t$TKmlWYUzZVNxE{7j>W! z!J&9duLL123$6)P@r2hjNU|QKnxYnnIj6`{hX?lte;UkNhJ>d5^X{c-cYC(GRNsLx zYmmzLr5#oVHe$T=JljxqM9}#)llt3Q{+6|Im8Tee)gmk|im3!4g5(hBEagnKII5=ZMl?Nwl#&SvGIm+zI! z7ZZ*9K$tcJtFxT?Ed59IU7m>Eo8JL@h3R<6XB1mgdA0i7)^71zS$RH=7n-yCI{Kje z0NvX3ewtw{b#&MSlV0N4_mbm(k5on43qFX!$WAL+oT`xBK;7a> zb0S}Z3^nvbH?Bdl08fy4ck6XHH%dg;#dlK+QvXBC$kCJ*IkZ!K39ZnKhBZK4sTFgv zQ)zB33-J<@z-@(g9YgO!_vSX9_di^3!*`5}zvJA*QjIL87R``D^zG zjJuTYx^|sA^4VINd~)Q2up!{_t13eP3FZ*iedyMu7yr29dvlNLk|pP0Vyty=&vxLZ z;Y4SE9i*?(Hrx1>iPgc{TEe}8VpmySy`LaN@TqKrj(DzJ%npHXdSGswfK!cTA;Dcz z&XPU;iT`E{7?u5~mX(x-}z88v*m>8Gc zY=oT%6(_T7s>)b&=WHmbI4L78z+vQi=qcpS7;Qx+zh(|rSuwmS{UQmh5HEDvZK_Bb zxgY(mADd)z*1H4l8Jl|+Y@Pi_*ZWRcZpem%++Db-nWT9y(FWR_xcaV>4?Sjj6Tvic8o*rT zwqb>aK=px41q`n9-x3twbj$D4i|q3Y>~}|xZU&8RhK=&aj&?i$x@FfulD)G5Rodm0 z5}aE`>HEKo03EHHs-H2v10*gh0(=zBP)9j6v=E4Ct3J}6TD>7Y#cw-h>NR%Z+YfY) zrdQR6V|6YSp;)okm4g@XOaF1dXMNn5I6eqB?#?`cXsQQJRfZ zM~tW1)9 zlz#50eH>zawC5PnDBQZ@9khU7!&lvIL_hVg=5t1RS$)MCePKBkN%H%sh3B(~1QxG4 zsz_fPoN-e#xSJe0olRAt3m zW+k*1OsrO83@>RPG5ADXe?Q`WNkX?Tpwnl%vv&1EHPm=rG8#>7cnyjRrlHkCd{QYTYEgEsHiCjcqTgHym|)#H|vVnJ0k(32CZzt(Vt%S z&Q6+c4|Hu{B>vG+6yRi;U$x*})r6Fx0??=cK`H>B>LXO;eM9BlK;_*?<-NM(6}&Xt z{W>sv>SZK5F8_Ho@#NB|;XOOm3aX*?Kw`Yn=yMonu^p1PA1*z#LJe9=L0slRaKBqn z<~mgOEQ`{jk3PPc!o{NYgfwkrHQZ-&*vYJT%>JIpV5zhNJj{K#88_!FZf(d}H-BWj z@V7xfDLm$ezvPgfzmxY24knk5Pq3kPJCe9I@dFO;`;TY9(=E`waZ6u)_}$n@I{js{ zx_Z~p2rbVi2aB7n3GsC$ylqPB4l+8w+8q9RmmgVRHl9$^k2LA z2IMY0HYa1i{~_1th{I9Df&$MZS4JgbRDrWhps@sT{x{TISa!Zy$v0l{@goFFal8^V zm%Qd9yc4lYMI;>Y&8bI#{!3uA4ODu#DG?EAG)CTl`8Oqj2#If;L}t!lt%UT>h*{zr z!_YbcT}Q+>!YNa#-y`}II3fh%DPTo^>rJyd;|=5j#)$z+)3k{pGm227A{rF9`?=aG zQBqU9|NBakN?=ugzZ^k6BXo+;9)X{T`jikb7S+_s3WChhz(nGhW`tHGpc&)oW`v)I zzhLK|o3k^CW0)h=iKv;=uY^{_Bdo+C5<)w}RM{KtjNJpp# zIHNq}Ngpw_1-vSLi4bL107Zy5qrmOwdEb5E6^TVAhsIr1`4ij{kCh&HKdpzn&&=-_~D3vMGf`}$U65}YDILbOv=xHKntSq78c<37u zK?2K}Zvk zO9S}1NV@_BtnfAkc44lHGaP!Jp-R9-p68q&&b0EJ|L69zXQbG)I!g#dO-m7pAZd8P zl#p=@%@qEB?-%ze7mfl|G|xOShDqTEmGFKE7;D~P33jJAuky!+*QI$UJ23I^xRD`v z%4?x{X;W~Yjad0Y^D*Ni*KjeG@1nOaf432tbJT&^!bhgSKh4dma_ zeFSbqzCo|My^wQNhb(whHca5=-s6*3KC1ZU-$wm7?)lQj&yuKMC`W zoc#L%z0~#kF{t|;H^6p+`O-~JK6HEm?!OeXPj*`%1J-m{FEac91N;!TJ0{8h(2^4v zu$8Zf`1(|6`VRkr>HQBiZNZS+RiE6IXxO5DUr=`s5XMdvH-vP?kaO#!_ z>pi4~W!PN80L8CJTY@Sgw4d;GMO@k|P`0>qN66wdI`b7JU%$*J<&$QB=|4D4AprGZ z_sb{p_!Aj9L5#=wNxp6I7ZPOCG6>3NDkFsd2cNr8%*ENhf1oz;<-GaNjdk7HRg1fsk zuEE{i-5ml13-0b7WN?==xu5r|IzOO_f}(q>x2&~h&${wS@$y*Ga~W3h1b9h4{=4Nc zQPi7GIr2`6Kv#o2XM-Z=U)I98vrP~Z%1HmzQe}jqB4k52DK*swU@H7S%+??e*X9CU zPg1*4?4Q7SEF*M00(Ts~9IrbQ555)#cR1M1_KhDjCPVxmE!wl~_Ckt`u+`Pv4?+DA zEr=fsx%yW}eesW{37(+|{O3;3)TGbU9L&@(O7;}X^>iouRt@5TrD^X#yE?u0W=Os| z#Ev=u)~im*(@oYhg`o?vn1fWLB}0A0Tu)v6^Ey7sIzGdC2JN~w)w-N${dBi#fe$XX zTLtj$%%r>GCiaB$1sN#;Wr2ftY30aZuL?y{4iTd;n0BU*-NLZAky+SNW9`V$r63f! zD73#YG@&RIu_%t4rTilt#^ zf1My(_5`P^1x}@tgFGan*sGlN5dgOcVmx zoF>KW?7z0xn;YBdy1MwJjKK16X8$!ZFTN8q&NVYJBE9vD4UubEGgphNnN^9~YQujz zI2iEe6|kErQ@rm`U9Hex-}i()wKc5DJJ+8ZZa zqDmQ;n=6r~#B7BzTsGA{^khCrDS`_Lg3WP)k}~8*g>m6C!&NiG1vA6+7KOTanxc4` zVtC8acsx3IJQ8?q(og2i4oS|_);q2iJ2DnKCLLB7fra7Yh2dF+p{9ty^Yc=FoFJz| zIvB&=R-fr}ndw_cS;Rg_!TuhLl8#)tZv?CTu++ym0|uP!&QoBrlVJ|H zb)U_enZ*C`*YWH%(d;!z>`bERTyp6+bmMZ#2Su{itc4z2kh&S_;^{by>yh#21Jd_Y z()R~4_vl4l+C^ScMSzcHn>&+Rk`MQ<7`JOIFj47fkGu$Z-dTbOUPjOYL}{$=z0DSr z1dhkEx|VET7xZF~T~D~~|Bv1+++mJh0(Fi|ZqffYo$c^!6LHj~6Ek0S{nkXk+ z9^hH;Cr&?v5PGP+zMfV6e69C(Eza}mW|z@1vkqicJx{N8JpRuGtz0ruO|SiJ%w@x#wb}n^rnW%X#5+dU%|_iIQYeFAyn_+?R<)nayQuW`V`6K9_i*D(`f6B z%D)BAzs^-X&GFNYN4>rE;-7sS?EHMy$?t{SHM2gW_-e-!gE)3zG$E69g;v-_?z4;Pv$1dq9U=P5(E;?uCUI&brf;Roy|K=(=Agi6E0&eqjIV2_?DiT zldVikG>W|Ih5c6!-^-hjFqCi?NFuQS(ki5KhBq zWtk6xG3)CA%f)5D!pAZV*A|e#kL`_sh)KtY>=03e%In^#MM2@=(o3*Ti!BI)1^R4PE zfqTr!3j(GK^qR~ik2Z29%npA?Db9|I^ajbM~GC2h98 zP9~rco5R1+PA2^$T~sZpYnn1$WqZDkrc68yG4cOY;@UwR=P3}P*3xV^%F3#$C1TdH znkTaSH8kPaD>MWBu{iA68Fy1~qy+dEPM(B6UGn`WPP>(mPf2(`Q}Cu@;(tBMb%EU= zWpQ&rWx7PDiHo=^l*DTZ0(4U`+G4d&j2se$WvXJDzT&$y3O0*;_}Ck>WRLGxq}l3$gxpa6df4r5Gm$ju(cSe(keUurqENEybYsrnAgj~kLv z8P+e%7ikn*D@eIrD4zD&p0e$Ndx93TJ1KxebnB?YWQ*#^7h`S1;11 zlo_*)YC>k0J} z4n1D0IQv%+!_*{s)d!=g}Ei?9`>P}o}%#;X+#-jedazpyXF&|c7czG<%N zdOPld*1~s}znL^wT#6gr&2Ng<%X+Th!>`kwDzewMVFvj@cZz`1f8 z$9}anZ9A9L@djRzmzTu-#Q%BVviHBwMoCu=Yi?^&W_s#m-?78qg2P}!B2gs6u{YE` zAIbuAR{boW=g!NckWxkzLsXuMW;uxdG zgFSFJJB5G3yn_QylZyj@f7{RyGuPopKWNyA*g-oOepa~AWf2G778)BWz?~-$H!l2{ zu9lW{|5j-u{2OA*6BCRoppAb~Bjah)r|D{YYS#?U`$1BonA209+q+l480J>K4G+CP z3y99O_W=^u$zcb<5eE_x2fAqZ%!Sb^}pE)x!jj!0F(1w)Gb+bd=t|l&&2;u z5|1sLS>{V$2ZR@$NEkFqom zJG|ikFFyazyDmUp$tPSXfaE1Zm_7=cI06E|y^F!ykHGZ5T#Dtt9Ko;q|GyVW|AqqZ zMHEKF;6&xyCAn|kr1TKz0TS3j@b0u}hP@z(D1eg9$MoI~{kLsiNPPeY;|8kYl&X?g zsuC`9!v7w25D6wY+$Wrq5SWuP`rkezo(ru-hJFTs7ihHLk<@^))R2MycK-2ynHnO> z4!(D!*zhllOcAPjd+fv5sO#ZgHJyH!y&V|a9cyF2Zjyz0nG(TY|($D{1%m40O zyfgBi8GZH=Lhv^dIOwVT{l$8K(^~3ZrW8$vtjQheG(2eW)@&9VUI9**XaI{tuWdvj zC^B&S#IR36WJud#!ibIH6ufzC?Ii!QtpEN)ziXiZZ1fh*j6IT>1IjY@zobdKHEM>v z1c|7xzclDiX+U{tAb3b}U1C4w2}$nyfYUYapcod!hN4ibI+T+-2>eT2!4$=F@JiUL zciQhu5bK*A@2i&#`0^I!AQ$CS73KWT0PmW@_bIG^WPuB0ff8du>Sy^Fj@`}&t-^vl z%fRjqpDY{7B^!E=<#b*fBFe*7Z9a)zVdHoBr0C1Y0w<6aNm1_9h63#R88{PGg5k?xvN)9JO4yT|fvQ82|MGn73mKarzxS=qn zZAMBc)QOl*mbkwt#-}K*pfHZDD6Sn>m*bmTVTGWlr*^w#!MOsRZezVnQ`Egt0rW=CU@m%oT}zd--1gggM)pQ>J9*?^Q~4GiuB!;=`quTfgtwUyt(g(inGu>}RqjjZxoX(Uwyv{jj;RXUVa?0jitQS8sDR5oMDoE!0+ z3kjUvBsSh^E9S0s`Vbl1#eTY(ls4=gt&-`jfJ(agy`$|8`OI%{Qf7*Zk69aBg|O6L zJtN)_3RxH|Gt<{K(??tw+#`e7)s>}f?pj;PK6R?kVWCfMq0eWbU*BjK*Jzi}$fUNC z!g$s}c1Cx;yCw)&=qENZNj`CjK5;2Lwa7oUNIbboKc&#V^4YA7YSyDVfu`3xSA6e* z)kki{EegeLK5x68>*8*|$kw3I2k4#^0WYhVes>B(a0VW~JAvUf=cVBo&`8bUOM;18w=Up1-GWIhl1THEp=_k0xlRC zUd(og>4YyeZWs+druSn9Z7W{cp5!bYYN>YQD~T`7349$RS(2@@0E~^Op=}Ph=??%! z@=3(jHs7AhEv^If*;MN7f6VNX2lK-7D(uDT_dCkCd~|fp`ey0HlCVbEBHv^RYA=(M z!o-L|?h;keFQU!#geYaFstfod)fv%7a-{{vbe`2_6N_nw_gO3xpft1WG&73>j#tCM zwH)v%m5rR5RUkZ}mpgER%+47FI`8BNrd(WJHMo3Q`v(87a8H5aTyVQG>R$=alo|Wa z?p!qXh`}lDbYcBzF81)^DIG?bK>SM*z2Bh^aUamdMWsK7=Xyw^%ls>ZYDE-Ivr+^a z#}Q~ne<$TAjAoA&T=|InK`XMNzeUY4; zMpO;d%hS5SoByxSr;K4KnN^DDQb6>-t3TWm92lTTe}K;~lEz~Y!yZN_6#xEzRZT`R zA!qo<0%m9Y-~zt?YKAv(Sh=FAK@rM?a&5=kgsfwuh2Uk>4Q7iXE4J z7La0^lA8-l|H@`50s@qU@Pq@+5qzYOP+-JhtEAe^AySWlrI!PQH+4ioW^ zNbLnFk|{YOWJ&5BA@N3bPV-=f`HBh^RM4QWODWUfy+(iwoSr{rztV!MoMD9b$&e0Y zj1B=C)&7)0Zx5&o5JWebB6;}E_{RK((alfsk6upqT*1n?lp%*j`RZvOA_tOWd= z`re3qCgL?MXdH1@0C_|_jFd*sXB@55o9Ht2h z;t4dNHzEiaF;%#0>_HugT8x8jr;M0q`ay%=5k}aEpOGGeTUR5Qtww|ZF4UE!5H>@S zxeJe0Ow33fC#K4pj}zHz<{|c!Nb8&)dPK?yKvbJ1x!OzBRszeE&iMwE3@pR?68eeX z^tCJdmNLV|it47-+&L6v=z7S${_2+WoZ@!&V--o|r*=m99<04`BpeoXH_@7cp{x3E z&ZTR7F=?GbEXgP9l{DU}T;~oN3W-)MYcBjq%687)ZDs|bY*?2IO{3SLsU@wga5SA{ zKu8&3FhGX-VNq3NJvE!f$d=Mt(OesnOK$rsYXNUDKThId69N}KZBXcH+nE~vHR(4N z$*KiGqK{r)Q|s?VPtWzFF%-LZ z$KQyEaKZ~mfPJ!svUb^3-G}gMYCysfh=7|$m(kv~q*e63aWiK$Od)lxj}Q`SnxRT! zq$)&^C?^UIZ!4y#Zd z-j`O)sfk48<&rsg-*V+%lE2Wodn=RsD%qvhpzA8eyBeSVd7+UJ4{@zmq;iUtYdK8` zahjkUU?HE67J`qJCH3gd2j;KCUhQPO?^l0^=N{)~?la=%Tc0FgW9KU*ukkE#ps^wN zcueCiV<(WTly3-KWk=mu!toK2S&J(z7m;d~U|dC|ca%%6kSNYL-otWcy7_abWJ5o^ zb5$DzmVMLsbl*6xSb@-)6+l6@^u@nxUEgF=sp#Sxi(dx`A7cDa27s-^FzPSOQIWNB zwoD?ny1U*e@TPwFuuJchsk9H0-GBf2M?db*W^!`RXK-^F%_~59zsH$gQ01u-qmEU^ z$1(4QX8Nh_#%9Y0r+(*Y0d{4;R=h^?4OeLnzW$fkj>y=+ECt^hMd!EGl%Og`pnnZR zG-T7Am!CgRPLNnS2jIFioyr!IcSY8^N}hUq zu{yYi&$TwLOA*>5cSKTeUN;M)qZ?!43ZH7x+XC{~jye*n1kn0LA}SUD*CaY*5PK00 zDvEs_{e3|>{-sspwDkGO3wq6|j$42$7Zas{;LGFXBX-uyje;C6EWM6EO^;84fEB9J z{htr>bls{oKfiUc&hoRBdAMepWN%I~PvVpjsFe|@tMF)j1x-U!?_)DilL!VzmQE+P*|9$$&ocTKIPTlm~56SMybg)80ZhN6rTmnF6e z5agR49piI}JrHa&+pp64bb`USyZjBVGMI{^O|)nW^l}xkxQjl_wBU|W_Y7_9;4Eb1 zD+_y_5(x>NUQd!ex-@%rl%NS}IQDhTyS@Hco>OwqmjX0zZp`^kvJ>N%J({QO>!|UP zmv8@MjXglo{#@aIU9k5Y>{k8}X_gXO@SZ6+U;q2sv~L>X;D{opIyUyl$rbaShzeWCc>E4jly@p2ZmosHg=LX86Jyb{W8R!CbMH^wb0=119yy>OQxJ` z>ZO2Dv6o}JK8D3BZ&ylNV^^j{GaiS{>;fS!({%mCrtlYkoXxwNymk= zYRB>(ItHV36+OJW7GzG&;REq)`NyulF|IrqtSz8Ad#_d@8`?{E0#v>u)kg&aE*9^geI?GMPQzT~IZ*hIP&NLXnjLuPK0Xy|luBebU# zcLEeMjXqA&_7pgbaBP{A4^tlNKsQ++w>@cLO__l%<^`BBI#aS`(@SZBDyzr$ZtH*e zX+$GO>O>fjgg?h=h#^#l$I`c8kNZK{41TO=9qXBB3U#f9`>PYV=!|gOblZ=6W+f%% zdgEt$pkVgem z^}wj^v$sg1_-p6p7^zak8nV|;d#-e7CSb4KH*AABCbscbcMcT@C4o*ij_3N#(G+;M z*d^$s4vn7(X}*D+Ju^NZMHa}t-sLpWD-w*27DUlcxfeS!$4QA`M}(1muoKXpMf!%V z2t(fu!+fiWTUcL&vXvlvo!E<*ir+9RBv?<7s4945@68nG`MMU$z(dKL6IV5;j)nL< zy3I9PHpGnEaLX{cVEy@pip`^e7aw@Aa3Yb)fK>QW^Qx-%M^!IgRS#NKFI`nnc%M>k zKS^=lj={pN^rc#tO&%;DPp9~P~40q>Z zgO@-Nhe8|}n!OUgU^5d#_3cb%<>J@WtfW+YGSR5rU$?}i^@}CVvVGZZtwTp|%{`GT zrsJNDPQpzlqi7E=i-&m9BIrzIIx?BA-Lh!ZlCUg|~dwd;W6Oc|+0>lua$C9?T^y61VzYV}NKT{tRX8OSZD-&A>^y}NNXRRB0q>AAijcgAP@W!^RH z#j2?4-7QJ0{h_u{=;Dmbt$nNySgAqZhMZ&qi8X%7W&v-uR^PT+%pOH2uz0@C#2e>@ z-h((@y4>qVAMKMD`lA|&vd>dw%k>w&z1AoGO<{Q7Aqbsd9x_$c>r~YP5AmGRPVchL zjjtYQ${qr_S3tYuei12~t9@x6v7R4b8EuKWbnN28a&QD`w+=~tz1ag>5yi>l|Be0# zJz~~7e*@`U-8r;~8!z|!uD{Ru3UAmsfp(9?JG3};*_msnE{e^s9X5Oh$Y#}BL^oDh zk`k2`0~rbW%D?A~!~|YQIh-AjNOof(tr?^O=}Zk~T|kSdxevX_iIK&MApX={TLLF) z54h`M@%6m&q1V`&ah5{_e91FY#hm1Va7>YhMuh|xg~H#p-IOZ7s2GqCZBHV6`FGfI z6Mf8p!J5~#VZ;5>2(11Zq5BJ*KA*nd^{~CwyskgIZ%Vz~ zdlO&kzjkf({J+kU?DNl0LpBXaaithZv7}Bkjb7QTh4~O=+9RsPs>OefW^5SJB zOj(`7Vr{7UHYl!}9e732qvOkt&Knk5o`=A)a@V=QddC9j?OgvkW^yG5bnT%BfF_?k zfWZ}t&Z_(pVV@tD`(GD%hA%lsRnnns)w_+!pPg zT7T5AW7=O-8MDhHDGSJ0qhwSr+B;T87Cce1J0G{XS{5l9d9}2VRw%re3I1fX=AqsM zH5YRE{)I_nD*2Ll$k$8ZdG<|5I&_hIJD<8|L3o>~0VAc+!oIFA>gBH$A^!#U(GZRf zBnr}^XI>vajnM6OWHb^qfa00my|JnWhe&`=)OdW?i@mt6Z7Jl|AGbu94;gt}t8v`? zMo7-Pml$b6>wVB0m}ZQ{hDyrL&7hi+O&X&0Lo*_qGv0k-SjP*xgq|o_rTe=f;lTXo|y-37OA!`8P9O=E|Qz1Jz2B z?leDGYn6^!@@G&lUdBq{jl#b^e0riKzbTMB8VZ&Wl7oMBZbsztmA&1 zJo<;mR-StQ*#p(h)@ z5VY8U15YsQp>Sq32mfPqP@t8>`e|%XNI6l}F^tdG`lelEn|l2CLTlHU_@hrWnR_g? zL66VD)P2U?Fv%2H&U`0RBwOYWGv?m}?C*5GuR1k>`+|alV}SLsc(RcxM_m6N9=oMOq`#D+!GCd3b%=UgEb^SfNSar$l>;8+M4z7t8_bun|O z{0+?=xQy3=31lX`on9+6f-5$9?ur^X-eOS2RlG9~g_VsAK_KSruNPoyKg`b{0sR_6 zT^=;~B%fAmi&NpRJfD0OZZ`i_EyI|!tp&kU=u!?w9xxW0z9Ym!)AhI@b#Y#$1$l3A zX)ZlEiPo% z?ssJ67a*<5Uzf~+I(T6=*JH&V2UcI;bBS|JXaCWV6A9&EZkt|A!jMV_vWhlsk@sR2`MqD&af6sgv^V{`is%Jv|7|J`b| zvk_9tsYm&}`o$-C_deZc$QCl_a=uv9@m+;)_O?^JrS4_dr zUeJoxUxErV;viQh5x1|z{Vs}Ekd}_F!>C0`U^984IT-RCw<*Xu?5>kO50mV7XSJt( zECJk~sOj{JA=UDwu7`Fyw!Ti;InD`Thv@1Ed4=wY{*v8nTyAF__A>fe^Ih!&g`Ap+E{n~q|188lXtGGuiPQ|@}{kur4=-a=V&(NR`j!1YJp<;x&R^Akhv z81AWGI~#@bxsHS{)*TE#24fH+@I0LE9HM|NTPx?T#bg);?VEK#wKTA``xv=cQY|*M zxNY^!mcbcP%>I22acgXj7ho#E;}q~o)%T*sVYj*G7*P!Cqfx3yw@f0Z@y++`))xY1$=0zj zNSbYHypQ6{y9(vvEsdXad>&!k3IouIxJlopb!qU4X2N<2sVU_cVDIxA_vgC_WS7oh zb0?utzmM@PZMX>(zuC}8szeBch6y%GChG{8#1)q#oAC|Qhj7yt8C7b9B=*)r1qf0< zbzV#bhh~0WsQt6CZ-!o-lqCqTZ}1|i+4Cgi3x1dgu@gMo!3+3s)vZDa+IY3d$p{k! zW_1Z7&!vOTm)7};S^{ervhPKHx8Wo0PZ2#lnw@1%enZkoe}Y{0q=U5e=Gj(_erc^x z-mEZ*gbSe@4fWw?imqF{J9lG99xUa1%$ECue4*2~UZmYsyd`W50zCoLJk0DcHcj|E zmyc;(zTkpwCx{oDMQm$&QOiTjS(Gg-CL zsCTU6$k7=Vo2hM>5Ybl8-N)#1`t4}7pS{>zn~Md@Pov+zzYa~KDZ@p{Kk<`H59Fln(X$k^pNrBwW*`9@_%2ptHp3~cEMbC5RG@$n}E5`e) z?IMI6T{+WAOsYdntG+Ns+%J3qDwfn4r>QbF5n_FHSbF;N>0f`+#WLlP_v_rHV*L!` zFnQ5mg5loCq24V5-YEkBuHMa%tsL=-9>oiw-*FN~;~bZ7^AR}cP}1~vvU!3Ed2>&@CUs|d) z*Zi5s28I`5d;ny7i8=vJ#V4IRK0u2;2&AB6^*m*(^NKM{qiL7 zIEUx$q@My~bSIPtMsFMz_v>P5zyO%Q%|gj}DStOMu4jXX7J?BR_`nmP2^#MoGUvUq z`~q}r!l;HL^Rt|z;ftJ`aBW}*qn=){BWb`a&g9&=9vI8Ab6QL_Irl_!TGXZ+cj@>D zNiQ&QR=K(ckqPUVys5E(FkLet5xUCJe|77Fh};pn{nK`-PUae7YNG4d)|dkd_Tv{V z4}wrbNN_WkUj4`|Cb0`9>W35b7cR6HEV#EeD8NlHd+mxAHz6=KreR6EAy>I8%-pgbkNFEs4+=x%p-AjOQGX1~ z?A_D4-I1-GI(m%COYzR#j#Fpv)J5jHk)xr<*4laKDph0CwE0eKr@7PGlIkvla#`3~ z>b1>U-gD+}C$o3MybmuCObJ=93ao%76?qU`uQyS>FiX^+N@~+CxXg~d^ze&pXMP}z4ik85*G`*o z1^1;o{rjx`So>VP){kHRf&wzs)n6|X+A!Mqu2IwL9drE7;2Mf{EnvFrrmIhzg)?-N zQS7A_G|k?z9fJ5y#=me>(E1!sad1|m3N#-L@9dDgl|WpX`wf=TM zp-IZo?WdO3Fri5Coc$TJ=_TZP+ram(KhMQ?zWLm#hc~XCH-MGHea8JHF-~|QYIHb8 zp|o}3qS@bb>17xKG!H&16R*A91(%z2Fny0!8cOhjj2m6h7!qSeKWWp_dnBML0FIf6 zDZGU&emD^8hju?$5@_tu>vB=u+HJmj-3RKvzn2TW#(>^!y5FydfEuAUAfx5|?9qb0 zbDsxPVclAFNK@dc!fHi-sYXwy@cF7qcu}{vK)qqU1+8AS5e2>jZ=;_-{SgH_#*Wk z^#=Tu?tO?8kK8^qWo6!fE5sTLZZ8iS%gD>UIXUhBBk0cRx$1P8MDzgUcKgh$8^Y*Z zTiR^T8r16k?;2mYTsKpCfC;9g$LIue6mT0#!o%8$WEH>1=#Ji#t}G?P&&^Fkk!@M~ zLyQu3k;ES*rgl}rMg9snuwQwcU~SMxZH#;Q9#^ycN*}v8M4&F-a`UqBOv8sAL_VIw zPj<||uj7ssu%LJW^qy-gQr11*%O{pE-}DZDQVL0LsO|+p-k);LnYC^03Y6n^X4$$^ zUoV?Jjt5_eFNON>2(wzWwF_0#@n`jNb?G##5}NUBl-15`RbVFah!c zUyef#i378+bMG+mP$=&(8CVQ2r zQTxq7%ZDt_uFU!hOvSy$bx$lc=*Wq^V>$;_ksq%OV=5B+wSBS=5%ZS3-X2?bwZdIk zKq&*BW({<@fe<*X+XR;{^WTG>m}*3Bt|!#Bv%5l)Urg!}(3|Y7e;HwT$s^C6jAc(+`v7hLZ{H6QyoM7D zmejT)&JMvGYq9L(<=eLGr<(&WXXLLko7DZ}M^pBLM_c5Mbk^MsneoiiaI5G3rl-Y{ za4$uBUp)5Q?1^Ys)^pb3u}r=Llryx1zZ}ez{R`D$|KC+oh#T&i zf4}e5*xX20niF{tNSV><@%Ti=q*~QxIuu!;$Qrsain#4k5^&S{nExQxsAwvFK#^I+ zckdidjqgaInt~;8_{*;ZqlCU#^Ri}R$Xa0R?%M5o?B}8Wddf0Dlwx|BL|r?#E&Gx9 zj3VSRiD2!-I=CC>;l?$yuGdDgT{0)=^zw&iy@|$~#a#D~Vp{_xNq%9bKNe0B?PSKG zh~etjv%?&?Hku;O6YdMp3ExaT57yWoFF}`ug+cNdtJhfJul8W} z-r}U-*w&a;t-Q7m?)gyPapcwfqJ?L0yT_ZRn9Iug`_v%$cKA5^vk+o?^qrX`Vquc` zoUy67n|@t>!XnN-f_bRyG~wnhCMvXaV=S@#8zQKk!T}_y5azz)kA6fW#rt`$?fgxK zJ=N~=uVI5Fm-*_^dF9gr1T``7r&C>VNRefO+fNL5>?-wxR*&XeNrFpS61M(@MpP4( z@W{Vro?ZJu76SHGOy*Dg-ys|&6cgpNE&U5p&_~ELAj$71 zL9v_+m<|GBye8rsNKcLqryB`_w^F!Fm&jBhh!;7qqq=B9XUa7M;`)o2A6r8^#ZqC6 zyW@qChp^pgI%c>Gl6Sxdu zKip|yxhV_2NlDh|=9IK<=H7EW?v@j_@oS#Y5n0dw$&LG*D%e>MuiSI|(=%wFQ>VKHJ5Q)>I z5q&)ygRTK(imb3<)lzVl^jjJ+xUWDnBAZn~G%If2hE}Wl9qDfCq7x~2!atMhdnY&SibzDd!h#95$I?TaJ>Dw0~bE&W3g#g8b4J5ayveA zqyZm8#J*9%Qrc$~sap;UsOjLkp02CTLERim6(8a$1v1<8|9XJ^eyW1q$RYNf{wdvD z#=|C%QNL}ryQXQVuNCSUZ+o`Ymue>fErm@>oA~|@kz=pY{oeL0dnqWCiCrrX!3Gvux?13mTXM$DRs3)dz&7~b1 z2TQM`Ke3@Gttu;aYEPupjg*m{=9}jM*jKLj`d?b}jzl&r^#G=y&OY^3L9RGWWsg_6 zHU2P%gOc5fwATl72lF!8$FH!Js6anur0q+t4@9+TtNAEjTZx1+I zQoMLycH_f+vPGMA8#?CX@NV#qoQU?_q{)UIRIF~+&nQp(Opyaz>~5vumu zF*o08wio$8r!T2F2t&1QtH94) z_Y}(6B~d#aPqF{nZPd5r?1uzoN6KSvUql7fF6-~f)x5Hegi|>zl-A3|h?ch)0_B5+ zZ1edaW^QTod(lNP!TThbqDNbJWkfU^ zSyTdK%dkF;NY~d&&)clM`u~+K1mP z-+5_>V)t_MLbB~)*LMwI1RbW0_YlXpFu3OY3{>dP8RJh>@sG+m(|yZer}3o1DAtmM z$TH@Ov>HY5mtvSHX|MlaDYJ`^N1sG8L0C^O>CxP?*VQwYQx=kCA(xW$@Ld=`;c0Rn zSPgr7KGsJF1O(vU-!i?Tk(}({S?*J_V9Qy?@0tymby^n|$-DpxJZ?oZ7hUOy(2=xR zB44R*2HUfscrERjJPKMp-V@cqOqWk4!+FGFC}|!?hXm-RsbXw|@0nsANY_B3;ag7X zF8^;IiVE8jt50h&N?)s*y)J5Av(1*6r1pB8a-{5YDnPzZ$$eX^IZmAZh7%Jm)oCZ* zT&cfbogGvVtWf}*DpnlDP#z zBAbI^>rgFkcfl9F^U+Vy)8q=$=~PHTJPNW}RGsMwpD_d^`dE?s*PT;)%z}J;Jh-4X zTFEM0D=;WzO2l|5kA<-(cJ-{epF2oLOl#N)kqDTGDmOL|tfgkv*Yq}^(CipPVADB- zCxR?1Hn6_vplfTNr>MZ@$X$68@>_~DFc%1Dbl_pv?R~ji82q*UKEOIo?^Y)#gL#gc&X1okRWF?zZmzxcqJxSF*jD4$=Dh z+Gv92SfH5I?zF22(TPj#a(zu{=gQ2Lp$z)qw!U%;kb2Bn{$lj`SYQ;$pBVH2adTb(l4I*43ynxFA>V! z=&ocyM2O&)-gc+)^MiWVE$&}J#kmegt{Szv6^x~AP>D!<#`rF+NUa^+2eXXaWbXHm zzg&VZEe}f#IC8XqP_(10@E(5`8?q8%vR=zos>Y-EOFZUH|2O3G;!ts=URLdDA+>O9 zFv{)xhL<+ZtO#32oIXPi{%D{R-ug9E>}1yhaG)B(T>NaKwYtI-vVACxAmT#dZ9r!; zYAFQamNC2mV@l6$daJd3fs1ds>8P}c^Ctq0vtNXqCs}%x?#=mcI7e|ba`Ybi>JLjT zq@27MNUe2Op05ql@I7fNOFi^%y#aJupqldxffR|XtDCQPgJ_SG1TiLLwbKh#Cr0VK zz!xCd^Tk&d%}7jvQJW~a4;hk(FoIpV^L>4a&hA;J(yPhdBd8ebz*Oz=<)Tp}PmF2- zH~p@ZX$gTjbA~NBq~C#r7SOghGazZqK~Ie59;Zezb?VCo;9xuY@k% zp|4^dN%o6%_l)zd-e9D3E2(GUaK+2wg7@}JkYPdSj4@`r^r>p-Nh_agBwt{f2J2On_#{sr}A^ouzx7p zdZtjy!>MH7^qDpU!73paG(Zyz{z40Q_YQdf&o@%P+^lroWoUq?Xmx zS%rJ&j*^TmNNI%3_?(?ASZcp@h`}mii%_|Ac(mhVt-)4T*Tj?t>LnDAvhQk1r&9^+ zC4fbM31soCj&Q+e*#F84dublvH}MyTq9g{P5BY!h--K0Qbb!OU!RtAgso|KoaeIopo^ajpwx8!3KI=u0?2fB->)ySux)26sqsNpK$=hDL)0cbDKA+&#Fv zyX)XCA3xjuXSepNnx5`@)iqP~-aYrWoO=%Mbt4?U4xiGC%r_$+z!_-J!Rq{LHeEZ& zfEKz;6pyQqaFhxI_q@)KJ((Oc1?Z{3wT&?wwnS!UNjdwW4 z$RM!h>(&8Gt?)9J{E91MeVwvPPs*wgRQwB!WsMXR0Rp9iPrM(_(@W;!r?v2!O#Gr1~cY2)erCfpT%?1$Wlfn zrt!4kx=-}=p}YwdUeJ`33lpN{A9ADQ{k=e62;C5@6+YAXKL|c7k(asienS9s6+{Ax z8&63B8}=pMczTPM?Jsj!G!KfM!Te=Wv8A<1L;jTDOxSvaMbaNB>ZV5oaCfn>H>E4Y zqF95@ZB!mOV}{3e8g<^E0caYhCFVS8KhYC|U|SgsASt)yN7cfrf&zmQ5`hO zoXnX9x7$QZ^({BLSH_p&(ZAQfQnR|AFe7_q!SoEmEGv8Li`VL;1Fq#D@<3-dUG9p} z4+3@AQ6F6Q#@9XkZUy=amD7>y6@opKJLvH22mF(YW$F3)YjSp?um(c zP0$Q*2)=2mesmIw#p>1#_p6y`{Y9pV(sfo@nE}>##! zpRJp|wyb%_uW_^m{A4Y`8dQfu6#s&>?E*Y2-T1PA3Q9XU0d?2S;cO=hEd-otoz7nq zGm{oU+YuAnlr9M#9VqOrg}3Fc>PYs-72{#+6DY@$iqlr@wZ7dA>iJgFqkrk-v=hJn z?fTmDM|d=w2zNP^|Jxs?Xj1bUH{T>=9}@&(eUVp)yF%d|HXXNnNc(55Ym30(k}J7Y4)$E)PTXqKsM3}l{i+o^h9;FEfi%Yfs7!}-lbSoM7 zgN;XSH(6;@BNJo6`)Dp1cpOJ{hw7%#7tvMNy-?z3 zE4;8gi3sS3Wax-iX^FyViI!=J-twGR zln)!71=(~rTM4STfLO2MXWZ{E)(^qaGQ>Z6M}gC*8~L^n)rcX`W^&vQzo?V##$ofX zK6xHL>U=(gep7$QY+_0O-FoLD%V+sjm}DrOhqFl08&ksE@#DMS@9Cc5`^)t_3-aEW zi@6|orB6S7bPts>X{cMY~)jyFdI&fn<^YKfB4QpkM)|g5))rMtIOiY$w6AqX`pM7MacFZ=9H9 znu2T{2ISHq#Kl_S&+UB=Uv3$=CIg1cEY$h0P*h@i$gQYGnu zte+MRFP@7x+cpGBq(`DKOc)F;FMRybkB*GJa9Z-TuZZ_hh|+lf5s(i1%<$}FAvA7+ zjwWozvS=Z?#*?9L0V0MOro2$@fp;I+HVW;5in*|M#+=mQ~ za(ow{@Q|HUIeKUr1=>_jUBWIrO>xSeT!Q-uZ-~F)dqKA|LxQeiJmOTt2=7mSIShp( zD^5&s<&P)pqE74H&@bQ3y`0RO*ZQoA-7qxpL$YTjaLS1ZY1<^}+8*iJ`sms!R6Ofc zJQr0wsZ=~y^RAb&S0KWz!zq}NgtU+;N2=$e8aR7`mN<$r0FGtoC@PLUZIOXs%`U+#Q@_ZxX>mF8K(1cmF2sg}|2Y(Ml3EuPxGr{5 zr?%Y^B1-Gu4auZf=av;Fb7GZUK>znIv@gatDsxOtQb{2n(2f=KQ2k5I^U?%0*#bCs~AHykpOc+JhG_eKP)##Ztlssu-)CJmG0J8)HAb44GJ8u)!!C5VF6T=b-8FW}@@ z!yV`&b z%fSTF8hQjFC7f^v<0pRCL|EzG zMF=ES<7LIm5w))k_s(D?z>%d7jPve)vJ&hl@;XRV^n!C|yb|TO-uwSNU=uOU?Dh_7 zLI7dz(T{vjPGglZuINtx-va@xIL1odD}j87Agn#c5&h&I)+A$;ZoNQ01Q5m^?TBV_ z4Xc|mN{`+z;D3IE<1msBsR_pE(+>IoL9_^Il&G*@E7Swb<){pP@WH zULx#Z4X{L;ldg;U_zFTjKwrY`pbk((E0b0NqCS3tro!bA1xs-73K}1|lhjVo0BQ^k zxuK*%$TOM;`K8p3&j3^m5;={eT?pZqCUPg~oyY;Q7(l-FL1Aj$_s-#%0=-?;@1-ykQ;$1kAap6uakrTaV+0DwW4&;S- z_^H+Ut?ur1n8bAS@kMz4rmX_9;+FpFMQpVm`fHY*9#2CI93)=iVGUEom=kOPfz1-k zh6sGaUlb2YVeyWI+UDEHUOB@1H|F?mS}PLfcxoP3 z=XA64ba^Sha*8md{oPE^yAU~E_{Q~H`|(E-M|>bF5DR`scHhK(mk;e?S~36BZP}=I z?TcOArBcY#jgC#A1QD7o?eST8c`!xhJPMmDmtU0GuVrB=6kM$RdO^;goeR1;19$+o zUecl(ajGHALtL|BB2NnIy`tUi>l1Xc;>z+^N5h9m%`ST_P^A%;w%@N040yu$(^)*z zLt=oGZOFfuHuOXq;?J@XrO>#V)~Os!Fr}SLU#wb=Mf>Jf<0({7VRrpd4eYJ`gX8pD zt*YM(1TVZH&kf9BFt~(47tm6j;#`tkKM$yKgE`E%kyW+nIa6JQ~Zc~6AzVkef{PQFJ)=;kguc(g1#1Uvk z-FEkCk>W1+g(bG~UG?0889MH-ibO0sGX34B#&|mbDlv*C@|vC_yD4Iu-8~ley(@i3 zI@an#F?OMC$4H=#^h7%~0?J9%aXZCOuiuXjcQ0`xf`M7#6axCgkJBwlv`?m=ELL^{ z5@*LHud~-^4nkmXJjJyrF$Af}ZrK4@q3U0Ov^k!-sgFORH>eVq7xf%9URmF^sZv?l zhn{F_+oDk|Z=V|Me#-xe$88}P>&J|qOBR1FOesI0crfW#i4?|H{XUA(!G|JClZkvj zk0>t16Ctv?1bhLzKEBGkSdwMkrAoXq4+x6n3U$0|I4NwD&Sp=P1p5Fv(LY zx>PYu3qNg0JWay-QpfhPZuAJqn?9DBJd*TW&-Yy$CiB3qv?7>X)hkcaYHn$LiRUq2 zz~{p3V#qAv5=^t4c-G`W_c_J84D!Fz(oi_>ZuPh{%8{V9O zS6`6#DQHGyQj(*pSxLBzg+2Wr4u}#l+^`2jliy60zHVM}qbD`c-Tm>bUB~Jy3U%AK zAA??4TGi*`y+O3d_+>xG5$ks@e)VW7$5#t2)9G1_j?T`>I1mMtkmi7fj2ayAW~`{1 zrW)q`#%+5X@yW*9IpExk^~-#gdMI&9d}W83(2p2~ z1o>EZ-YjF4t)~NNu$+vhPJTO2={&Jn&}7?C;sT}BzMUt(4@LB$bvn?Ze#s~JH@{rm zUH(xERbeV&xFK$v#wSeQR=5Xq8}L_u27`wf7PA-g2ZE< zv5?NRQ%1*Z{9@PhOF7iK>=e{bw_QcK5%(;Z`l+m{Nrq$Lpc ztq6GK^^O)`(ofoa{diwi%w^^i(+P@Pa^#{wu2?y<)u>j2h8rSV#{hpC4h+!`>e%yL zJBOgW>MVA~8Ew|?JJaV~ramoZ48o+j9d`%f7mMzm`HT{dLg}(}w45@*igvI*D#|3n z(eT;L`d@ijuVmGZGpAqCpQYP#OkozAgN%2F?Jt2oILzc!fqfFI4A|#o41c@?DWlB8 zGiNVFw)p4PhpICHe#```x_>(HVnzKe%deF~el>m7znImarog!g&n@Ab#f7K&yE#}6 zf8uTKkKN!fp9ML5)YD*z0>0$#cW8I&S%)l5k}!~V3W?JvsvvLCJ(M%^Gk$vRBe3Mx zhWZI;(j~_B4;g9Pm=4|_JAKNJ(B%r#{4i=}!`YvQn+e$mX=csqx=`uX`heNehju)( zzDdE8N8a7tznvSL5S#I-S+D&X$x^2}7!Kou8@QCJl$|qy3g^rr?$Jb86s`$w5G)E3 z#<|cP>32bkVf#YkRsSUw>rdc}c8#%lSt2HYZX9sZfRsN38vUz3!C12=&){9uQp32I zjR75~Cg`>I#v-*eW)ji~_n2A0JSWM3TkJV!9?NfhGdVQ3xS-=+Qd?cQ@tw#Wu|0zI z+9t60-i`#rkvloB&Mx2yONHm>kFmN|{E;AzF(kXR@dU}u2q_M%@J9==EVOiC^kiWm ze6(?8(8yZ?+ z-OA#c750HdcQqGX+{Tt?zNDZ&PQ>XNC)Fu+uyD!9z{7c^biM8CVZ<0i3^A39{;@Bb z+Fhwg-eDu*YO+mh4^wA=XPx9AN{BD;Z!z@+BMadGWDK3(>54rkB0R|z$BR%h=C?Sh z+Wd9FN~P;8nvnc0k?g!o6P-;8Gf4hgD|hN&=-m!U(8MTw1%ZisB1Kh9Ksr*&iUqsb ziLa$P$;WcJ_!-KZ?#_#$)u)8!%{+Y1YD zeajrxx=^Hb4=9O+;2{6dY_Ib39zx3?Fym?bDouu<6~GU8kvs7Aol-C#$quv4IB3XN~Q>*ilZtK zo3^=;dXUkDWlU&tZV(eV5A(wXsZ8GKrn}K47!5TmOtKoxQpsAT;W?=%16fna{$FiV z6uO&xh9@Mm)CuDL!G*6kN>cXjy(r$UYVb`XIx*JNDYT7=TykL~RsO4gOHnU%h=njU~rH6Evzq4-k z&HOf-zMs!+%kolHKC8F4F*loei+TyEqF9Fgw7;#6lcW?8lQv7ltp*zxRj;d~smlGU z#_G7t)s#cM`4h3|@UB8*F;35&-1I$J6KorVr3A_1K5%7TibE3O2t3B4pX0E0d^iah z-lt>K_OH^z`?os;mJdheXvAlqqH= zi!m6*Zgaih+OvrM`WTdqCk|}}@d^Jp)X(Hhgb*rbi1N}T;<9N0kWp@`Oz<- zY97zoeJU3K`To?0^BPn}QJNawRI!k(YIleIb{$iXh=&7X-p>aW_aM}ia^WkLz8>fH zGq~eR@_L+UBmwX4IAl;B+&**1;#%=`OB`<^q}%fV3IXjFcpGXVS0hU32JVeJ>f}7w z96s|Y6=?&!(JDx#_ccG2Xl9^-BdrnRi}v*W#G zRkuV%G*THRP09kRk{>1+QfYphAL5&Z>-CE%PzXr~{KWjxLxrdT=!9SE_l{5tNnKlh z(x_{b=^AZ{VkV0J<{Pkb^Nf>vHBoVu8>5qmJLW7x{w(<%dTnjE&+wD&aF}Bb7{4Lk zvRR16xDmCQN@7r=u8@jrZ;XQ|ER=w{z?RKJva-yd1({rbTnKcEQY@7(`~~&h92@M= zjpN7n#425AQKTd^KAcT-Z^UR0WKLgGPK;vipVUlT{n3-&6Yxnr3}>zN*8KBx9{u+s zSyOW%9CQ^VH+NAf^urISg5SETfcHga@Mw(0YB^C zPerRz%I~1;ir)BF_g8gXpb6i*e5MF1(Q_#MR&Npo2Rf5G^tiq!I&wEG)Z^5_o^;Ic z_5?)h$;y^EazgoE|DeAfvS*0?M<=ZGx(n(BOdgxs_BR@%uE9#5ht^QYBmK4vwMe*l znTS1ztxh}2_Ez-c`k7%5(-n62(ek7wefuBLUEY<`B5vucPCy%KlF;LtfR3wxYQQLs z-sNfQ??YogRm2Gqz2E!my-zG()(_A4V%9l&Yd9#ECo>s>13D}@;UFq~PPaj$S$)oq zl`hX|s7=yE{BIYrq_dQQ((>~B9gi}#=Q&c`y0E5J%SE=ndZg7^&rdD`I>7G^Fc5Hw zN7A~ToxpTE0uZ!d=qW7l!L#*VFOA-Oj_R-(Q#cgQdvgX^&8>Gdbkun8)_C0Kug|lb z3TZAHumPMymUZP)3$iY$QyH)ACVagk2Xv11xxP_D=`2mTsoH2!F`^Fijl3pyBY8vB(P z+sktwc0(9cQ>!btv zIyc_Kw`p1LW0rtLE3zc!; zdF}g+3X9|#p3oLQ^Y2_vEzJiZRl&LQo(w4(NeA=CdjC9V;q*I}(H1^!l;CGgA_L3V z7!sY9%dFr$hm!R#n`B!Yt6=I&P)X}blus>6BoN=DA%gtwgss{Q4qw{ZzqK+AFHMCo ziAa3@qn2j-@UT#U6cjooLbgF19Z{(N`vg_};)g+^jCd$Zga zrbtg?=+LHF_D&22u+sEbJvZk?^o6p&Bd07`Jev9;*O_*&BaaVvmmu!(*9O6-Dad-# zEAZ-ZH|%O=Yr+-c%AppNx0r2kHqQG~4HccB!-s#Q6fWls=g${6e6nc|dtcTF7-Nr@ z^12yp3$uy!`M5^Xa%^)<`!U!%OoBtZ@60yW)vFp$RWN(a!p(E7^%!e+&BB!&d;Y5( zf$as+O%vNhjn!slF!9GcOvTfXz|C3e0KjpcU{mxt)nU*X#r>O&?$;}Os8Pcq7IS$} z7x|x_4<;pkJv__Qlw-eauQ;M&KOc#)+rEZPRarIuq;ga#Y{IO?cF50SW`Idz%o#Li zgVCE2c=p-Z>u$=-)MtX@i_XieFS*zl9lzZCbieb}oGKm719@!mwyXeKF6P_!#UCrcvc~jKR3Pl`4q-x+p)rw=V1BnO?p2YNbKQENYA}r# z^^`T?8aGSc5>2FdzsNz~pI7Ti`%NI>8xEh;j8n#Tj(eg%YR8w?>`^LH38j;u-~8xS z;e&- znLhr?SCq0LAnKE*q@{6hB7F)LTDT4t8o)T{!#=>Knuws97*)((mi4n+M|9@EKB$+; z*3A@cP-#}PzSO>Kq+)6iRx_qrdh7JMGq%C9D`S}aD4?!@+}xHq_=2}vFISZ}7L-%E zN7e?<-*upvzhb|P7Lukxp12S2-ES}aVi?}4e8^r6co8SZTPwONAo3tUF)46an|l`H z`^?f@tE+8B^^0X*4TQT3sFa^1hw{umwe+7xkb}sjs3_TgeC;XNr<>A?eP^?! ze@+{kk^LW8T=sE(!li%I8hveX6w`Lo7aUX)!9VDm%UwFgbnbu_yWuvsRhRkZ{q(nu zr`ENeD{qlKEQYiL5!~#3c%`^M&b&i^(>P(*AGb+1UAjjKY7TasM?`=LBrY=(X2KMR zU;bcmMO|#+gtZ#!{xH5aW10fUsNJd0fXK0#Nlhj8&#K66ra!pXu_;Jv`tS4FWJU44 zql(-4c;x4K302@qjOzFW@!;C4d7_8gvJ6Wdn@P6=ZFsxiUlDrs|3Ec#* zaNxBbu*B}n)g;5nR?orzq9vqVT%779wuX^#Y3Tc`L!+`Bz=`srf#AcO!#)|W^DSLt zVy`h0`PJ~g=eIdMne?z(mVp=94+UEMrGse8B*Jwe(N6p@~C(x zPe)`QMBHpDpF7!D8578=;8L|0H5Sb)3PjEE1Kj&01fcvZ> z*iKE=xV{>($Zq^N+d&hb;RkITJ|RNcFv=5O5Y=z|tDUVxqV!-(FVNjSIJ&pfUpLX3 zO51n?(FF^rRMxRjIv0^TPa?l?BfT$0f6hXCUPXAN`uO%+zaXozy!s#?jX!F*15t!{ z@H_9b7?RAoyFg(Ecjv&(to=9mo+{QM&Jrz_4>(?fl>ZEG#d`R6Y5c1S9dAbU5j=N^ z8_)WO8pu#rC;B)U-qdyx^w?t3X@E7@QN2v;0A^SKIMumi@q~vJSfVGi87a*9@l~Up z*^*E%eJn|~;AnFpQWK+k3~`~1JAZ?&5!ZGZte!@MohxQ{SzQnHMelgE(Qw&L7%qax za35<+V%vfU4|Ci1If-Mpgroegs=Jr;Y9Y7#tOM8}*M~;Xq2@A$L=Pcp_zI24r>&VG;~7Hr${}2inxt~uMW>&9lR9*hgx#p3L0K3bV3#kLN7iK*L{*3v4=6j zUE^K@+MpsoO}SMxk7crnHRBJ}LWQUmP3oxN$XM;~cle~7U`FZJ9~_)?!QmD@!`iY< zV`&F4<75Ljy zPBkX4Uk0U7!asj~FpIrx`-uX0+gE6rRGjj$%V)xZucCB13t>69NLkRDC{=@zZI+VF zFqN@B+5JW;E2f8v`;7BsEiadIk~DhOy=EBI-rb&?_@j;^b+xeH5u~j%}Dz^7MShWv;%qK5V^a+?}{((t!6@<2Bmwlb%S8x{8+1 zh~R$Zd)Slzw%*vT$<4-Sw`rsV8Vbl8vr!QHZgdnX|B~)Cgiq$wBa7CJ=(}25QY1I& zuNcPtKQYc$VVDJ@0AD6el~)Eq;CxvRDicw};5E-|9l z@k6>l9ErPjc1EU1>MrcKXS0MVR{?PqSlhQm9nTD!zA|O?S+pDsj#Q}22o3eubyK8vv@R5b)MYYLY ze?ZyBp40s%bJ}YfgKM_h2YK=27kY46#^q=Is%TZB``K|k;Mjm!58w14#H}Ow3CC6_ za_Obr)|#(68S^K2aq}RjH{8_iJ8OrP7B)JvL7_ zm+|k8yPZ5(^G~F!<}& zgR2-)U8?=s0RLWx%7xM&bKevu)U`r}X}XGSG;m}Ao|a6{%Epbg&f|{CLhz|KyD~Zh zIikT6W@xVQt9497FZ4GcU`X~f3LJc zuB`_V!ADhfIr{l_JBSlyds+_lff&ynP^^_u==_r7fUS)s9q46oe`JdiDU~|)fhuoP z?15+}Lg}Fkr0)Ltj_vKDx7?_~mAbDh!CzOxzpk7joZ)@EQ}Fjf>Ut6GYI8Tt&L&gG z&e9~#AHtN&nB=oj6_k!}WWc0~lc>pV1237OcVp`SEa>C_DCB~X})701@K5U$S zZzP;xUam{n!BdQc;KH1fQhSdjVMSonawyNVgZOJOMVfo!=EfH`-A8#@%rkDSHa6<8 zTa8l6c1tMle9mcOvVkDtrKTUfgFM;27EK(7^8kvzol{!=dzJHiqMj#7u81vM`>VUm`u)fXL8G=n7td$8C+eXj>AalTe~u;vH#1PA_;`l+dE(*LG-+u)V+<&9Tu%s`jf%S$4c0bEJ*%I zSwL3|2Nh&J;E(Y<4aS=Ay=%@-EX!Np$nJlWn|(e-^Rf;mb~Yczt>OMSwMI-(CFZ47 zu0nI`o`j3Q&RwL7ykKri_+{9rSHbv)F8KX5B0s{n}u!IPk`%|vT|o^sKo>pkwH z?C1jPR##^OFrspbrZ)~RQ~&YZ&DoF)M+1+%=?B}3f&_7wjLJJ6bEt3R4>@t?5nqrR_f=SiJ2a=7LKSn%;+7ypHp9!DQ+&Xetj(>`|9kEg7iUB;W zgeKKN59xAKOfh)`37kpTmdwMPH_Zv{53IGhG^!)9P^w`$>ghv-=erPMuUmQ;b~+D8 zA4{Px4^KkXjnzojT`%<%m~FmYQ1(cd^5)c4%>baEDYs~#BwHR zx$lFPFVR1Kml=Drhb(qH0)6YJK*vSaUC3M7T?nvt^ST7_4A&s%mcn=3a6b{g$dp=l zI`TI1y4mOcX7qL`7H2@SQzAURDWKG)qmSiv>rKDLYTlM1$c4;hR~l5gMi5MbsR=Ou zo$Lvh_Hs#5$5PQH_o{2D`st)QI z1g}#fl$PT+h--=jBGm;1w=+p@ga}7DTjJfbabiDnlzPv!7)u1m{c|~na#uJQr@ls| zxUQtScB8z$qq#PeIj|Q!u$MV_OP+Z10$=pM8=O;zWTH}f`b5uM$;*0;$JO4M-Myh% zzHx-SytTEM-B%pe939AQ92@stgv|R}r3hTb3KM+%-HR_0spSJCd0}+vr^gGJs-2m6 z-H{f%C4{VHSigbs_Ug9I_Wso1X?*?V75P8z z#pYEjivCK->pi33C=Ed`y zX&P{+)z4AlNdhgSX0CnL?CO6qzXeO?YT9Dt*6j;|FTfDc>%Q>a%f({`ga3FF*?eEN z-*$XbzqjPVxmgd1(3tr357ZEY@?BGd<8%ifYOLYvk@~!suRRlpeOKB9wD*!U^@lsh zWG{ijE6-%`>xX-PmOF9WiBafr*`(O=YQx#iBRw|ERnIn18y?wsv=_b_jOommK_0fAA52jFbDr5?pVT__rE>|9K@Tlv zj-cP%#39J>lY(isJmu(kx^%yiwBcQt8>3!ZzFVP*009h#ff-H;W1svpCRNQKOql!)7n@&5>g^LMZgc3<* z^|3fVkRYgV?Mm$@L%YQOYnw8*kcshk)dRl)BW`AoQRYLU?}7NnCi@#$sDw`GKfkeS z`(2ECH+pXNO*&Gp8^~TVWZ%%H*5j^|!UN)coS#pt!u{#?mBD*BmBAb|mA#`@dq#sX8)#wySbOm`AOncGN%j(jK~0$7 z1H?^|n}W!8?96wP<QOg|FgifC5USQUGB9YQtt6=$L(ois?gTy7P8>M^C5Ma+PVh z3#;o`N6$hBiofm#%i6tWvRJXMO)fpGUEH-}P9?O#8})F=TB-kw*46I!I;*1DmZP~s z1~GQaHLeFQnr}Z)*^|bejf|O%A-!opSb1>e!d(%Q-goP%RC2h<>`nd+`s##^wPxqp3JThgDo*7! z1Au9>mnJcP+i`4sv)gsT5fBcplyB&5(6|s-Nn!>J^=LM-HQZ#3Ui8G^8xmu!k#o9H za3D(NjEq>IZw`lhC#EZiC{F4;+6eZVF3~|#;n`bBIY&m1m^pi$BSFhBbvSz{&UcE) zm{tnVVahGWLT=P5t3dE8-G0dfQKo$r5S6UMvN3^vj0YmLU|x@v8V&90Mwvc>ApQFh z#^?Q;8O^ghdqX90L95XB#eZgnxdxegL+E|BevWyfBUPlI(K2#UP4IxEufaP?V#2{aw)W#Epba;bR0~TF8jVh|xeLd$s2^g%ef^5S z{0oi~Ys0$+>AC2P*xf4PZ6cKuN>=l;0q^Rh-!!YEX;HITkuYBoA1lcCw(}&YDL~_{ zYsA)n$uw-QeO$#2>^rrj;)qf7E)tHoDr5D$RkCh$@eC#NB5n5^3H=ED-q-qGeoTyT zVgLAg8p&m=m`*7Z2V3%#?T^)NgNz(bvYaYX*Eb)qpF)anpi!8rGTHj~5&6%VzR#rN zGM=#JzqX4i6eRZqA3*2Od`3js4Yduj-Y@3Y5Iu}ixfoP&fmQ*RtALDvy;qR$0!+<+ zF4OL+4}SY5dQ?wEG|FIxzc)(o@>=HNekUZGb8_lZsmM1VeS3R_WY8ZaA}Z~*;)!L) z$PDlA574W4B=sgyQ1D9$rtXM{f8du=e z;ius_WUxoU1-vVF0P!f8+TtwW0b|ep8}AAEhVb=$P>?{?g)Ur#_BX+ve}&n-_DDL< z-BVw3?ulUk7-4?a&%PTy*tRihZ~xDTH4=VncGmTl$Li<-A70+|xSGcKBGf1934(K|3lsr3IBwF>t`9k4zE~k1zTZBE zG-cy{#LeNPLE~B2_$`A0;-su3L15sWXwPk0og5H) z&0Qr*02|C$o^{@$_w05<^!Dwez=iJ+S8$DdPT%Q5~D`mE~;*w#@^fKQJk1j6^)`7~VLC&LRLzhl_~v*Ufie*5Pj6=Wv> zZ=d4FTi&-B=KmYkfd36PvgA6GrK)$ZsprmXE1G?AJ)aXy_I#==>Iu(<|6rx;uHXl5Y|~Xkz(z)IP0p=kdkC5OW4xQ@kT= zvYoW>Xv>|j{U0eI=i`6D>||D#HozDc9ynJft94zwmQrF0iv8EyEf_|?j8-lEr76FW zbcn1!HXUblOIlodDqTACQw|hbhtJLR+Uv35TaxjFxqk6h zYO5Eq#a-@o8@({AW4V4JB2>C!Z%IpOpaV8YucZ)2%sa%=MT)|&>D8dVT@eK}7%*K@ z%_p(p!tX|mgC>)=qZ{S?D!OhyN!QogsoCEB5;qcROFWX#Q&mGQ%*dmcj3f2|>Qru5 z>=)XU{!ne{1NlPqX{0m1JSm$+vC*3>+)ZXH)x#19~dZO9#SG2ki# z6x7~0a1$Qq-b?~g=aHtTrW##fhBf!h*V~{|ix|tn_WKo&%v~?0qjW8rdz(LUfNH$*m zv((kH3!B#|F5{_xW(XDX8bV>0XnFq^M0WBFCM{LFQM85Sj%?Ey4jc@dpTUk9zCH8# z%Uil7_e5tMx=ky+k>g^Jw8{QO5f8$m-3mU?*r?WbmK`)E*9t-JjK4jLw=QrVYgQ;7 zkn)WBCABW&yZ=B7@D3Nf+B-r4Xy3`vE+V0zU27>eY zjA%|DC4I9!bL!Jrc9T8o47lPw6|Vc$p9T04P23A{4E(Q(WrH}p4_NH3W6~!8HUAmd z3_ECbqSPWAjnNYRzLaTmVD(_lW#vb}{!q)&UGa_RAZf2_W*LwoQVKUPp@4YS+4~+{2@ny8;H~oQ0EZ2-_DuDT%G(DA3$> zeeiq-*pH1NyiZ*!LmO<^#>86 zf23TjJxk*+Tk@F#0 z{YReWe&7EeLk#v?`vI8akvA=D7pdYB!c+UatZ4Eso;$QlQt^nh*Dj=T*R+l2A>F$XV_e;_)HhvqmW)T zF#b5|eS6T?LyE8?XEQ)s`~a|%%4PKAl!qqB&L!hMko?1nD<}OFpk}PtTs0AoO{uH( z1ry#*-W3JMe#-mj%hLZ>)S380*}V-sNf8-YLYA?H%3dgptt<)2o-(0AvhU2eD@)22 zvSi;15kj`X$i9<3+gN98L$)zvws)T2?|I+%54b<)KIb~;T;Fr8ZU)1H2F63fNtwq% z3&%vU#9RhxXDuQy<55a$`BA^e0Qkf`6Z=}T@S<_okUK^ENdCyt(O_;|0IgEJ96RpR zVtdFrwJ*Zlby$yD8n^+>Jq3*0fHcO{T*widjYUDJ&>36pAF=LMyRBDH^bw>GK0oC-hI zCwPj~z7;^;v=yQM(w)3LwNlVLlI`rFGZ)%Bwy?qf7(PyE1XqE9P^@1Dql#V#_eG#z%)5N^eDJAUx*ccsY}+uPJ9wD1#cSqqVn1U2km@?6BL%Dc{Vk}}UflV59K95xV2M9i zyemU)IL2@M-N`6Fuq4WlIUO@ROCA-pTCwYOa?ISndS^k_!?*nTY0~=N*7zflgD_=E z=)oan%x71l&xZ)uK;NP>_I8~_TQaT-E?DV=hC-8tD(voLCkyQwi-LwlWfc>O3j5{P zj~$!N9u)7W;B26YMk>2VQc&&k>#OX0!DQfN-Encr%g zMK|tV0});yit8=kDY$1HBuVJ%tchRr{CP5i#vJ6asRNzJwtr+eo{AoM-1ciFq(U>9 zVpwom!~y9`4whxN zOsv=i0!m1D{$ce!p-Vv*`)SqU^+{|BCMZmy=;0IN6YWMvmXTvy+CDki@xbj<&r+$m zHmlTg+Ph;{cHsARF)u;8V_INTQOJXUHq&=Arl~vm#zvkebzFXKXZ_-QgZRnUyXw48 zFm3;t;GDYPVTJ0e6>NYu^U}&{+8T}mXv>k(9uz6?(>?FV3oR{S%&!O-vQ>=N)_#di zQ6S6XeY-4QmRyZoFLSMs5?}jk(?odqbAvNe&t&iFfNk`D<(_V+LXm0-%rx5&hxA1s zU(Bf>3LGDVruN031))4H$AM!-_H=r}55^Hn(Q62@Yp$F78fEY$a=IynD z02H{nm|jNqsPorGzmhRDbGpcCm8jD<59Sg{yKoOL*t3rX8sOm5 zwR54+Y>)!$s%)1^`!L_;%}CK)R>+)(B9|uKVx>v?NV3eN?2p|yjk7bnA@7fTg~N-U z1er-+3p?17PQ7NtrtU6}5ksec^T~uxWVybyt2Q3IUGz zC1Lx@VpmC5GZ~l&lCw9d@Ba2174G#(&#zeK=;E?y`26PNQbPdCc2|zbMHPfsw`+(H#TNB<>s7^b=p=pMeQtyYj zCaaM9^25fVp3Xic?hKjn!}mSRBDB9XxoH;xT$WP=URY`@bQylNBG_gxXrKpz)D?>X7xX33Sp>Zd&X%;mO1-cW;CY32z}c{aQe zfhoQ$!U}Xl5uXluS1r(Xe=en6AUO9@rMpc3e??J{@vNug!lIjr41Sw*MS!a5RCeyr zyxzP|UX~73#+Z7gl63-0J6-`f+~gI4AJbnm2UK^DBY#k=59!lsQcU!!#3ThRJw}o7 zhg|)e32pT;@nMVbx!5__eK!HE128tV@Z5q6KUq3jn^f3{KI8Sn?`h>r$CzyOviR@s z-zz8{#N3u2x;0?jgT?Kv+T)H~eImZN>ZzrWntAks%=KP{GH+?;7$J5AGImZnQ^sel z7q>vk9t;R;bn6APeUR~Fg80;+rE%bO0v8YWyC8F?64(c?@_f&sKF=NFC4k?qt6`w^ z){|%-zF4IT0aRYwN zQAQ!o1w%ja^%HoJE$wWM{3srHOVaLkyFyLf8^e&Ywjc3)|J$-20dS(IiD1|`Qi~c~I6shDT)U`@z{!-lzjaz*ACX5)D_$PTKn?A^Q$FmD)tk3YQo_6!rdm_A-Z zxTD#<5gF-Sci~fW=|RgzP;l8#Yejkp@M_~_nkNzwCK4J_y1p0%HAtCc(Z&@|th zF-CFPD>?ZCpil1|!06p0tP@#D-d{@f(5`gNBaL8W1I34rgR+m0bws2JKWg(?C|;U% z7*gxge%sHa8uVhK2V-PWyx>gVhgWnkecz|2Gn{DzkuK3nIuXuzc>dw<-~(Q0snI~C z)E$?JO{qFo=HFc2R#RXlY_WaHp19Fm+}G+7mn)62K+nCA%hUve$;JbtJ6d5+SV2!j z=c6DuC*Egw_$SX*tZQ+t-Hwbf34ZY6^KfyGQPfF8ErpwQVzYJ|=qIkBT_N9qry+ix zRUpEEJf4$CXRe~-R>pxVSZ3f#Sb@5&s}vN%7v5e|ZaF*9J@XXSPtkP*BBoLIws%du zaj9uQynbf9Ze~31EsW!r(B3&Aa{fnOg~w~eqyP=qOL`qc)eMp&Z(WYQ7K4KrKLjcb zIuzQg!Pgc0*z(ma@^zm@kL3TF{KCt7Xc#zdPRmW!(Vsd#5|we`?40M#H0Hsdvj`Tu zw8fnI@weUgjN`E6Hzlszok(N4YLp|W=1f8!P*k>(#4zUaNfqt6^%+IunW3TaZ=ZDn zumx#tXnWt+9Eq@w-Mn$wXUo$wh9_(@ZA16`3*+Z6|s`#3sCvg&aG`wb0Lr zK1fNiEX&56sm+%2orH-lLhLVsbSL3vE8(b(@MVeQzd}MUlHyiK-d_fG=@$~(hhCPC z^o@(S<&9~DfrWUi)fl{e9>D^TYuYO`M?kE3sJhE+xueyuLbsfvU#T>zpy>pG_7S1+;0>q?dKCm`#8wE zb%96U>{C3Icv?(8KU^q6EmOu?$ySlX3f|)!)jTS@ z7xl{`#m8nPwA`w??2ifVU1hwV5~v1Gf81cNltiSLlSI`;1=1~g+wwqPzy;5;jIxIQ zkY(%9Dha`*o(oIZ5mAkS^PU}^+K~?SV=5P$82V}Jy+|v17AHBz52$=swh=|YsI=9* z0#7amb@wGpOsit1Vk*D~B^9mQ?+KCeIT~oi)I^ga@`ZVmv0y)h5^KL ztv724W@*^@%og;pb%z_Z%J*H=(q(^Qrr4L7MF}X@TDw0nb&^_J%0vpi?KlBO)Nnli z`r#P$M?fkotrrJN6?i|C$1``{++dT|_Ba4mV7Rpc0zlgeEk)T&{vprmHmdu2+quCr zZiGQ^(e;hzs;F@{E8ucPA%dt&_k8h52AOHS7U#9a_0`E=y8&8T@hs47s$p}l=zVrhNQ10f`*nqw1AMhtz+Nm0eIA|R5I5P1!M4cIGOgWz4!6tS3 z1cG37wi}Zv{(Fbassy$RkzGBo6(#~VFOM}>^I|#T7u*++TVlOC%tQJU4>YYza5W`Lj7G-anb8I^NKz>po3w3+AaL908;2a<^?7#3z*-@jr#wME^oBqcg1wq3mb~>iaufeSW)ol41Lto_7T~jo!#{XE$dqN^e>t&f}{8%wRSSi+Qct{=|E>Lp$?P|9BUs`h}@uA;}0q>-JmY$0R70_JVL;QCD zuXYvgYn)}BhJOUg?9*%AZ_lq-F?|np<$ZR<7fi(})WCl_eXOb2VdK%5zTLWVPVQKM zuW%=<(?j(UF{rXbwwy$FEk^_7NuiC5$z3x%TLb&7sNa`rDtBJ@AG2D)Q#m?_&;niA zg+H%voTCpvOJ^yBrgOL`gnsM5_B4DnQ*%!4QFI*5l?9xSq_r>oZ?yP^nVA#kyRfIH z=x4O@o~rgFvL?uwF9Q=U(QT3W?n5B&4656-ij>i#*? zcRQ^rA~H%}_nFDXX9;&daOPp(DMNQy=udke2-9F@hqQB`im^L?7qV|dh}k;hrr&3% zOK-x@0%%B=>uvy%gV#nFdba$YSK>U?+3n(He<_!1n|-w{FxKeN+!}O}dG(&$BS&xa zeAJBP=j8|OH?|BT3ok^Nsotj0LKLMet{NzV-hce{GaY}U|E(WpYt4pWXFjB>ksN#> zI_qw6{(ZBGMN@9}y8)c2YrEPBpSBVtB(n2=Fct%KL(3JaD0xe-*)k~**PzsUEal~0 z2D8J+N2ap-nf6T1O7LPz%ftj_Pw;RcWk>Mnr{)WMlN?l_7d8qT^J-aBLFuWWT*WZg zjvU#Ert#!@5z$aKqPS7l$z4b(-v&`xYu#ysnEC;wp`N&a44Ri_HlrT*4|ShEth_J( z5V)D9hUaxV)o`AfkYdEX>d~hJJeiIj|In82bgRt|Gdq0Yp4xU&ytkXkbpBVZ8#2B3 zttzs$Zi&_^=imo&cSdn0=$I?NhfoE_%I6-u`Tkr$82{$%Nyxaz9J6`&`6;}J!0*!^ z8s;^4r;b6R`=Lj#^>}sjE^^$BS7s?Y2@(YCq7L#QvO0oC6vPpt`#~l4ei8<&92GoA zePy}MzwfX7bk^|e@1BT6H^h!)SZwDFsAJ-O^2x7{rW#R1H{b^` zzAQ9hcr!9k!)lrMjSFREDu%Lp2>4vutK%TLhLioeDVq*}VI|UZ8hT;d&0n;z^V;P&b@xWuy0*g?u|dO0-`ol{wg1+b3fq1GZE1y+RV0DRiAsqiKO;-{GYNG z-3V3ryC05&4~2eemVZy)xq|N+C?V(m_RoFE6fN7hu7#|XvnokoHFg5;YYaQ;e8{rN zws{W{`-vO(ANsL~(Za4I+U_h~?wt?*N@j`TZ)~ktx?ye%k}sdUMUN$;fGdr4nWsZc zYS)}0KAo^l!@u(K(5q;}$8RfM#^MVJGK;Y9UM)XVP@@0ru&{Szp3cx`7fDY)u68@1 zFjf2HD>)Ofhg7X4cB*oA;vrQtIHzh4emQ*COuP+#^0tuZ+dvLDeGFyaK?Z758x|?9 zZDgwtL=&!KRNb|0WC8H}gJY&Zb?^hJnq|75UShhb==S3A zx(2km=mO{nz$+-iB_c<;Vf@VRS$Hksrdjj8(x301u7-%flZbhQL^S5F zAl|)h+i0=OH3)^>*=Z_2uq7vDo$ScWRUhAiSWj9`cT z#*)YVxMY?J?8{k*dvhCb7~?<{6KtMu0;UWQ$bS|5lkI(N2$HKRN5!ROYxDC%9OO76 zxUeA^TgqY8w1LQeiTX&D#oyO0Co6saJGc~Jwmo&LIWD{T_U}+?$rwwP>k5|Wt_q9h zjG_gKnfX$Fqa=yUE=?@3gajO4d~i@?rQUSXW(!T91OF)W%9WA*xn(ils&;a6Q9zpp+I>1GP2H6`_9E~G*Zcr!9q^N@bQtp$k_X=j6#NF-9)5x45 z>!a>C>6+_7E@ThH+`pi_@M(ul{&BneF8c5$0Cb7C6aSa34<0h#Q3;~Vk_it30^?6S zR9m(q%)&&VvxEP^^F;q0M*%S!2kc&UCbj6fB3qZOTwgD9r)b>BC4~8WTmv2Mubp{B zP;h+RpK7Yfix;wKzu`9Ti*TCK{^VCDP?_OVz9d`F$BT#o6JLtkl3N zztF;JeL{?}ME=bEqVa|99dg;^^F=gs<+16AtT^SLFZFIl?H%!dS2cKO(ad zvdQaSr{~|T)K~+d{B7Ma&b{}ri1=9x)3QRp4SBp567;bq$18a_OdQBfm95TOGrUp* zyQR^()-MVC11mHSx~bf!Q(!h)6-z!gA59mwrxE*|uhTAm(0a{uC6Y@{?$oIV82dLM z8mP3;T+xb0=yeAjv(Ho1=#x8}yB1Bw@)y94jimCo9*6uDYgzYxmyVK$4LFXoNY z2hD;El59*7(dlR8mq+M}q!2Gf+eLi)8f0U$ntA!SXY;Pu&tJ*warfn)To(#^z@b*O zbs4Bfv-M>QETyjO*UUm*O_-uG_E+*l%Ya^!4#<9Wzba;;(iU9sx&0&DW8cX-bi!oH zUIA>_CNr#;R`0xQB{*k!x?FPUL;k?9FtTdG_I*hmXP9lGr>k~drBNf+7td@KSdQxA zM0BJf=Ead~u6%z^5SfmdbjC=!XQg%50nZGpp}5_u@RdEd&3d=oqura`NjhP|Qyur? z(sEh!MUpRGf^5GK^L>2BXs%M<_5NAu?)6}zdPxje{pPBI!*tra;a~L59Gj@;E(Hsp z#Q7_(uaF&i3d`_GOEX;ttR#`GDUQGC;m|iix-E0f1b57gBb<1Y0ll`HvpzRG1!$*Z zbev_iEY@d3XX5i5Ny{yFV=$Z{Nq0H^Itc__`Xef+GtO8~R>S`Vx?L`2#$P{EugX*} z8Q@JO0C7A|+@q6?X zif@Ok0QOaA0$D25v>q9e5tsAUB-|yi==6T@g+Ph8b1M(iF}2%Jj*}Wsjj&s!zbfBj zfA^Bol!hDQm)!af1T7+9yI#>{P?OeDcv@Qt+!~f0QFM)x?3dHNq6E_hZdU~V^Lxw@ zX0VGnvdgOQ6pG6u(=nl0lHEX*>QI#7tcjodp+^p%TZvD>_)2pREvc)8d=QXb+K88D zJ1HtmU6G)>33z!N80$4lp36Yj0+}570-ET|r+3oY@3it;p1qXICj{dY!ui^_o+^nD z=Kb9JC#a;e)APqNX*7RzJhx*x=6>qHa$-l*s(GbKPbmFq%4_?xm9{*3N2bN`On1Q# zBt-AzoTG!6vGi=brpsjI&&12i4G3HkOY1(f)_zJKI0G=9Xl>ZimlJi=h0}SC?fU`L z>!A2(eX9K!6^(q|$Vl(C7haab)U$G0+Z)S}%qJHjz_<>UR@Oa=P2rn%aUex!QZ>B6E2v90KpRo8r%Y=8a40(Y&)dn#r5K^PzV_&_bzJ%kH!Ewa81c>m{}3UkD{X2a#oZ zxm(;$&a|2wt~6BqsXJ+_~1_DFJ;Nzce$sd474`hw%-2mbUu}~L3^Kd zsVZc%!G<~Q<2W^c^KH!3kI+DVu+}HBbodv6ck~EJm=GYGN7rx2K9?9E;eQX!!MxWs z>|}Hdg|zCE? z)HOM=L3aR1CV%XD1zNRewknV7KbDW_vipmR;mEH0e5B)23)=#fY=NSfhClw?v8g`R zap7bb?GSh5llNY+zua>zZ0J zi@m>xhW&*UU49yfIqeP-Xjy;=?cZr+~aOcG(S%G89hFB zT@rR8{9ly_&6Tk3l_(I~H?X!J;5Y*>8@zzf2EQT6ZuH+9_$Qlh@i#uDUU6EmEVwE>;8XVdV) zo-SE$;Ilu%=X@B<#vROgN1csIodaK#&3>>ZGA+h8Edk|{L*%iBAaIvMw~)ohoteXK z5*`}(`ZdCY+Xt80O_kbk)1t$rnmckoo z#Si|9@5RnxR=U3@c_%0N&jU%mjY)y`>cQXC!`akBIZL|SEZf;F`;d>i>5qExk9zq3 znRs-amwqjK)b{q+6SrLmw^I+dLlIY*hyB+k(_1tqxL77=WhSI4wuq5D{I?%OKO%k* zNAYXlYu8d0Hc}D#rXu1v-M-i~JfB3&LNH`#yd~c(gPWKfs+$bV2gN5xS{oW01`MaF z#3sj!Gha?h85_GT7?FN6meVpa&P~#0H$PyuWBMk~|s05b3JR2>Qe818r;zRZP3}kfna#78_rl ziw>wB1za)()LaDs$4KD@ebgOdI1IwHg-eVMqG<@RknD7E*jt8)RzbKYrl{>w4< zXQXoLtfK{`FEn4)?%^HodsotFUecl2t%o;b0L5<3yi-qZ8e(i3fnge~8sl2Z!Y_{L;ZwF3a1wJ* z`&sWpAY5zDE-W$QM_)!P5ysD5jJPBLq9 zW^qCqaj_*(D?RQm1O%BF<~aazN*0%9CYfdy+d?~R+gv0RXOIYI7zt;H!9T>*>R}^Y z=Z-o57N09Gw7;cvt?zDtmdD*vE`-nAx*7`Ao z(a&OQ8$$5r&6}5(qZxPBKl}K7=L9ziZ$3P090Yo=;kn?Mp4YdrJEBE<7=KnhY}W+Mee-K;em^b z)l1sdUDp>+cY*|iq>r5MvOdDB+kB4bunqEck@OOk^qD$CC+)%>-(?(@S$j)>*#0q0 z@J+8S^o%YP0nmw?gm9_LKiA1@^PWN-d9>mk|2s?@f7!LS=@r;5x(F`5D0iI*E^yl8 zNPOc6AVL^1`YuXu<}fb-Z}|(EpkIU7jw?L>1yCS*i~f zCkiGZdYgaf8J?EU&>5eWw`Idyt|$ao8?=io;LT4VA{AkD6%nM8F71*)q>4{VdT1_Q z6qYaybI3gH1n+byj(p^GIgY$rKOzR^nv$vx${E}Iw&+nB{DJ%1vR8ZXXZo#$38z|< zYR&H>|JCwj%6Wj@?&lkjAyQ^^2nV-qY;4;n@LLB|Rxee3nDHTph^vudd_j&IC{vTo zH^9TbM-CBm+0|1Ro}fqz2X_;F7h`f1^t=s}9UqAZvbA-bAQ5mR3$ zm-mtDaJ>G=QSCIk<57tHKDahs2!VtjeF(9TMLytqD||S-#Sio63<`WN=|@Vnn3#7@ zW@-na$hIKF2oj8Ga?`eJt3E!?13vRSx|kUK zLz@wLlZ=nxt{>%i$jv6}@~(oRbONC=h_`076C*?jLlW}zk*!h@Yyw3p3)-S~Ay(y) zI2yvtohaRS<)wtdIyh$Rc3%db%|{rTF#kkja}$FCrxOj;dRgQO%}}JUifbqzYzd;1 zO`KcBytCSrZ?*pXiOL~LLz_WUPVPQX87rdP4WU|;G%EEM3Z2|JNNF!qUK*H0?H%Su??rKH%~U1 ze!MfA_tw>gq?kTqGKXyDq)n^bRH@pE5)SjOP8FBk6_+h35I1~lr*uN6bW$gLSEqDJ zr}UEj#V;sS=LIQc2|C%l8Pq1Fm}<}SUPZ;^47<-gQ7cY-kALFWNw}Xg#S6PT{OV54 zWiPkBj~C~W#EcTu1KE|kcad(Lv}WOst=jQ4k>&{Ln&0WSr=}87a8vg!1vhCooG9L7 zPoASuAGb|mjGti=wHQET3Z`b{j@gE23+|`=M(2Z7uR?OGwoc%rxx+QMv|ZgelB=o9 z6{zYGmu4D?7tE-LVoM{Do#k}*LM9|?e*7MsWTLo!_Y-$l31BfAeU!itaahOeVY_@} zHO=_;SWw;ia?dAga$4B5swH)#Re7YP`Lv^XgJ5rUNmE)ZwLPIy@1QfK&fnSs45DN) z!LTakOBKXfOLABg?4k3(7sQvEjG64pn*KW4)$4f1U|rM?{$W*wjx_V!bR`@|Ryz@k zw)5K|wzI!23UJf}bysK2@SbJ+an53t-jo{_)V40Jq`+w4j8Uacg2JW6qK)fni9%GO zW?V@3Sdef^KPNSYDCQu{KNHVC2UI?xS3QYUKh;(}s~JPhyZf^l`Oc@C zIsgF}^7uS^+dXrXb{x8696RNMH3iq(*xj{FZ~aY}|2(3=x~}U%HP20pX$InI%{uRW zeLI8?Sn#($km2!=^L9M6Y z+AvdVN>8~9ZsXtm_D%h<-Q3RhlOm}U)(thPA~JP!W@iqoz2muWv$9HW>}n)Bwp}j( z59lV(nsJ4TSZ+3JcxS#QFKJkJaQ)K@YzueK_2lk^{abzn+6E7tKgAc`09ppO4$lI{ zg1(@@?8VNU410F`4J?_{{X5Ol?7d=Lqpy&33h^+OK z3%hNBdo0Mj_eXDjacqG#@A<${G{ZpY$HOom_vLQ4;-e{lif&S2LTjUBB9Yq1F}c`Q z5_tyE18sX6h5QwcLzbyohnJs_wqvd^YOUEOzEr8uD>8wdyz_$8h+^rBd=GQrdf2pl z!W^o|UHe|N^Q%Zv+osTOiMUTza-Rm+=CG70Z@Yq9ebdwFu$(fV-q%QC>=)C|=A)NwO(Up^u+Gy@&qvDI2=Li;%K_*Q&PJ zGvj0dU5x_!&$IXG@#Vtqwdd+7e@J0X=$HCs5_b1p|D@`NV4(pg&9k*POl~CSzKLN3 z(b7!WF-50=L6mWxVP8hsd3TqiWU~K043Q__u)_DHoJ)5VkdWm-p<}w@)}EwQ5sSQ-ALs2X$-Mp zr=&b`r$r>K38_Jhv*LEV57MEZnw>hI#Y;ejwPCE%1Y-9ZC^5@p?ya6w)=Ayn&?aJ# zL8%iuSNd3as8ium5KuU@PYAEA-=Nz4=6ro=gEs+LQmt){qap-uW}_P|?-ofHU)sEt z_vMo-__t2=ifxji?5uX`0?$`+x+~trLRsIa#>LT!d50h?mWgUsBf&C%2+^q&cP1T* z5nD405na3hS3%z!Q>5s$lO^U04-cC}n&wm&R+(RaqCBIIPJxES0*D?GRk@>pg}#== z*9~VQT+>FmDuHSiE0dboG}%1ZK#ZXvXsht}^gkb6d&;vlIY53|#^{o(Xq5f5ik-?U z8m#Jk$I8`#_YN*MQAS($P>42@Qx46|8nC=&dv^?oCY=S0Q(OH(Z&^b%`K<5k+Ebn7 zN!5G($K;O}Sb)_<7-lVI{^11^gs~CJtH+sC*mD#EPKYz77pFt}65x-(IW~ghLiPOv z^gF(uKca8rG+Gzi)DocrEUV8f>FD?3t)e*it)RqV8jG>Y7dnkr<=c{$tzgHF^=4=i zcm&5F+Wy{Jn4FuOWV>^3iGCAoui!tOKkrEG&xwsC#dp$oBw z@0tCh!?h1MeDh3&FW6B+;g1Fg7V5&6g@0;T5&M=f$ ztDMMC{7wMxE&@Dzub&TnwiBP|ZAOg>jilQ?W{-f21k#DU$k9i+KJPk&R42plq66*s zwzqR33!66U82P86UxS})DiR(?E=4=lu-w3^LMgnDiIi<+#IrCAoL@4UWV)AO%NYX? zBprV}h6UF||8}Vpr+}`B9r^JyreBu1GoLfD*aiboPKE2?O4~@4w%t)kjinp~oH9%l zAj2=dpqorP;^e{L0TnqFWv=M2+oG89Xs*;;SvwaHt!e)7<5=#bQfrNl@iYo+=$cxR(#Q&#fMgb;yCLJbgCV(lDDTAq;3C#4rjC|uNS6C0b z4|Pq=>BPQVG*7+3a>sPXa>sVZ0o=d1!@8FCQ^*2sa@Zb)jGLQyIS5f4xSGck!D;@;+ibz5N7SJ90D3GYq;m72~y$s*l#$x zU!tDL$;XktyF(*55<=#yu#W4o7@yc*1lQ%bKdbam%?Yt`s#NfPQzfCec~{>neDk5O zAZ?O~7!SAJnS7AfxoK5#N$p{Cmbc>T@2h;D*^R5RNrHiOGCi@cz)Bt8B3I^>L0ri-V-U#SaN(xI4k=lL9Dme$<<77 zZnCajR&VgMTjPs-nZclxPMu{e3pP3W?e4aj8FI|z!EYeFZUOc;Gijr}oYhMM`1Usd zeoDC3?3$5dRMWIII6(Ro#@{cOa?%w@4odrIl|{npcC6^4da-6uO(;97;k2fmI*zw! z21P~kU{Czhe%;jYj2IIJlNjJ0j#9VRZCU-b_cHquz6vZew2-W8!=1{wICt`VUdcU^ z^LM6cF#y)J5l*#SZj}_AD?U4U-cN+BfRC3c`1;ZX`Dfd`p%%ZUl3if_2;jEZj4z#G z@KLXBvD8Z=aoCb#ThKc0Fg+~Y8liG(=Pyo7s_n(_dvaPlo>}mM8JeYV=D;*BK^M@X z-11s>Ty=cwywkOMeF8pyQMr6Fxp|NpJ8xK3+XSxDIDV|A-915A{)wG)t_q_N{Py$w z{B(sZ(sf*EIR_<5LmF`TV5I{!?)MGO8~YN>+lA^OyS+K*iID!3wEi}V7*W(JitpJf zC(Y+9p81XS5MF1QO_Y(Ayh7K!y#ts<_dZOXIA>~6IFsUPs3Z^W$b?4jBI0svdSC^PM z#=TaBUpYyNuI~SaYo_G(6k+spWBxZlF(HDRCRRJY19zYHCA1+OuqugPQHWy+-^(+b z6--LSib_;zYh$b~WM*F)S4^!SNW96f^yOSLj{zJTi%pHcB+L^p$=-zx)*MXrXwZ(P zoK`rv87P*z#oXf4P<-0JWhS%y0adA&@p9zWcI_WOCG9*L5kWlvRCXJlj!X>9(oNf4 zx+|}{&KJo4s@%yg3>I1p>k!{){^R7CpI|0>8-h?|C{`#YdrOw<(WOE>`XIO^I_pvR zt-QLjx8pQ-p=k9;_Nbc7J!89>2F#2et^Et!e`HMWF(cM(S<8-klEa;J?KJyg)wIji zJM9nghR_ec#?Y_BI;_OBnI`D~YqfYjK4$%nX5{L(D6dZc!Bj+C;OM4Q}3v%TO(J4Xy8M@#U+dlW9KoYb1QgKZ82>3es- z1c6x0@iehibVRRtc&nr;aR4uFRh|Af(0AF>WyGjJD_;$nRojc9xP9P0a5wZcWaj0f)DG;_vymTXZu?6!p#G| zbhoAjAJdi3m!}qQhl3ILrnw2?&@}?HQr-Mm`83o|IzR#HF}gf**cQk zIP|YN3E}L-Snn)8mrPTd`01@=Do?7{Gs#-x@jqM9zZlW4rsrp8DtsefSX+zX@I2$hPmr`!BUA$DDuj>a?;G?0z91bB^I%<-YJZ^vsG70LdLa3f!JD zQ^y~Op~MeHoswncKiuUiK9=O)fjSNiK9|oX6{##rdw7Dhf+NcnpVN|xsQGL(CXR5)LG=_9WVl&7`1w^?$bibtiCK-DP{T=(@onK!M-j9r9`pyzpQ3N`t>z6~q-^+$%646$G$0f2pea?>1K#NbUgGd8viRSV*6`@ z0xGm}XraQF?;w-@pyO>s3b?*fb%-BuGN11+6qa2YuL^yuS;zEai2r%AD*7WGPhqhU z%}>pnNwFbg7<6+|=^?qW8p~YsEcOEne&4yL`@fj^rL#z%FMaNGVfP+|8QugYDiD`e+tv{nWXdusu=yT zi?VTwy5QN)ph^@k5l-9_Yv&;UZN{V%Q9VqjL(J0^4jcnUj7>&- zw6(xsm>!;A%r71jczmyrroO2r!V1y5gAKs+ z@YE$G*JIAprOwmiHf~>CJ4_jwWlc}cerm6mt#RxS2BF20Wb;rpMEJi9>tY3cumEX$PjZ8R}ShTl}+nu zmB0{lF;`6LBFt^Rf&$|>=ggPs5%Al;tNe5BDiQPeuxIrMQXyN+l7%WU4(`%SH>cq- zSZNAUrnvQ5@PGUQY`-LO1=2T}`oYHoXz|jRxuIEKh|r|0QJJlkn}Suorkv7_5wOfK z@A64+I_#KU-`k&Fd)_{uJeBO8N9~;Ru9MXW($sicGT@gp^a}|@!9wq$KnZ?=>6BD> z`wa`)JQEp*5r0kPvlm^X?9scTElM`V+A+|f3|>L0Kmo zrPrYi)K6ieIG>M$y2+m->v2`RXiEIPRp88ceQD^@oO{oI#%38ncZOu?Z&iWk)GjxN z(zXWs{P7L!-(_#%HA2x&CQf7uLilVV7`aX`A%nU~aQ;@dcrI{WS}G-Auj2 zU0`I-a90E|&&G-1qTW+Q1^hUZpM!;TU_QP!6xQCnx5JC{lH`QxTYnFT3bA$v&4X;Y z1z?wx5pLGrL@=VH>hg1jy25ntw{fNsMDT~bF~WkQ&Vzd78Zt44FTwUBIFICx1;SEoUj@; zaGqX6;1$>?q`x~LUw{)qrX_#W2H0FeKjs3Tj4T}UHL7W0ylJRs*Nn@R=lf2ZC1@r_ zd=okZV|@l`08~c^v7ZZh+v;u*DI@{|R@*sN*O=o^?z_B^4a4|3WR%Qf=&bqyelBYq zYco%+9_uA!Dwvqs*lFAMkJz@+@JZNeEfk5U3^NPoXl16qD=;t*a;DS_Uhqjzfep7d z!n?&qvNGpMiUwyJSJ6bq82P}HyjNo7;~3wH%R&5eHpN^B%FNzB`ZkRd z%qv*j2gF?H-#9iV=g;my!W>V^He5sIa=573>YFs@f*Zdsj0}9e@)?>M+-6$4AhmNX zY2c05N>|fLuhvR8(8A~GW2(K8z7FnD`#FkbBwBbL095KN-p7I0rR1*f;A)P=9&R7L zp;0YPdW?y1yp3K+@#^s{DU{N9DnoZwtl;2l{=lO-NKS~9$Mmz-owY_+|OJG51{nNnhZ z20j_DIH)qXjVQ;f9o?*>Bgo+JXc&A48lQ_vO~-VLZS7)zOVzXJhUUr=goJ9#bbNyU zjcW7F`vxfsfxPR=L~c5pIpka2g1!__<8z_?)708Se*g7Y=wSxeEEsZnCBFoU;eB+< z09010g?PlUBcBE#p%1ezn&Exo+6|9J^4GH4R4kBFfISeOnar8 zK4Wx>AR}M?_MBj~FC%>w45oNwez)L<1rYGkvOci&LP)Vu{y?5363SF}Uk{Ej4gFlv zKZ-nONMmhUaS2M2ou9*_V0^yviBW(0OxE;5nU+|((mTVY7LZ!vI#N_>h2Zy%_X44n zm5Wp)#4clX81#)&1gdB92*XLQ@jvwj6S?q&KUrn#=F#hraBS6~tq zon~;{J$Bp7+;Q(IYq45|WdbrsS2RdKUUN*LUxJC8{?!U|~yFVDfmbP->ZfweR`K0%%HNlNHaWleag zb2}n|4MnB%q?!^I*d%)6j?2AI^+Y&n^$-QuAb_fSGt-H zn+2Ug`FmxjQ`}*G`4Xc{= zgfW%oEpHd`vxJ4S=(v^lHc%UD*ywYK$*_Y>`xHgL1{v zo_Nb|-P`1W=bF+Tdo<39U1O|S^DOk5p61;K^+jI|TkWqk!6>FIerUC(4^c7BhC{pz zeFDT9=*kBA|~~$LnaOu!|f`l zd30mL+fe6_i@58`!Q@&$FOLkGX5PUV4JOOiZ^yd)+BZ>E+liGHEzjiHM^4UB)OsBM zoiuLMz_!0g5EE1Y9qinv5|uYVJ2UiMc%x?oxm2${dS~^?(BlHDeJWfVBD{^)h}Sro zygV%iq%5EtqN_@Gt>`7h8jd`4-j#KEV$48!_1XJJnYv;*-sSP$(R7gAd}!rTX3hHS zl0)}IE7+eLP5xzBPD>)sib``zOuWgi)!0<3OYzId2RRC)q84cb`hH|D{%E-4rlD;i zqwfb*0=#kUt>6KIGrb-#Aq6O zCqqy-wDmT&TaTHTju+Huw#i5%F$;JZ4t|p5V=~uhDq*#@d2}a+#-&5_-ENfK94PtD zj;*#x_vLr+W87KXVHRTht(eB2fCPsh@!aEKP!Sn?6ttuZbH9o$ z4`lGo5h;C1 zd;y;eOMTWE^@&g{{IfHaWHv0OxV`D?`78H0(5N>uTglb%uK~L=IlpQf^IXpr#gxWp2$_4Z!yM-;&p+`aHhl)(St8TjA+ zU|4=wd01`u`>@Wi!LZ4&#jy4NI%1olq0evdtdE}?iX(hznJYYpWKYYpQIZw=>* zXpP{DY>oWyNUPeg$&u?3=p6Lg%uIh?`cF|E_-|GH^m=<0w7sR5FK?G+YN z!YpPACPvDH1@S)Da%@>98DNUY3}eajKMQ2Hawroy-*W_XC^smwM`u~3yCd(*!@Pt) z3EU6aBYcM9)cu@|x<|W_ZxL(K8eY!ix0=DhF5QjdF4ko38tfJyofT%_g9;{Y;_|sa*0!}H&`M0k=lY=z|OI^vI_{airueuqlAC#l_v|Za1Rm zDeR}5yr?qAozCt!&4tFItXcoJ7R-8N<=_KyvIIN2^dq_K^hz}wJ@+rkcn#2^)zke&$GJpTG`hh$B1KQKesMksKY|>e*yg6W@AIq=8lPaH*T!^4^ zxO{4r-unE^;r>+W=&^qr|GVvy^m_R|cNHA-fSfq3EvV!SFSRU7>ev@Otu3YGOfR*( z+;;U$QOHBOBKGp`uXpf~X)~EgJzQ9uIQZ~(Y@@@;N#4N=LWS{b$P)`L*N`IB@^Gn$ zEDy{??hyU=Qh-)us59QlG)Kzl>-wbXdc3N5V6I|9xUo^v;TtxV#q^c4vYF85Mdug3 zvCEU{(>Ail3GWF#eopq{h;Y~?g`D8NR1bE!gQ2fzVqEL#(b3Au>%J9JJDm~+*B@bpY+3h0|Td2aih8n;%vHBzK$b!wIk6W=vmzm8pRfv8*nQ_)nVAdZzBs5NgsDK-F9H2 zzK$LOCq(mAPe^{2pZI zF7_X;V%pdUB*otz5tLvOn5#~Al^qbdj}A(uGqbJ>Sve0x7(7Nj8HSN7d|J|mPLhkd zkzYcjJiwgY!2=rSB;BST;cn~l6q26rAI{r>dOh{#ec5mBf|Lif9XQv>_&#-XwfgTc z$&|n-l{D`rR+6oEAA>m)(RPh_cB#7O+r~B}Gk}7Zk}>!^!_{D6WNy`+&(eor@2$x! z9*N!*A$^>EGxpjyr#IQv;vR?;cADMQ2^j48Y9gP`>B!nzyKnJi z&(mZ*9J>>xJ=}xK!|xKFa`0bPg4GNuypFortI%@#`=h)$ zqhHFc8M$8EoC)ti3|Jq~*DuuW;atjN53AL%jo-do#ft^w!~$>WI@CttGN8#pmA3=o zkNt}`BW>HaCAVs9o{KLNTJ-%td$%gYMU1U~H8ZCM1b0n$Ty+d*B{aWR)rxa7c$j~V z>)qrcvHFzHtet5Y#XC7aHQ%sPN5p=$z`!iYy+k_zA)vb4+`=GniK8$yA;_Kak z1J2R?9QLkLHxszD*10+CgyKIvPA2Hy3byyGS3LVLRDcrIMYxwpO(m?ublsJ8@9n*b1rUt*xV9goV>1s1D&{afps z;1&83ca!HToplpi%S|HVK$&FEK63z?vlX5iS$P)f&+Qv?@s~3<6Zj7YExopy{hLcvRqa)U&f{v02{w zS$C}B(P;EAmf-mMR26p)bQatChpZeIY@Klo`Qdr@ef;{*ih9Q+>-sDTWR*L#zRuFb zd5y#HhWAbrSMJ3(q-$E%>%ccT2bK-f7-jeeki-+@n1P=HAq2vXf@FE`a84tgB6+PqtmAX0Iug$cZ{G5>GhrG)s5@xVnKJk{ZP_CdEI+e z{X`YsXaD{cNiV8%8@+!aiTH7`ZwLo`Od+bl%#_T;^*17%)dHp%ZIif@_>;H*M*CpL zz>^3oC~5l?9k*xUJ@_JO5ZM5+DOMs0I6okjMRiMwj;~^4)j;zy@o4!cy=bL#^}Y=IZQb zxSI&*%^xr4a{v`4>cBJe?)5J%P+$N`07Exp~L>ePNF~0ZAY{IupTRmECN9TXuFT5V4;z{ zp8+I`DEG$2;sKm6a4Kv9q%tK=RXvlaA^Hhi=1j8iKQ+YHgwt>`_ajD1Ul{!S1v zmJweVC_rhmle?p3+z)M)|G6cxQfGZb2R3O17o9IYNAB1OIkAujvf(hhFD}L7?H$v5 zqhsc-Bze{eQ~zEJ3p&S(5o_;p zG4ZMC%xlcueFv7wbt9GFn4>i-z0ZHlF@g~o2cd}AYd3nIO#SPL zo|7fKa?6loyCR0pmDL3(Cx$_8Jat^6IIZqaQ< z;AA?m=@b?Dmd~rH9Cic*a$y4kOPYo2j{N|`~B89z2) zx?N2lJ|v+1ze4~23Hcxffk(Q%2XpHZU9*%k;4rY&-POG``?Ifznn}$-3py(DrTZ%W zuQigL%u4+ZJ7_w#qDHht$kq4~$J-H5y25Sk)B@=J#x0TNQ-aLy0}>_{?O$H+o#B67 z2CkrPk60l`UdFM!8E-ntt7)I=J25!^GA;W0D*um}J1YUA!IJvmq(&Lq81fqo}a{p)50RKm?;C_2?qbWuO z6IeEC8tY0!io0{$#>d_vF)U=zWgnPwlyT{u{vl;qG751x5XD zX-fp1lOrO56?;#J2MEHc=MYubk7&=Y|A()C3-jmB>NNJteRTctuc!Z{AOw`R_x~gf z)~`p<(a^?0e**%h>C{~>^h-Y|pA~=XZKM3Z@Yz2Z?Ko-c!2c(kSkkT7UBye>%&+iN zuyZ;2q+3|rwUt>2ICD|TKG#2Wjccj>%!I@NHN$JLO{WwhM_se+7uLQh6U&J4@;AdM z`WM|?0t>fBV0zxQ%k+a26bPz=$cS2 z(O(c+Psb-h7D9NCX#$*eKu|%<15%!%+~&Xz9#>8Bi~A&T(iz{f}5xR zS=#@bunm7@O2BKX^pCVVC^rlLWK$8wCx<>wV|XJVv{!La90{OOAP$8`{8(N+<@Btc z?wRMm!Z5%nuk55^^<)s)R7)H@4@G`VFyEbSrs0tZsxs7PHn1!BE2>oWT0^Y;>3+6Y ztsecyo3*xwgeqCf2uuy*<>&&R5yk&bpuaEzkAEf9$ioDG2Qc_y0{+Hm0-^g&nq9xK z%tt1LlSNxDPxwPvvcTPc*_q;&=sURuEDHR~-g}85M9)l!Cv!Ky2huS3%XHX@ae+sb ziRZ^yiqZRtHU@~}`dfm7M23cgdQsE+S&_^~?woH=0Ye}@(-YtLvB(c`V{WYU8G7ds z%cvd(M8um%CV24<%623e_vfBJ{7?Ln3;DjwAYVRDiH^U*Fd87!4%>|^I1JN`tAO)f zJFG+^$vC}2&Z+6a7bLd$bz0d^c4AxFd#vYC33!6dln6s2{`4q?lu~3)O;l51 z(S+j0%2lX9y$sr}STqKG^Pao&c}FB_9zxshj(_5MQsgWA{bRBnZy>syLh-y+ELZI)E>ooA9rEk_Mn?rq(D-w}cN2R2 z^IzLv8Q=mAVq{Zu6zEHlJ_>f^lqCC|3itoSSi$?IA@sviB5kV;sl}>-0ZDnG1DHs7 zy~{%~I$l=hOvT2P@;~h|JMv{DBlkr|`V<8wAl@(RO7`u3j^?D=E}o|Py81K#IeDxe7Z zaou@f`~0^4<#v|&>0jaBgEF|Inwc_o`SCJOrr7Ng>lAPI-;9AmT989qD3rTMg6zbP$z;6-d!NS_f5Pfk zX8=@>s)od2T0@>1x2$l;jW=O^doe^Ku2qIIG4F_-L8A{(g_*yK3UN~Sj3bJr@DI=M zE(jT3k54_QQv>YXeT3U%#=%cU3}=8u;gj*!yfyZ;CyNq97To@_y$DWNAwWc=fs;Os z1X=}Dcp)U=O{a+#!1(3;8S(C=^(%{j*ITJ--zBGPKd*ESU8s&Ms$-Z%{L3OZHjaR5 zRx!!Y{`v}|4KlhRLHAb{N&C|;D=Eg%&-ElSCo-XS_d9%5&6YOF=KpNAUKI?k4irGl z30&wp^(BBs-t~BG4W@P_JG=z4C}xwcRS=Th51Z`96YDWgvtCOqUvmK;zWc~BId3TG zUWuPrTwkhiZY&$b?N1R7**AFzMzcW{xgtK^Nat6CY05k8_L=9Gg(64}ePV=!f9_d)VTXYL>4z?LnN7eSGjd}n zHDb#T+k)#;srOr_TK!h*D+EIozCM0(wQJENh^%GBL8=X%l{1Hh-*VE=6bjFa7|-cG zU8)EEyVX74<4B(kK=^1CBXxm)?6V`N_Dj&?04s3ENuy#PIvm5Q$kVea=+y2nSLuQj z7wO(?iMb$h$?kegREEZxbnz&$b)t~Ju$jbHy@LA6`?)~g{!s3M(AS+dK_MU|rix^a zT6gft5PQFC&f2>JWsNVclghL-Ksm`tAs+g4>K)b>|7yV@S^LvJbT@M9)HNA8=^pxA z39Laln@?r!!^rgU4=YgqbtV;0+p(>_?vheBF<<7DVP>9xk+_faF92A!4EoyK`y%*T zVrd88ZglW11ioJ;j$ZVwjA4b-0@%fOU8`iPAz<)R8~AzRw)XlyGqVD2Z%l50Y&lG4 zyO6aP3JSmIjvLv$~AhBN%+`;(EcR`|HT@_&+eM^QJApG00 z&6WLwAnyJq`NaY4h0!K?*wX*2jWhp;s(=4Dw~)pbWl5Go$TDP^gi%T=Ybh$s$da|O zn{0E0NYc1(%iM*eNg^64J2QopWZ##ukFk#|!_1hOkLu3j`@{GA1LyrdkJov=Ue|S< z^B&3n+QM#N^z<IPv8kOcy_G*>PgiAN$L#Q1tl{>J~w)zQsJ#G zL4@v?5>x+bMe0v?;%B32r-uG%az6{bJ^pnwd67nqz!?Y}eLx?FLr7npWdP z{M4>yuZ|j2|9gU(&0wLWu?b_jn8W;3)2GRd-rgsRN>DL!XNF4|ynv)@@bK@JXH$L! z-=x8}7~Da320)|x2D-mRA^RdJRvnC6kX+2fO#KQW)Z|h870@Gz#W&@Ec$5&m$rD~0 z6Trkuh%o|lm>|XSl3}<*z450$Oh43%^s+-m=-d*7tS*wdV(-~l?N{;ze3-xMm$RS# zE5wsk95XY<-LcIWll}()J*QRhJ_Rsm*1%Fa?6%whmeC7a+RLQjkEq98LbpWEo@E4P zNR=bro~L>s{V&phGRfi0I-SURoR2qdJI~tAH?fM@X^=pietUfw)9Ck9O?B26{%Xi~ z<#5$$s`sn?j&p9BVypZ_vPaAi+hW}+DBg%)#jqS_IOD4D5VW|l6I%DM z4@$f+MfFm-Ny`GMv+w4T##}$F(xh+FYSWrXIW=sew|Vbr#&q3ASmBj3)b0#exud$M zFAjLV+3)qv`~fp|PEkpG8B~ToXuMHB6X2NE27$i(+}{OWi#?mJ#_`YN6Lmu2eD}7Z;_L`@*dV6fz}lA`O>*(=L@$YA zECFfgXXD0UW;SJvKZL3#-%$?5_fnPcMqu_E8Z8ZD&W&^?m+|YV|0$AE3DUc^7 zp*3KS0MljGlRNQl+*5QjEj20$$`HTgd#i#_9S6h|gK<$^-!A6b$~+kI`|hBiH-Sqr z5}euzcM@Hvo!paQvon;?YrMwq9m}@dD^AA`BbOP*p==^kux{G91l-{ZjNsn3xs9z| z&oXm(*o%6S(Z8sCUL&HMkarwe8@ELMUUmz7SM~J!?SOOns~5#9?lBMN$<`l>kEfE< zAihM$lnmalnsu@ z)}=D*?3-(Pk+mN{6suZ5A%tBbT{dDb_H`w=Q_q&60>Il3oY1XkZ|M#Lwu|l?!5{d2 z-Zr(PTH76-+)FMmL+|`Da%gJ8na!$v2o_U~8v?VXCb`i7f03qDQ=hp%7xrn-> zHyAOtXhDiO{i5z>x(N}I9?f{sIRZ(vT9${PlI9NlZodJSHI+YZJjKA`%@)&+iQ@w= z8zaa;P9_`ldfQN2cx_EH(SLM#OA&W=@&#r|&vP=olljz?o>X7 ztB_{`k|YQ zVD04Le9-SSKH;1)gHAmobr6V^TH*X5=!{DdxDcdy+Edt>+eJoBjMGI!nTPYdaw}h> z0Jg6A7w|N9CT;&O246;J3}4Yk(V-;{r`pfW^DT(}2RmfL?oM9e?;=xcCTg?WL-=|Z zK-PmQmKu?y7bsxl@kDs5Ik-VS^h4-aOqJduw;%VSJ}aYe6=Pjs@WKapPK+OU0f0*3m=3HS>Cy2P6eS3K%tKx!hJ^TpcWqPVfzM*g#q5MAq zSGBY5i#QA{ncHc;1%Lw$F7UFt*@zCv`G_Ou&lYf>=ckH>OCmJ***-{**3IQTb+Yn( zKTDWD9QbtDljR;eGv?m0Jvl|eB_C-rTI$^!}p}JPZA33uY1vOL%dak?J3`#@o&6GGaB->dK+4cF3v$gu> z!L0D$yYMb+gFgT>XBB6{wa2;BjRU2M%g}JR>hVw{6G6O?;Q0I ztU0jylP|G+zUO>$V4pjo#c}1<++HJ4#j8-l*_{c)nHFQ1Ck>Ni$4a-U+FtnjPV0as zq=YhD(dt4WaxfC3G}u7Mu|bUGXtv+<6VDuZdNuh8aPC7?(N=-DOn<|YtOZ!V${+r^ zE@l*-|5@Y6E;Vy%E1=fbj(;5`vJqu@-g2!wt-*Qx>X}LC`j*qf9sHerGbSVY0PfvS zm~}28{pxy%O8C;a?3r4UX0#X>F#WuY!du9CAXRK!64Oa{_1{6^Pc6FwI}-*k@&Qsunz# zpZy0sitN5t+!KnIapFc}po$`xvH3;!nvY`E64q$BqUr9*0plA159G6k`<{UtD_mGd zRT;I0*_&V5Zw^=kOh)*tl!B@#FS&+p(_ss*WxR^#a?+7;yo$|c`sKl>%E=11lt%EW zoyYlW*0kqOYT1o3yO5f~)Cwjh!OAn~lSIqq__a`Izv5LK*w8&+r)niVi5_4Ozn}}Q z<41QG!quW@;DVkkCqKScf@tn9c>RPJl23=-cV-Qzk-SEksWo*GW$hbOD$Y7^igfQa zvrKVY)Hf<@sU?aEP8+C4?1q@aCDXB3yw(s%>(C{2O_!3Kju!u*TU+oN10Ot6X_hJR zZdq#ahrv&S{MCc_Yls6X7s}zHCpQz_CHHY3DRx>iS7Oh<&MwN)_c%GaLs~M0cI1KF z^Xrtj>(+kVng;-{8iENnMkaJzQ{g2sdMfT@u&cx{ zcvGS%+(p(o2K+HV)+Hg4%Vbh>vdJsE=%!Tu0?)UkET9p`sv{3jJP6^O9Cip!3r715 z!n9n9$_>cNwrInN2!G-AB6oeI-qVJmLbi{IuMd@9lJjp zbGJA2P{5dvwL`L5nR?F;mr%zZNl*AY(N5%2Q6hTHViB>?kri+i7+`Dp62>vZ9XV*P z0(-K&nn~cQx&Xp`uLf>>wX`+IHDHdGEjwPJ$pOKRKP(*$mh|Z9ZW{Y?pr-NC2qk80 zZL)N9v+b3HM>Lj;!-!xjQUPoPqpVd z2sJQ~7QZZIfB4FRI*;z{vfAYGt|kw3qN7#ODY5mz$bAAP%$nl@J&(QWe{@Mw;9t&k zewLLH#|YVuE6u@^G5Q5;Dx+mNp&k6~FM?Z^4CFYh(eh#+h3@Y9@x-Gsqksn+87??k zK{QJ9AfMQ+<=8|&ye5aaMiWGGgf54T?cUjcDu59d~#chADg6|Fy6y9H30-(EI8K2 zF(sSKdl6QBXZPP#;zU`|LDSQ8$YMcFPLiXXCCe!$n8x0@IqgCQ`jNKm#UXSUyNgIM z%}NN{45PJfg=ht^RS8|sWCa`EkfWTptW3w7+hJ{+24xC#DENjGO_XC9aHJbO9b4Mty& zn`jW&FW1W3C}yK2s4VbTkquN{goo4W^;WK|wt@FCGyUjwRn4R8+WR}&zkRHi`-?Qp z@h%_4#dATOzxk_08_;I1t`VPfkc<&i%;!-Q^Kh$n6T>LXDCWm}K0}pC8Alyc_EDa7 zjhhdWdoHApIx}e~XJobQZ&AejI-_|nOgKtLVU{aTh>u5N%LZ*_ds~Lb^ZCOt=>+MN zR(VL)UXr{oO<3X>#F^gwgI7)VCh7R>hH}$MkTCxw)A{(iDw?AgmEuGcVL4zlt! G*#7`Rp0p_d delta 8692 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 z Date: Mon, 8 Sep 2025 16:54:33 +0200 Subject: [PATCH 81/87] more stable extraction of version in FRED engine --- .../matRad_ParticleFREDEngine.m | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/matRad_ParticleFREDEngine.m b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/matRad_ParticleFREDEngine.m index 443ae7adf..ddc10e684 100644 --- a/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/matRad_ParticleFREDEngine.m +++ b/matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/matRad_ParticleFREDEngine.m @@ -468,12 +468,20 @@ function writeHlut(this,hLutFile) matRad_cfg = MatRad_Config.instance(); try - [status, cmdOut] = system([DoseEngines.matRad_ParticleFREDEngine.cmdCall,' -vn']); + [status, cmdOut] = system([DoseEngines.matRad_ParticleFREDEngine.cmdCall, ' -vn']); if status == 0 - version = cmdOut(1:end-1); + % 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('Something wrong occured in checking FRED installation. Please check correct FRED installation'); + 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'); From dddf27c2590eb9744c5295bcaa25af9c9b5e5960 Mon Sep 17 00:00:00 2001 From: Fabio D'Andrea <104505852+Fabio-Dan@users.noreply.github.com> Date: Mon, 8 Sep 2025 20:14:24 +0100 Subject: [PATCH 82/87] Feature/vhee (#828) * changes to master * revert * dev edits * merged Fabio & Louis merged * Normalize line endings * Renormalize file modes * Ignore DS_Store * Correct File Modes for MCSquare * incorrect merge revert and code cleanup * Final cleanup of incorrect merge * fix VHEE stf generator name in matRad config * fix some naming issue and GUI display * Add TOPAS parameters to FermiEyges machine and facilitate use of the TOPAS interface * use physics list from publication * add paper reference to VHEE physics list * Updated Authors/Citation & Added VHEE example * Integrate asymmetric calculation for focused VHEE into matRad_ParticleHongPencilBeamEngine, drop VHEE Engine due to only minor additions. * revert matRad.m script * matRad.m script now also includes comments for VHEE * Single Bixel stf generator orks with VHEE now. * rename example to keep order * Update Normalization of machine data * fix tests * * add test data * add stf test * fix stf generator default radiation mode setting * * Final code cleanup * Decision to name the default VHEE engine "Generic" instead of "FermiEyges" * Added documentation * Remove commented code Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * add some basic tests to cover VHEE engine for both divergent and focused beam * more comprehensive dose calculation tests * Update VHEE tests * update energy handling in stf VHEE generator and tests --------- Co-authored-by: b96935fd Co-authored-by: Fabdan <104505852+Fabydan@users.noreply.github.com> Co-authored-by: Niklas Wahl Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .gitignore | 1 + AUTHORS.txt | 3 + CITATION.cff | 6 + examples/matRad_example20_VHEE.m | 93 ++++++++++++ matRad.m | 6 +- matRad/MatRad_Config.m | 7 +- matRad/basedata/VHEE_Focused.mat | Bin 0 -> 19542 bytes matRad/basedata/VHEE_Generic.mat | Bin 0 -> 104925 bytes matRad/basedata/matRad_MCemittanceBaseData.m | 2 +- .../bioModels/matRad_EmptyBiologicalModel.m | 2 +- .../matRad_ParticleHongPencilBeamEngine.m | 24 +++- .../matRad_ParticlePencilBeamEngineAbstract.m | 123 +++++++++++----- .../+DoseEngines/matRad_TopasMCEngine.m | 25 +++- matRad/gui/widgets/matRad_PlanWidget.m | 9 +- matRad/gui/widgets/matRad_WorkflowWidget.m | 2 + .../matRad_StfGeneratorParticleIMPT.m | 9 +- ...Rad_StfGeneratorParticleRayBixelAbstract.m | 12 +- ...matRad_StfGeneratorParticleSingleBeamlet.m | 2 +- .../matRad_StfGeneratorParticleVHEE.m | 134 ++++++++++++++++++ test/autoExampleTest/test_examples.m | 1 + test/doseCalc/test_Analytical.m | 9 -- test/doseCalc/test_HongPB.m | 31 +++- test/doseCalc/test_TopasMCEngine.m | 128 ++++++++--------- test/doseCalc/test_baseEngine.m | 8 ++ test/steering/test_stfGeneratorVHEE.m | 100 +++++++++++++ test/testData/VHEE_testData.mat | Bin 0 -> 32889 bytes test/testData/VHEE_testData_Focused.mat | Bin 0 -> 73688 bytes 27 files changed, 594 insertions(+), 143 deletions(-) create mode 100644 examples/matRad_example20_VHEE.m create mode 100644 matRad/basedata/VHEE_Focused.mat create mode 100644 matRad/basedata/VHEE_Generic.mat create mode 100644 matRad/steering/matRad_StfGeneratorParticleVHEE.m create mode 100644 test/steering/test_stfGeneratorVHEE.m create mode 100644 test/testData/VHEE_testData.mat create mode 100644 test/testData/VHEE_testData_Focused.mat 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/AUTHORS.txt b/AUTHORS.txt index 690d7fc31..3ae8c88d6 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -11,8 +11,10 @@ List of all matRad developers that contributed code (alphabetical) * Louis Charton * Eric Christiansen * Remo Cristoforetti +* Fabio D'Andrea * Marios Dallas * Edgardo Doerner +* Louis Ermeneux * Simona Facchiano * Hubert Gabrys * Josefine Handrack @@ -37,6 +39,7 @@ List of all matRad developers that contributed code (alphabetical) * Claus Sarnighausen * Carsten Scholz * Camilo Sevilla +* Mateusz Sitarz * Alexander Stadler * Uwe Titt * Niklas Wahl diff --git a/CITATION.cff b/CITATION.cff index 41b30e17b..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ś" @@ -87,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_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/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/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 0000000000000000000000000000000000000000..d8af419fcbd3fb0a31e713ae1934a4757667c660 GIT binary patch literal 19542 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 literal 0 HcmV?d00001 diff --git a/matRad/basedata/VHEE_Generic.mat b/matRad/basedata/VHEE_Generic.mat new file mode 100644 index 0000000000000000000000000000000000000000..5276e16a9c2d40b1119432b1a8be4260b5d91edc GIT binary patch literal 104925 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 z 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

x~O=-Th#G-%8-8d)*iQ2nU>m)68|(0#-Y23k)^@ z`621V;V2NxvlE6M02Tzz-RxNi9Qr-+!{BK^Eg{h;Itvh6HRG2NFM3}&Xu13!K+x`$ zFe(M?)xA@v)d8kaLqV0J)OM8U`EU(b-EwkU)hq@oHQ8T!hJiW%=xK9a%pkK+b9ohu z!TZ6_LxXL=+qbNE|1Kb^S3L7kKQQCCb zWU?27-%NHS?h9rR)4t4S$9V?3gj&Jxe`3%8`O4xb2DS;(Hplo3^ri|Pqz5p_;NPw| z7000b%(gi#NeqZWMe3Bp45kg*sVJp05c=IoY`@K*^mX1-pFjp5yg2RG!WnF`=Z~Lz z$e<@(XmM|tLEfX<-i`_eD=UAj5M5)S?KB~{{f7}S<0ZVFk*R6ErTQ9n9DmUPRz$=3eAHI?lgY+&F&GcKWdooL2+Z$eB*0>V{lJj z`kg{OgKOKLJ>B?##u<@Sym;cC+`(+(F6CwIh^YSsgWBPcOWp1SZ#VWVYx&B+C-~7Z)z=KHr%P#j zWHD%;?efjJn1LEzjjy@NKql!jYhZ-->+vD25>o;xr_Z_X`xrPY*#}*eA;{&0s!pPD z&bzb}i)ItFOKKm~YGvRc!GF%vA-EN2;P^wAV6#P$lu-+VdDHhSl(Z!1IL-5Rs$uXf zI(XY89zjJ&Wn9r5g4%+m7AdO<9wqRWJJY)6rH>7}N)t?1lBv(#LJ;{fzN^_UY zenIP>cpG?eqkv#mME$wsMFgWdInDbu2(}J(b@b7EoF+!#rV_2Sk=Eg{qQ0rekDx?jj`}YY|BssnYd2AxPl5i;@`Ql9 z_2swG9R!Nf!|k7>2%7zTLV808zO@KLLqlmkD$VUl^9hP=%H1a05e$7Z8FQm`|CsYb zcG)a~F`s!KUpo?bjyGxVn@R9WEQPCni01R4wdvbjg3Dk1cn4?D{yD73@p{9+u2(-a zB8VVqUZz!%I)RE#-<=z@Z?&;kvPRbvG_lrSe)%iGKXx>APpl-E&D=LUWI?cERgv)m zhG3W8(M?J;o^aB3HqV71?$er&Dn|*Hxn0QbSVf>Zxo%&pE9Jqew(k@5>sT$k_mJ$>=Tk3xo zQ9TK5+&gIe5=UJ{|8)f0C#+(`DNc>;chj^922l$aT9g|SxGI+MniUE98IO*l3d(=) zh?h`81ntUtjXiq_{Oy;oo=SDLl{>L1>^gz_eJ8$m2i5cPw5p0gf(EshtI8=q=YBkp z&CDW*o~55YX)1y84DFDmb45_e;H34MP(4gjf0{sbW#$;RT#4#qP`UbU9@ST$z5bgG zlpmkOrcd1G1ilyh3=dG<^iE}++M-5~oMw5+Xam9T(mAgW`w+;QZ6}Y4363q@cW&=( z>i?oe_hT*9Lzh%WJgxJwua)viHs$q)zsGrX5&n^)m2#Z+=hyzuDg`~t@9E$u7rqFd zI(~{0RJWVOTvl)g2@E@DrIZxVIg4sEcxWcVzt|Sx&uJcO^Ce=}B@i4>;fS4mN9$ns z=tte5I_jCMrE;3;%vDv^`W3-X+4Hy;j6`Uf6II%-EyCFw>t@g3h|rKcFDL6gooo5d z>#e&964-}JRoq25xIb#n19^hO3;L!^GAC%6=Al_k_5Sc{gzFi~t8KeQUtkfzSF3~@ zo=Q{)Z33Tiwg^sRZk?`DBAC3How17QSvt0>ts-88mzo#0tfzC<*VFktu!+7OS!*w} zNz*tdJi<0gig0BWJAI&xVAAD`%}E0UMcx73vn~>(FOm=x1c`7mE6#Pdr3mwaHYmUA zrur63_EeztO$_KCU2(>YzyWHG+jMuZqWe+PCVLHBLtsL-nf z>wJsdpM0c!XyL9iqw{F;&#en~(|TmoT~cwR3Idv{DF4 zi-(PW@~B`!SnJR8*+U_s8O&?)}!x8uTx5O1LbHK4ITBl=a z7o&TUfgFp)k`iOFB>&v$5@-GAH~BAH>~HHo=ac@=_fLB!UAy)Kc(7Q09-I7GtexIl z{WksO_a`ow_5Zk&{tK?u-*9LAWpCXSv~#ETCjUS0>;LV( relativeTolerance matRad_cfg.dispWarning('LateralParticleCutOff: shell integration is wrong !') @@ -751,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); @@ -775,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)); @@ -789,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] @@ -984,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_TopasMCEngine.m b/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m index 19ef5ddf2..58ab20995 100644 --- a/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m +++ b/matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m @@ -19,7 +19,7 @@ % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% properties (Constant) - possibleRadiationModes = {'photons','protons','helium','carbon'}; + possibleRadiationModes = {'photons','protons','helium','carbon','VHEE'}; name = 'TOPAS'; shortName = 'TOPAS'; @@ -53,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,... @@ -117,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'; @@ -790,7 +791,7 @@ 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' @@ -1628,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 @@ -1760,6 +1761,7 @@ function writeStfFields(obj,ct,stf,w,baseData) 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)); @@ -1778,7 +1780,7 @@ function writeStfFields(obj,ct,stf,w,baseData) end switch obj.radiationMode - case {'protons','carbon','helium'} + case {'protons','carbon','helium','VHEE'} [~,ixTmp,~] = intersect(energies, bixelEnergy); if obj.useOrigBaseData dataTOPAS(cutNumOfBixel).energy = selectedData(ixTmp).energy; @@ -1944,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 diff --git a/matRad/gui/widgets/matRad_PlanWidget.m b/matRad/gui/widgets/matRad_PlanWidget.m index 912668689..e0b3a2a20 100644 --- a/matRad/gui/widgets/matRad_PlanWidget.m +++ b/matRad/gui/widgets/matRad_PlanWidget.m @@ -33,7 +33,7 @@ properties (Constant) - modalities = {'photons','protons','carbon', 'helium','brachy'}; + modalities = {'photons','protons','carbon', 'helium','brachy', 'VHEE'}; availableProjections = { 'physicalDose'; 'RBExDose'; 'effect'; 'BED'; } end @@ -1256,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); @@ -1300,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 diff --git a/matRad/gui/widgets/matRad_WorkflowWidget.m b/matRad/gui/widgets/matRad_WorkflowWidget.m index e7416a803..e66759b76 100644 --- a/matRad/gui/widgets/matRad_WorkflowWidget.m +++ b/matRad/gui/widgets/matRad_WorkflowWidget.m @@ -289,6 +289,7 @@ function btnLoadMat_Callback(this, hObject, event) % check if stf exists if evalin('base','exist(''stf'')') stf = evalin('base','stf'); + % check if dij, stf and pln match [plnStfMatch, msg] = matRad_comparePlnStf(pln,stf); if plnStfMatch @@ -307,6 +308,7 @@ function btnLoadMat_Callback(this, hObject, event) 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'); diff --git a/matRad/steering/matRad_StfGeneratorParticleIMPT.m b/matRad/steering/matRad_StfGeneratorParticleIMPT.m index b32cd9194..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 % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % @@ -310,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/test/autoExampleTest/test_examples.m b/test/autoExampleTest/test_examples.m index 0aa2507a9..e95ad3cda 100644 --- a/test/autoExampleTest/test_examples.m +++ b/test/autoExampleTest/test_examples.m @@ -24,6 +24,7 @@ '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/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_HongPB.m b/test/doseCalc/test_HongPB.m index 9024d2661..40f5300b4 100644 --- a/test/doseCalc/test_HongPB.m +++ b/test/doseCalc/test_HongPB.m @@ -33,7 +33,7 @@ assertTrue(isequal(fieldnames(resultGUI),fieldnames(testData.resultGUI))); assertTrue(isequal(testData.ct.cubeDim, size(resultGUI.physicalDose))); - assertElementsAlmostEqual(resultGUI.physicalDose,testData.resultGUI.physicalDose,'relative',1e-2); + assertElementsAlmostEqual(resultGUI.physicalDose,testData.resultGUI.physicalDose,'relative',1e-2,1e-2); function test_calcDoseHongPBhelium testData = load('helium_testData.mat'); @@ -46,7 +46,7 @@ assertTrue(isequal(fieldnames(resultGUI),fieldnames(testData.resultGUI))); assertTrue(isequal(testData.ct.cubeDim, size(resultGUI.physicalDose))); - assertElementsAlmostEqual(resultGUI.physicalDose,testData.resultGUI.physicalDose,'relative',1e-2); + assertElementsAlmostEqual(resultGUI.physicalDose,testData.resultGUI.physicalDose,'relative',1e-2,1e-2); function test_calcDoseHongPBcarbon testData = load('carbon_testData.mat'); @@ -59,8 +59,31 @@ assertTrue(isequal(fieldnames(resultGUI),fieldnames(testData.resultGUI))); assertTrue(isequal(testData.ct.cubeDim, size(resultGUI.physicalDose))); - assertElementsAlmostEqual(resultGUI.physicalDose,testData.resultGUI.physicalDose,'relative',1e-2); - + 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 diff --git a/test/doseCalc/test_TopasMCEngine.m b/test/doseCalc/test_TopasMCEngine.m index a3dff06ff..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,28 @@ 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 @@ -89,12 +89,14 @@ confirm_recursive_rmdir(false,'local'); end -for i = 2:numel(radModes) +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']); @@ -132,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 @@ -226,39 +225,40 @@ RBEmodel = {'mcn', 'wed'}; case {'helium', 'carbon'} RBEmodel ={'libamtrack','lem'}; + otherwise + continue; end - 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; - 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 + 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 - rmdir(folderName,'s'); %clean up 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/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/VHEE_testData.mat b/test/testData/VHEE_testData.mat new file mode 100644 index 0000000000000000000000000000000000000000..9d4aeebfbc509b00c133ff36c8d2689c31bc5565 GIT binary patch literal 32889 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%0AbLH*J)UyB0pb&oc>|5&~mBj6In;UgCt`*MV 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

c6IBclN@c!pc%T=f2fx{`=6Ia-ojSt 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 L4Uz)@4qvHsr;x2o literal 0 HcmV?d00001 From 7b7075dccb803110934194a57e8299f610568191 Mon Sep 17 00:00:00 2001 From: JenHardt <84377305+JenHardt@users.noreply.github.com> Date: Thu, 2 Oct 2025 10:03:47 +0200 Subject: [PATCH 83/87] to prevent the matRadGUI window beeing to big if used in MATLAB Online added a switch to check if it is called from matlab online or not, the easiest way to check this is with isunix as matlab online runs with unix. Without this switch the initial GUI window can be a little bit smaller than fullscreen when run from matlab Desktop --- matRad/gui/matRad_MainGUI.m | 3 +++ 1 file changed, 3 insertions(+) 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,... From 046b3631fc0d6502f45384a4fa5e23e97f027421 Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Mon, 27 Oct 2025 11:07:45 +0100 Subject: [PATCH 84/87] Update CHANGELOG.md Highligh changes for 3.2.0 --- CHANGELOG.md | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c221a221..069ee9629 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,36 @@ # Changelog -## Development Changes +## Minor Update 3.2.0 -### Bug Fixes +### 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" From 4881d26d6173836e470c2cc702d11e4ab9c9675c Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Wed, 29 Oct 2025 12:47:59 +0100 Subject: [PATCH 85/87] bugfix in property setters for ImportanceScenarios --- matRad/scenarios/matRad_ImportanceScenarios.m | 4 +- test/scenarios/test_importanceScenarios.m | 91 +++++++++++-------- 2 files changed, 53 insertions(+), 42 deletions(-) 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/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 From c227f615936ebf1979e4af08ba07f038c8eb0d55 Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Wed, 29 Oct 2025 14:49:20 +0100 Subject: [PATCH 86/87] fix for missing constant RBE in effect projection --- .../projections/matRad_EffectProjection.m | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/matRad/optimization/projections/matRad_EffectProjection.m b/matRad/optimization/projections/matRad_EffectProjection.m index 22915b240..ca3e0dc7b 100644 --- a/matRad/optimization/projections/matRad_EffectProjection.m +++ b/matRad/optimization/projections/matRad_EffectProjection.m @@ -35,7 +35,7 @@ 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') && dij.RBE == 1.1 + 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; @@ -50,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 @@ -62,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 @@ -93,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 From e455e93ab00e345e878d479f49ba39132e5c4d6c Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Wed, 29 Oct 2025 12:55:26 +0100 Subject: [PATCH 87/87] update version --- matRad/matRad_version.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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;