Range shifter positioning for MC and minor bug fixing#880
Range shifter positioning for MC and minor bug fixing#880remocristoforetti wants to merge 21 commits intoe0404:devfrom
Conversation
…oxel belongs to target
… first/last voxel
|
@JenHardt could you please do the review first and then I will take a look |
There was a problem hiding this comment.
Pull request overview
This pull request implements range shifter positioning for Monte Carlo dose calculation engines and addresses several minor bugs in the IMPT steering file generator and Hong pencilbeam algorithm. The changes enable proper handling of range shifters in MCsquare simulations by splitting fields based on range shifter presence and correcting positioning calculations.
Key Changes:
- Fixed range shifter positioning calculations in MCsquare and TOPAS MC engines
- Corrected
matRad_calcSigmaRashifunction calls to pass the full base data entry instead of just energy - Added handling for edge cases in IMPT generator where targets start/end at voxel boundaries
- Fixed
matRad_getIsoCenterto properly calculate mean along rows
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| test/steering/test_stfGEneratorParticleIMPT.m | Added test case for range shifter generation in IMPT |
| test/doseCalc/test_sigmaRashi.m | New test for sigma calculation with range shifter |
| test/doseCalc/test_HongPB.m | Added test for Hong PB dose calculation with range shifter |
| matRad/steering/matRad_StfGeneratorParticleSingleBeamlet.m | Fixed undefined visBool variable by adding as class property |
| matRad/steering/matRad_StfGeneratorParticleRayBixelAbstract.m | Added rangeShifterEqD property and logic to handle user-defined thickness |
| matRad/steering/matRad_StfGeneratorParticleIMPT.m | Fixed target entry/exit detection for edge cases and range shifter references |
| matRad/geometry/matRad_getIsoCenter.m | Fixed mean calculation to compute along dimension 1 |
| matRad/doseCalc/+DoseEngines/matRad_TopasMCEngine.m | Corrected range shifter position by adding half-width offset |
| matRad/doseCalc/+DoseEngines/matRad_ParticlePencilBeamEngineAbstract.m | Updated matRad_calcSigmaRashi calls to pass full baseData entry |
| matRad/doseCalc/+DoseEngines/matRad_ParticleMCsquareEngine.m | Implemented field splitting for range shifter handling and corrected positioning formula |
| matRad/basedata/matRad_MCemittanceBaseData.m | Changed access modifier to make getRangeShiftersFromStf public |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| isoToRaShi = stf(i).SAD - rangeShifter.sourceRashiDistance - rsWidth; | ||
| fprintf(fileHandle,'####IsocenterToRangeShifterDistance\n%f\n',isoToRaShi/10); %in cm |
There was a problem hiding this comment.
[nitpick] The sign change in this calculation should be verified. Previously it was + rsWidth, now it's - rsWidth. Combined with the sign change in line 815 (removing the negative), this represents a significant change in the range shifter positioning calculation. Please ensure this has been validated with test data, as incorrect positioning could lead to dose calculation errors.
| isoToRaShi = stf(i).SAD - rangeShifter.sourceRashiDistance - rsWidth; | |
| fprintf(fileHandle,'####IsocenterToRangeShifterDistance\n%f\n',isoToRaShi/10); %in cm | |
| isoToRaShi = stf(i).SAD - rangeShifter.sourceRashiDistance + rsWidth; | |
| fprintf(fileHandle,'####IsocenterToRangeShifterDistance\n%f\n',-isoToRaShi/10); %in cm |
There was a problem hiding this comment.
I guess this is because of the reported range shifter misalignment in #877 ?
| end | ||
|
|
||
| methods (Access = protected) | ||
| methods %(Access = protected) |
There was a problem hiding this comment.
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.
| methods %(Access = protected) | |
| methods (Access = protected) |
There was a problem hiding this comment.
Is there actually a reason why this is commented? Could it be deleted?
There was a problem hiding this comment.
Typo in function name: "test_generateRangeShfterStf" should be "test_generateRangeShifterStf" (missing 'i' in 'Shifter').
| function test_generateRangeShfterStf() | |
| function test_generateRangeShifterStf() |
There was a problem hiding this comment.
Missing semicolon after property declaration. All other property declarations in this file and MATLAB conventions typically use semicolons to suppress output.
| useRangeShifter = false | |
| useRangeShifter = false; |
There was a problem hiding this comment.
Typo in comment: "Selecte" should be "Select" (extra 'e' at the end).
| % Selecte the energies that have a RaShi for | |
| % Select the energies that have a RaShi for |
| % If more than one energy layer is | ||
| % found, one of them is for the | ||
| % RaShiField | ||
| energyIx = energyIx([stf(i).ray(j).rangeShifter(energyIx).ID] == 0); % Select the one with no RaShi; |
There was a problem hiding this comment.
Potential bug: When filtering energyIx on line 391, if all energies for this layer have a range shifter (i.e., none have ID == 0), energyIx will become empty. This would cause issues when used on lines 394, 401, 404, and 406. Consider adding a check to skip processing if energyIx is empty after filtering, or ensure this case is handled properly.
| raShiLayers = []; | ||
| for j = 1:stf(i).numOfRays | ||
| currentRay = stf(i).ray(j); | ||
| raShiLayers = [raShiLayers, currentRay.energy([currentRay.rangeShifter.ID] == stfFieldMCsquareRaShi.rangeShifterID)]; | ||
| end | ||
| stfFieldMCsquareRaShi.energies = unique(raShiLayers); |
There was a problem hiding this comment.
[nitpick] Inconsistent naming: The variable is named raShiLayers (plural) but represents energy values, not layers. Consider renaming to raShiEnergies to be consistent with line 189 where a similar concept uses the name raShiEnergies.
| raShiLayers = []; | |
| for j = 1:stf(i).numOfRays | |
| currentRay = stf(i).ray(j); | |
| raShiLayers = [raShiLayers, currentRay.energy([currentRay.rangeShifter.ID] == stfFieldMCsquareRaShi.rangeShifterID)]; | |
| end | |
| stfFieldMCsquareRaShi.energies = unique(raShiLayers); | |
| raShiEnergies = []; | |
| for j = 1:stf(i).numOfRays | |
| currentRay = stf(i).ray(j); | |
| raShiEnergies = [raShiEnergies, currentRay.energy([currentRay.rangeShifter.ID] == stfFieldMCsquareRaShi.rangeShifterID)]; | |
| end | |
| stfFieldMCsquareRaShi.energies = unique(raShiEnergies); |
There was a problem hiding this comment.
Typo in comment: "compuation" should be "computation" (missing 't').
| %Number of primaries depending on beamlet-wise or field-based compuation (direct dose calculation) | |
| %Number of primaries depending on beamlet-wise or field-based computation (direct dose calculation) |
| % we can eliminate the layer from the | ||
| % non-RaShi field | ||
| if isscalar(raShiIDSinLayer) && raShiIDSinLayer == stfFieldMCsquareRaShi.rangeShifterID | ||
| stfFieldMCsquare(stfFieldMCsquare.energies == layerEnergy) = []; |
There was a problem hiding this comment.
Potential bug: The variable stfFieldMCsquare is being indexed like an array on line 349 (stfFieldMCsquare(stfFieldMCsquare.energies == layerEnergy) = []), but it was initialized as a struct on line 283, not as an array. This should likely be stfFieldMCsquare.energies(stfFieldMCsquare.energies == layerEnergy) = [] to remove specific energy values from the energies array.
| stfFieldMCsquare(stfFieldMCsquare.energies == layerEnergy) = []; | |
| stfFieldMCsquare.energies(stfFieldMCsquare.energies == layerEnergy) = []; |
There was a problem hiding this comment.
Command injection risk: mcSquareCall is constructed via string concatenation with untrusted data (this.mcSquareBinary) and executed with system, allowing shell metacharacters in the binary path to be interpreted. If an attacker can influence this.mcSquareBinary (e.g., via configuration), they can inject ;, &&, or similar to execute arbitrary commands. Fix by avoiding the shell entirely (e.g., use Java ProcessBuilder or .NET System.Diagnostics.Process to pass executable and args separately), or at minimum strictly whitelist/validate this.mcSquareBinary and quote/escape both the binary and argument:
% Safer (still shell-based): ensure both elements are quoted and validated
cmd = sprintf('"%s" "%s"', this.mcSquareBinary, MCsquareConfigFile);
[status, cmdout] = system(cmd);
% Preferred: Java ProcessBuilder without shell
pb = java.lang.ProcessBuilder(this.mcSquareBinary, MCsquareConfigFile);
proc = pb.start();
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## dev #880 +/- ##
==========================================
+ Coverage 53.85% 54.02% +0.16%
==========================================
Files 308 308
Lines 19938 20011 +73
==========================================
+ Hits 10737 10810 +73
Misses 9201 9201 ☔ View full report in Codecov by Sentry. |
JenHardt
left a comment
There was a problem hiding this comment.
looks good to me, please change the spelling and the semicolon copilot suggested
|
maybe we could also use this opportunity to write a range shifter test for MCsquare ? |
|
This PR was automatically marked as stale it has been open 30 days with no activity. Please review/update/merge this PR. |
wahln
left a comment
There was a problem hiding this comment.
Nice work, but please answer to the copilot comments and address my remaining ones from this review
| % 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); |
There was a problem hiding this comment.
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)
- Using provided range shfiter thickness of %f mm
- Using generic range shifter thickness of %f mm determined to allow ranges below the shortest base data entry.
| end | ||
|
|
||
| methods (Access = protected) | ||
| methods %(Access = protected) |
There was a problem hiding this comment.
Is there actually a reason why this is commented? Could it be deleted?
|
|
||
| matRad_cfg = MatRad_Config.instance(); | ||
|
|
||
| if isfolder(this.externalCalculation) |
There was a problem hiding this comment.
What is the problem here?
also you don't need "else end"
| isoToRaShi = stf(i).SAD - rangeShifter.sourceRashiDistance - rsWidth; | ||
| fprintf(fileHandle,'####IsocenterToRangeShifterDistance\n%f\n',isoToRaShi/10); %in cm |
There was a problem hiding this comment.
I guess this is because of the reported range shifter misalignment in #877 ?
| matRad_cfg.dispWarning('Target exit on the last voxel'); | ||
| exitIx = numel(rho{shiftScen}{1}); | ||
| else | ||
| diff_voi = [diff([rho{shiftScen}{end}])]; |
There was a problem hiding this comment.
diff_voi does not align with camelCase naming convention actually.
| properties | ||
| energy; | ||
| raShiThickness = 50; %Range shifter to be used if useRangeShifter = true; | ||
| visBool = false; |
There was a problem hiding this comment.
I don't like "visBool". Alternative suggestions? "visualize"?
There was a problem hiding this comment.
Does this also test with range shifter? I don't see it.
Minor bug fixes: