From ff0a59a4cb7471ac8f7365237023a49886860be4 Mon Sep 17 00:00:00 2001 From: RemoCristoforetti Date: Mon, 19 May 2025 13:43:08 +0200 Subject: [PATCH 1/7] Add getTissueParameters function to bioModel class --- .../matRad_LQKernelBasedModel.m | 18 ++++++++++++++++++ matRad/bioModels/matRad_BiologicalModel.m | 8 ++++++++ 2 files changed, 26 insertions(+) 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..8dc4b94d8 100644 --- a/matRad/bioModels/matRad_BiologicalModel.m +++ b/matRad/bioModels/matRad_BiologicalModel.m @@ -251,6 +251,14 @@ end end + function [alphaX, betaX] = getAvailableTissueParameters(pln) + + % load machine + alphaX = []; + betaX = []; + + end + end From 6f9ae923eb4fad2a310fbbdd3ba65ff419cb7c4b Mon Sep 17 00:00:00 2001 From: RemoCristoforetti Date: Mon, 19 May 2025 13:43:57 +0200 Subject: [PATCH 2/7] Update PlanWidget to allow biological tissue definiiton --- matRad/gui/widgets/matRad_PlanWidget.m | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/matRad/gui/widgets/matRad_PlanWidget.m b/matRad/gui/widgets/matRad_PlanWidget.m index 03cf3139e..73df899dc 100644 --- a/matRad/gui/widgets/matRad_PlanWidget.m +++ b/matRad/gui/widgets/matRad_PlanWidget.m @@ -1471,8 +1471,26 @@ function btnSetTissue_Callback(this, hObject, eventdata) 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))]; + % if all(isfield(machine.data(1), {'alphaX', 'betaX'})) + % for i = 1:size(machine.data(1).alphaX,2) + % CellType{i} = [num2str(machine.data(1).alphaX(i)) ' ' num2str(machine.data(1).betaX(i))]; + % end + % end + % Get the current biological model + + if ischar(pln.bioModel) + bioModel = matRad_bioModel(pln.radiationMode, pln.bioModel); + end + + [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,7 +1525,7 @@ function btnSetTissue_Callback(this, hObject, eventdata) % define the tissue parameter table cNames = {'VOI','alphaX betaX','alpha beta ratio'}; - columnformat = {'char',CellType,'numeric'}; + % columnformat = {'char',CellType,'numeric'}; tissueTable = uitable('Parent', figTissue,'Data', data,'ColumnEditable',[false true false],... 'ColumnName',cNames, 'ColumnFormat',columnformat,'Position',[50 150 10 10]); From b42cf613fe11ea1f1403c499595819cc4edab73b Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Wed, 3 Sep 2025 18:52:40 +0200 Subject: [PATCH 3/7] clean up matRad_bioModel by calling the validate function from the model itself --- matRad/bioModels/matRad_bioModel.m | 58 +++++----------------------- test/bioModel/test_biologicalModel.m | 11 +++++- 2 files changed, 20 insertions(+), 49 deletions(-) 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/test/bioModel/test_biologicalModel.m b/test/bioModel/test_biologicalModel.m index ebe801c9b..f88dc2c11 100644 --- a/test/bioModel/test_biologicalModel.m +++ b/test/bioModel/test_biologicalModel.m @@ -23,11 +23,20 @@ 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_calcBiologicalQuantitiesForBixel_MCN From 083c13c53d6e24ed23731697a602606188aa378f Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Wed, 3 Sep 2025 19:02:43 +0200 Subject: [PATCH 4/7] add biological model tests for getting available tissue parameters --- matRad/bioModels/matRad_BiologicalModel.m | 7 +++---- test/bioModel/test_biologicalModel.m | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/matRad/bioModels/matRad_BiologicalModel.m b/matRad/bioModels/matRad_BiologicalModel.m index 8dc4b94d8..283648ce4 100644 --- a/matRad/bioModels/matRad_BiologicalModel.m +++ b/matRad/bioModels/matRad_BiologicalModel.m @@ -251,12 +251,11 @@ end end - function [alphaX, betaX] = getAvailableTissueParameters(pln) - - % load machine + function [alphaX, betaX] = getAvailableTissueParameters(pln) + % empty values in standard implementation, needs to be + % overwritten in subclasses alphaX = []; betaX = []; - end diff --git a/test/bioModel/test_biologicalModel.m b/test/bioModel/test_biologicalModel.m index f88dc2c11..51af189d8 100644 --- a/test/bioModel/test_biologicalModel.m +++ b/test/bioModel/test_biologicalModel.m @@ -37,7 +37,25 @@ 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'); From 51cc35827118bc53bf7a2a587d9cf5682b30fe6c Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Wed, 3 Sep 2025 19:05:23 +0200 Subject: [PATCH 5/7] sanitize tissue btn callback --- matRad/gui/widgets/matRad_PlanWidget.m | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/matRad/gui/widgets/matRad_PlanWidget.m b/matRad/gui/widgets/matRad_PlanWidget.m index 73df899dc..fff548cfd 100644 --- a/matRad/gui/widgets/matRad_PlanWidget.m +++ b/matRad/gui/widgets/matRad_PlanWidget.m @@ -1470,18 +1470,18 @@ function btnSetTissue_Callback(this, hObject, eventdata) fileName = [pln.radiationMode '_' pln.machine]; load(fileName); - % check for available cell types characterized by alphaX and betaX - % if all(isfield(machine.data(1), {'alphaX', 'betaX'})) - % for i = 1:size(machine.data(1).alphaX,2) - % CellType{i} = [num2str(machine.data(1).alphaX(i)) ' ' num2str(machine.data(1).betaX(i))]; - % end - % end - % Get the current biological model - - if ischar(pln.bioModel) - bioModel = matRad_bioModel(pln.radiationMode, pln.bioModel); + %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) From fe37711ecaf1840b9450de045a65c71c2123c930 Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Wed, 3 Sep 2025 19:22:17 +0200 Subject: [PATCH 6/7] update the tissue table --- matRad/gui/widgets/matRad_PlanWidget.m | 56 ++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/matRad/gui/widgets/matRad_PlanWidget.m b/matRad/gui/widgets/matRad_PlanWidget.m index fff548cfd..ecdea4368 100644 --- a/matRad/gui/widgets/matRad_PlanWidget.m +++ b/matRad/gui/widgets/matRad_PlanWidget.m @@ -1462,6 +1462,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'); @@ -1519,16 +1520,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'}; + + %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'); @@ -1537,14 +1564,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 From 06f23e47d79b9277534b110f96bca0e0f98b0754 Mon Sep 17 00:00:00 2001 From: Niklas Wahl Date: Wed, 3 Sep 2025 19:34:56 +0200 Subject: [PATCH 7/7] test tissue selection button --- test/gui/test_gui_PlanWidget.m | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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