Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2b513f3
Minor bug fix: correct call to matRad_getIsocenter
remocristoforetti Nov 30, 2025
786c0f4
Minor bug fix: explicit direction for mean(), effective if only one v…
remocristoforetti Nov 30, 2025
d095e94
Bug fix: add rangeShifterEq thickness as property
remocristoforetti Nov 30, 2025
d4ba549
Allow user to set rangeShifterEqThickness
remocristoforetti Nov 30, 2025
e2f9e8e
Bug fix: handle of target entry/exit point when taregt begins/ends on…
remocristoforetti Nov 30, 2025
c8b6743
Bug fix: get equivalent thickness of range shifter
remocristoforetti Nov 30, 2025
4436e6a
Big fix: call to matRad_calcSigmaRashi
remocristoforetti Nov 30, 2025
be84504
Allow access to getRangeShiftersFromStf
remocristoforetti Nov 30, 2025
9814d26
Bug fix: handling of range shifters
remocristoforetti Nov 30, 2025
924c44b
Allow for special characters in filenames for system call
remocristoforetti Nov 30, 2025
238b83b
Big fix: Move position of range shifter
remocristoforetti Nov 30, 2025
7a223f1
Add tests for range shfter generation and calculation
remocristoforetti Nov 30, 2025
b05a836
Add test for matRad_calcSigmaRashi
remocristoforetti Nov 30, 2025
1834041
Bug fix: range shifter field handling
remocristoforetti Nov 30, 2025
856730b
Remove plotting from raShi test
remocristoforetti Dec 1, 2025
8bc7fd9
Update use of round functon for Octave compatibility
remocristoforetti Dec 1, 2025
7390a02
Minor typo fixes; add externalCalculation option for MCsquare engine
remocristoforetti Jan 30, 2026
aeb01c6
Remove read external option for MCsquare
remocristoforetti Jan 30, 2026
9790a96
Octave compatibility for MCsquare test
remocristoforetti Jan 30, 2026
550b64a
Octave test compatibility
remocristoforetti Jan 30, 2026
5e6a53c
Merge branch 'dev' of https://github.com/e0404/matRad into dev_rangeS…
remocristoforetti Feb 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion matRad/basedata/matRad_MCemittanceBaseData.m
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,7 @@
end
end

methods (Access = protected)
methods %(Access = protected)
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The commented-out access modifier changes the visibility of the getRangeShiftersFromStf method from protected to public. This should either have the comment removed (if making it public is intentional) or be reverted to Access = protected. Leaving it commented out is unclear and appears incomplete.

Suggested change
methods %(Access = protected)
methods (Access = protected)

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there actually a reason why this is commented? Could it be deleted?

function obj = getRangeShiftersFromStf(obj,stf)
allRays = [stf.ray];
raShis = [allRays.rangeShifter];
Expand Down
416 changes: 279 additions & 137 deletions matRad/doseCalc/+DoseEngines/matRad_ParticleMCsquareEngine.m

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ function chooseLateralModel(this)
end

% compute!
sigmaRashi = matRad_calcSigmaRashi(currBixel.baseData.energy, ...
sigmaRashi = matRad_calcSigmaRashi(currBixel.baseData, ...
currBixel.rangeShifter, ...
currBixel.SSD);

Expand Down Expand Up @@ -629,7 +629,7 @@ function calcLateralParticleCutOff(this,cutOffLevel,stfElement)
if strcmp(this.machine.meta.radiationMode,'protons') && rangeShifterLUT(i).eqThickness > 0

%get max range shift
sigmaRashi = matRad_calcSigmaRashi(this.machine.data(energyIx).energy, ...
sigmaRashi = matRad_calcSigmaRashi(this.machine.data(energyIx), ...
rangeShifterLUT(i), ...
energySigmaLUT(i,3));

Expand Down Expand Up @@ -818,7 +818,7 @@ function calcLateralParticleCutOff(this,cutOffLevel,stfElement)
if rangeShifter.eqThickness > 0 && strcmp(pln.radiationMode,'protons')

% compute!
sigmaRashi = matRad_calcSigmaRashi(this.machine.data(energyIx).energy,rangeShifter,maxSSD);
sigmaRashi = matRad_calcSigmaRashi(this.machine.data(energyIx),rangeShifter,maxSSD);

% add to initial sigma in quadrature
sigmaIni_sq = sigmaIni_sq + sigmaRashi^2;
Expand Down
2 changes: 1 addition & 1 deletion matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m
Original file line number Diff line number Diff line change
Expand Up @@ -2684,7 +2684,7 @@ function writeRangeShifter(~,fID,rangeShifter,sourceToNozzleDistance)
fprintf(fID,'d:Ge/%s/HLZ = %f mm\n',rangeShifter.topasID,rsWidth/2);
fprintf(fID,'d:Ge/%s/TransX = 500 mm * Tf/Beam/%sOut/Value\n',rangeShifter.topasID,rangeShifter.topasID);
fprintf(fID,'d:Ge/%s/TransY = 0 mm\n',rangeShifter.topasID);
fprintf(fID,'d:Ge/%s/TransZ = %f mm\n',rangeShifter.topasID,rangeShifter.sourceRashiDistance - sourceToNozzleDistance);
fprintf(fID,'d:Ge/%s/TransZ = %f mm\n',rangeShifter.topasID,rangeShifter.sourceRashiDistance - sourceToNozzleDistance + rsWidth/2);

end

Expand Down
2 changes: 1 addition & 1 deletion matRad/geometry/matRad_getIsoCenter.m
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@


% Calculated isocenter.
isoCenter = mean(coord);
isoCenter = mean(coord, 1);


% Visualization
Expand Down
26 changes: 21 additions & 5 deletions matRad/steering/matRad_StfGeneratorParticleIMPT.m
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,21 @@
end

% find target entry & exit
diff_voi = [diff([rho{shiftScen}{end}])];
entryIx = find(diff_voi == 1);
exitIx = find(diff_voi == -1);
if rho{shiftScen}{end}(1)~=0
matRad_cfg.dispWarning('Target entry on the first voxel');
entryIx = 1;
else
diff_voi = [diff([rho{shiftScen}{end}])];
entryIx = find(diff_voi == 1);
end

if rho{shiftScen}{end}(end)~=0
matRad_cfg.dispWarning('Target exit on the last voxel');
exitIx = numel(rho{shiftScen}{1});
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent variable reference: Line 138 uses rho{shiftScen}{1} to get the number of elements, but should use rho{shiftScen}{end} to be consistent with the logic above (lines 128-142 check the last element of rho). The exit index should be calculated from the same scenario as the entry index.

Suggested change
exitIx = numel(rho{shiftScen}{1});
exitIx = numel(rho{shiftScen}{end});

Copilot uses AI. Check for mistakes.
else
diff_voi = [diff([rho{shiftScen}{end}])];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

diff_voi does not align with camelCase naming convention actually.

exitIx = find(diff_voi == -1);
end

%We approximate the interface using the rad depth between the last voxel before and the first voxel after the interface
% This captures the case that the first relevant voxel is a target voxel
Expand Down Expand Up @@ -176,9 +188,13 @@
%non-reachable low-range spots
raShiEnergies = this.availableEnergies(this.availablePeakPosRaShi >= targetEntry(k) & min(this.availablePeakPos) > this.availablePeakPosRaShi);

if isempty(raShiEnergies)
matRad_cfg.dispWarning('No energies available for range shifting, please change the range shifter thickness');
end

raShi.ID = 1;
raShi.eqThickness = rangeShifterEqD;
raShi.sourceRashiDistance = round(min(ctEntryPoint) - 2*rangeShifterEqD,-1); %place a little away from entry, round to cms to reduce number of unique settings
raShi.eqThickness = this.rangeShifterEqD;
raShi.sourceRashiDistance = 10 * round((min(ctEntryPoint) - 2*this.rangeShifterEqD) / 10); %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))];
Expand Down
17 changes: 13 additions & 4 deletions matRad/steering/matRad_StfGeneratorParticleRayBixelAbstract.m
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

properties
useRangeShifter = false;
rangeShifterEqD
end

properties (Access = protected)
Expand Down Expand Up @@ -66,10 +67,18 @@ function initialize(this)
if this.useRangeShifter
%For now only a generic range shifter is used whose thickness is
%determined by the minimum peak width to play with
rangeShifterEqD = round(min(this.availablePeakPos)* 1.25);
this.availablePeakPosRaShi = this.availablePeakPos - rangeShifterEqD;

matRad_cfg.dispWarning('Use of range shifter enabled. matRad will generate a generic range shifter with WEPL %f to enable ranges below the shortest base data entry.',rangeShifterEqD);

if isempty(this.rangeShifterEqD)
this.rangeShifterEqD = round(min(this.availablePeakPos)* 1.25);
end

this.availablePeakPosRaShi = this.availablePeakPos - this.rangeShifterEqD;

% Available PeakPositionRaShi has to have same size() as
% availablePeaPos for indexing
this.availablePeakPosRaShi(this.availablePeakPosRaShi<0) = 0;

matRad_cfg.dispWarning('Use of range shifter enabled. matRad will generate a generic range shifter with WEPL %f to enable ranges below the shortest base data entry.',this.rangeShifterEqD);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This warning now only makes sense if the rangeShifterEqD is not provided, right?
I think we need to messages (and it does not need to be a warning)

  1. Using provided range shfiter thickness of %f mm
  2. Using generic range shifter thickness of %f mm determined to allow ranges below the shortest base data entry.

end

if sum(this.availablePeakPos<0)>0
Expand Down
5 changes: 3 additions & 2 deletions matRad/steering/matRad_StfGeneratorParticleSingleBeamlet.m
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
properties
energy;
raShiThickness = 50; %Range shifter to be used if useRangeShifter = true;
visBool = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like "visBool". Alternative suggestions? "visualize"?

end

properties (Constant)
Expand Down Expand Up @@ -50,12 +51,12 @@ function createPatientGeometry(this)
matRad_cfg = MatRad_Config.instance();

if isempty(this.isoCenter)
this.isoCenter = matRad_getIsoCenter(this.cst,this.ct,visBool);
this.isoCenter = matRad_getIsoCenter(this.cst,this.ct,this.visBool);
end

if ~isequal(size(this.isoCenter),[this.numOfBeams,3]) && ~size(this.isoCenter,1) ~= 1
matRad_cfg.dispWarning('IsoCenter invalid, creating new one automatically!');
this.isoCenter = matRad_getIsoCenter(this.cst,this.ct,visBool);
this.isoCenter = matRad_getIsoCenter(this.cst,this.ct,this.visBool);
end

if size(this.isoCenter,1) == 1
Expand Down
23 changes: 20 additions & 3 deletions test/doseCalc/test_HongPB.m
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,23 @@
assertExceptionThrown(@() DoseEngines.matRad_ParticleHongPencilBeamEngine.isAvailable(testData.pln));
assertFalse(DoseEngines.matRad_ParticleHongPencilBeamEngine.isAvailable(testData.pln,[]));




function test_doseCalcWithRashi
testData = load('protons_testData.mat');
engine = DoseEngines.matRad_ParticleHongPencilBeamEngine(testData.pln);

stf = testData.stf;

% Add rangeShifter
stf(1).ray(1).rangeShifter.ID =1;
stf(1).ray(1).rangeShifter.eqThickness =1;
stf(1).ray(1).rangeShifter.sourceRashiDistance = -(stf(1).sourcePoint(2) + 100);

% Add rangeShifter
stf(2).ray(2).rangeShifter.ID =1;
stf(2).ray(2).rangeShifter.eqThickness =1;
stf(2).ray(2).rangeShifter.sourceRashiDistance = -(stf(2).sourcePoint(2) + 100);

resultGUI = engine.calcDoseForward(testData.ct,testData.cst,stf,ones(sum([stf.totalNumOfBixels]),1));
assertTrue(isequal(fieldnames(resultGUI),fieldnames(testData.resultGUI)));


56 changes: 56 additions & 0 deletions test/doseCalc/test_MCsquareEngine.m
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this also test with range shifter? I don't see it.

Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
function test_suite = test_MCsquareEngine

test_functions=localfunctions();

initTestSuite;


function test_MCsquareDoseCalcBasic

matRad_cfg = MatRad_Config.instance();
radModes = DoseEngines.matRad_ParticleMCsquareEngine.possibleRadiationModes;

for i = 1:numel(radModes)
load([radModes{i} '_testData.mat']);
pln.bioModel = matRad_bioModel(radModes{i},'none');

w = ones(1,sum([stf(:).totalNumOfBixels]));

pln.propDoseCalc.engine = 'MCsquare';
pln.propDoseCalc.externalCalculation = 'write';
pln.propDoseCalc.numHistoriesDirect = 42;
resultGUI = matRad_calcDoseForward(ct,cst,stf,pln, w);

assertTrue(exist(fullfile(matRad_cfg.primaryUserFolder, 'MCsquare'), 'dir')==7); % Check it exists and its a folder
assertTrue(exist(fullfile(matRad_cfg.primaryUserFolder, 'MCsquare', 'MCsquareConfig.txt'), 'file')==2);
assertTrue(exist(fullfile(matRad_cfg.primaryUserFolder, 'MCsquare', 'currBixels.txt'), 'file')==2);


% Check parameters
% Read config file
% linesConfigFile = readlines(fullfile(matRad_cfg.primaryUserFolder, 'MCsquare', 'MCsquareConfig.txt'));
fid = fopen(fullfile(matRad_cfg.primaryUserFolder, 'MCsquare', 'MCsquareConfig.txt'),'r');
linesConfigFile = {};
while ~feof(fid)
linesConfigFile{end+1,1} = fgetl(fid);
end
fclose(fid);


assertTrue(any(strcmp(linesConfigFile, "Num_Primaries 42")));

% Read currBixel file
% linesBixelFile = readlines(fullfile(matRad_cfg.primaryUserFolder, 'MCsquare', 'currBixels.txt'));
fid = fopen(fullfile(matRad_cfg.primaryUserFolder, 'MCsquare', 'currBixels.txt'),'r');
linesBixelFile = {};
while ~feof(fid)
linesBixelFile{end+1,1} = fgetl(fid);
end
fclose(fid);

assertTrue(any(strcmp(linesBixelFile, "##NumberOfFields")));
assertTrue(str2double(linesBixelFile(find(strcmp(linesBixelFile, "##NumberOfFields"))+1)) == numel(stf));

end


19 changes: 19 additions & 0 deletions test/doseCalc/test_sigmaRashi.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
function test_suite= test_sigmaRashi

test_functions=localfunctions();

initTestSuite;

function test_calcSigmaRashi

baseDataEntry.range = 100;

rangeShifter.ID = 1;
rangeShifter.eqThickness = 1;
rangeShifter.sourceRashiDistance = 9000;

SSD = 10000;

sigma = matRad_calcSigmaRashi(baseDataEntry, rangeShifter, SSD);

assertElementsAlmostEqual(sigma,1.1,'relative',1e-2,1e-2);
21 changes: 21 additions & 0 deletions test/steering/test_stfGEneratorParticleIMPT.m
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,24 @@ function test_generate_multibeams()

%assertTrue(isscalar(stf2(i).ray.energy));
end

function test_generateRangeShifterStf()
% geometry settings
load protons_testData.mat

% Move Target shallower so that range shifter calculation
ct.resolution.y=5;
VolHelper = false(ct.cubeDim);
VolHelper(2:3,5:6,5:6) = true;
ixTarget = find(VolHelper);

cst{1,4}{1} = ixTarget;

stfGen = matRad_StfGeneratorParticleIMPT(pln);
stfGen.useRangeShifter = true;
stfGen.rangeShifterEqD = 2;

stf = stfGen.generate(ct,cst);

assertTrue(stf(1).ray(1).rangeShifter(1).ID==1);
assertTrue(stf(1).ray(1).rangeShifter(1).eqThickness==2);
Loading