Skip to content

Update on merging VMAT into dev - #377

Open
wahln wants to merge 193 commits into
devfrom
dev_VMAT_merge
Open

Update on merging VMAT into dev#377
wahln wants to merge 193 commits into
devfrom
dev_VMAT_merge

Conversation

@wahln

@wahln wahln commented Sep 20, 2019

Copy link
Copy Markdown
Contributor

Dear @markbangert @eric11210 ,

this is my updated attempt at merging dev_VMAT with dev especially concerning the new optimization interface, but also dose calculation.

What I did here, is merge dev_VMAT into a copy of the current dev branch. I think that this is better than what I did before (#376 ), because know you have a better acces to the changed files within this pull request (and see the changes i did within the merge commit).
Also @markangert should be able to close (#308 ).

It seems like it runs smoothly now with the old settings, i.e. fluence Opt & DAO opt without preconditioning. It doesn't work with VMAT yet (dose calculation works, but fluence optimization is weird and DAO/VMAT doesn't work)
But anyways, I hope you now have a better basis to continue with the merge.

Some comments:

  • integration worked quite well with VMAT Optimization Problem deriving from DAO. Yet I am not clear on how to do something similar with the daoVec2Aperture stuff. I am not familiar enough with the implementation, at the moment the changes are in the OptimizationProblemDAO class within the respective functions.
  • for me siochi leaf sequencing with fails with the matRad.m script, but only when sequencing is turned on and DAO (& of course VMAT) is turned of. Yet I like the idea of putting the number of leves into pln, so I changed all sequencers to have the pln struct in their call.
  • With preconditioning enabled, DAO gives worse results than directly after sequencing.
  • I don't like that the fluence optimization now needs the stf only because the tiny part of VMAT code. Can this be handled differently?
  • Should we just write a matRad_VMAToptimization that itself calls fluence optimization and dao?
  • Merging was at some points weird (git associated some wierd code parts with each other esp in generateStf and dosecalc). I hope I didn't mess something up there.

Let me know if you have questions. I will let you know if I forgot something in the list.

P.S.: Does someone of you have a very light test script for the VMAT?

New: Siochi leaf sequencing with more fluid number of kept apertures.
Still respect constraints of min/max gantry angle spacing.
Changed the first gantry angle from 0 to non-zero.  This makes things
much easier.
Figured out problem with DAO: the leaf speed was defined as the
difference between positions, not absolute difference.  Then speed was
constrained to be between 0 and 6 cm/s, which forced unidirectional
motion, increasing the objective function.

Temporary fix: allow negative values.  Eventually, change definition of
function so that it considers the absolute difference.  Will also have
to change the Jacobian.
Fixed leaf speed constraints and Jacobian.  Siochi sequencing ready to
be pilled to dev.
# Conflicts:
#	optimization/matRad_constFuncWrapper.m
#	optimization/matRad_jacobFuncWrapper.m
Testing Revert
This reverts commit 3fe0406.
Weight to MU changed from 1 to 100.
pln.bioOptimization put into options structure
This reverts commit 79e5632.
Added leaf speed constraints back in
Changed leaf speed from mm/s -> cm/s, greatly reducing treatment time.

Added function helping to test planning capabilities.

Fixed bug in Dij sampling, when no voxels are outside of the core.
Shifted time variable to be the time over the optimized beam's arc,
rather than the time between optimized beam angles.  This makes more
sense when calculating e.g. dose rate.  Also, this will make more sense
when doing dynamic fluence calculation.
Minor tweaks and bugfixes.
Split the daoVec2ApertureInfo into 3 functions so that we didn't have to
keep running if statements on VMAT, dynamic vs. static fluence
calculation, etc.
Added option for dynamic fluence optimization for VMAT DAO, including
support for interpolated apertures.
Support for Jacobian scaling
Added support for 4D dose calculation, including deformed CT cubes and
deformation vectors.
This reverts commit de9682f.
wahln and others added 5 commits July 21, 2026 22:03
Everything else matching matRad_calcPhotonDose*.m at the doseCalc top
level is a deprecation shim that warns and redirects to the engine
architecture; the VMC wrapper is a real implementation serving as the
backend of DoseEngines.matRad_PhotonVmcEngine and does not warn. Placing
it in doseCalc/vmc++/ with the helpers it calls makes that distinction
visible; the engine keeps calling it by name. Touching the file brings
it up to the MISS_HIT style rules, with a vmc++-local .miss_hit
grandfathering the legacy wrapper's metrics like the MC engines in
+DoseEngines.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- apertureInfo.newIteration was written once (with a comment asking
  whether it is needed) and read nowhere - the answer is no.
- optDelivery recomputes the per-segment maximum leaf speeds as its first
  step, so the explicit maxLeafSpeed call before it was redundant; the
  magic second argument 1 is optDelivery's default and is dropped.
- The wDao local duplicated w for no reason, and matRad_sequencing
  assigned a visMode fallback value that was never read.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The files revived from the old VMAT fork still referenced the fork-era
LICENSES.txt; the repo standard header references LICENSE.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tems

- matRad_recalcApertureInfo and matRad_recalcApertureBixelWeights are
  implementation details of matRad_refineApertureArc (its only caller);
  they move to matRad/optimization/private/ so the public API surface is
  just matRad_refineApertureArc.
- matRad_bixWeightAndGrad becomes matRad_calcBixelWeightAndGradient -
  "bixel" is spelled out everywhere else in the codebase - and its
  pre-2018 personal GPL header is replaced by the standard license block.
  It stays public because the @matRad_OptimizationProblemVMAT methods
  call it.
- matRad_sequencing gets the same local dose-cube merge helper as the DAO
  wrapper instead of an inline fieldnames loop.
- Example 22 no longer spells out matRad_backProjection internals in its
  commented dij-reuse recipe; it points at the documented 'reuseDij'
  option instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pln.propOpt.runDAO and pln.propSeq.runSequencing are workflow gates read
only by the matRad.m script and the GUI buttons - no function the VMAT
example calls reads them, so carrying them in the example suggested a
plan dependency that does not exist. The example keeps only
pln.propOpt.runVMAT, the actual mode flag consumed by fluence
optimization, sequencing and DAO. The stale commented-out runDAO hijack
block in matRad_WorkflowWidget (the GUI gates on its buttons, not on
pln.propOpt.runDAO) is removed, with small hook fallout fixes in that
file and the gui .miss_hit covering double-underscore GUIDE callbacks.

The numLevels property now documents its VMAT semantics in the example
and the sequencer base class: it is the starting stratification level
count, increased automatically until every FMO beam yields at least as
many apertures as it has DAO child angles.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR is an updated merge of the VMAT development branch into dev, aligning VMAT/DAO with the refactored optimization/sequencing interfaces while adding VMAT-focused utilities (delivery metrics, arc refinement) and new regression/consistency tests.

Changes:

  • Introduces a dedicated matRad_OptimizationProblemVMAT (subclassing DAO) plus VMAT-specific constraint plumbing and post-processing (leaf-touching cleanup, max leaf speed, delivery-time optimization).
  • Adds VMAT analysis tooling: delivery metrics (matRad_calcDeliveryMetrics) and fine-angle resampling/recalculation (matRad_refineApertureArc + private helpers).
  • Updates sequencing/DAO integration (including deprecation bridging), GUI wiring, and expands test coverage for VMAT sequencing and DAO/VMAT derivatives.

Reviewed changes

Copilot reviewed 76 out of 77 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
test/sequencing/test_xiaLeafSequencing.m Spelling fix in test comment.
test/sequencing/test_siochiLeafSequencingVMAT.m New VMAT sequencing + delivery metrics + arc refinement regression tests.
test/sequencing/test_siochiLeafSequencing.m Fixes incorrect test suite name and typo.
test/sequencing/test_sequencerBase.m Updates tests for renamed sequencing property (numLevels) and adds deprecated-alias test.
test/sequencing/test_engelLeafSequencing.m Spelling fix in test comment.
test/sequencing/test_directApertureOptimization.m Renames test function and adds prescription scaling regression test.
test/optimization/test_daoGradientConsistency.m New finite-difference checks for DAO and VMAT objective gradients / constraint Jacobians.
test/doseCalc/test_TopasMCEngine.m Updates sequencing configuration key (numLevels) and minor formatting.
matRad/util/matRad_upgradeApertureInfo.m New migration helper for legacy apertureInfo field names.
matRad/util/matRad_SpotRemovalDij.m Refactors logging/formatting and cleans up property definitions/spacing.
matRad/util/matRad_getFieldOrDefault.m New utility for safe struct-field access with optional warning/deprecation warning.
matRad/sequencing/matRad_xiaLeafSequencing.m Removes deprecated functional wrapper (Xia).
matRad/sequencing/matRad_siochiLeafSequencing.m Removes deprecated functional wrapper (Siochi).
matRad/sequencing/matRad_SequencingPhotonsXiaLeaf.m Renames sequencingLevel usage to numLevels and minor formatting fixes.
matRad/sequencing/matRad_SequencingPhotonsEngelLeaf.m Renames sequencingLevel usage to numLevels, adds miss_hit pragma, fixes typos/formatting.
matRad/sequencing/matRad_sequencing2ApertureInfo.m Removes legacy sequencing-to-apertureInfo converter.
matRad/sequencing/matRad_engelLeafSequencing.m Removes deprecated functional wrapper (Engel).
matRad/planAnalysis/matRad_calcDeliveryMetrics.m New VMAT delivery metrics computation (MU/time/leaf speeds/constraint fractions).
matRad/optimization/projections/matRad_DoseProjection.m Adds optional dij.scaleFactor support in forward projection + gradient backprojection.
matRad/optimization/private/matRad_recalcApertureInfo.m New helper to rebuild/interpolate apertureInfo for a new steering-angle grid.
matRad/optimization/private/matRad_recalcApertureBixelWeights.m New helper to recompute bixel weights/shape maps after aperture arc resampling.
matRad/optimization/matRad_refineApertureArc.m New public API to resample VMAT apertures onto a finer angular grid.
matRad/optimization/matRad_preconditionFactors.m New Jacobi preconditioner factor computation for DAO/VMAT apertures.
matRad/optimization/@matRad_OptimizationProblemVMAT/optDelivery.m New VMAT delivery-time optimization (rate/speed constraints), with re-interpolation trigger.
matRad/optimization/@matRad_OptimizationProblemVMAT/maxLeafSpeed.m New VMAT max leaf speed computation.
matRad/optimization/@matRad_OptimizationProblemVMAT/matRad_OptimizationProblemVMAT.m New optimization-problem subclass wiring VMAT overrides and helpers.
matRad/optimization/@matRad_OptimizationProblemVMAT/matRad_getConstraintBounds.m New VMAT constraint bounds (dosimetric + leaf-speed + MU-rate).
matRad/optimization/@matRad_OptimizationProblemVMAT/matRad_daoApertureInfo2Vec.m VMAT-specific apertureInfo→vector conversion including gantry time entries and bounds.
matRad/optimization/@matRad_OptimizationProblemVMAT/matRad_constraintFunctions.m VMAT constraint evaluation (adds leaf speed + MU-rate constraints to DAO).
matRad/optimization/@matRad_OptimizationProblemVMAT/leafTouching.m New VMAT leaf-touching cleanup post-processing.
matRad/optimization/@matRad_OptimizationProblemDAO/matRad_OptimizationProblemDAO.m Accepts legacy apertureInfo via upgrade chokepoint and formatting cleanup.
matRad/optimization/@matRad_OptimizationProblemDAO/matRad_objectiveGradient.m Adds analytic Jacobian path (bixelJApVec) and adjusts gradient scaling for jacobiScale.
matRad/optimization/@matRad_OptimizationProblemDAO/matRad_getJacobianStructure.m Refactors dosimetric Jacobian structure assembly and constraint ordering.
matRad/optimization/@matRad_OptimizationProblemDAO/matRad_getConstraintBounds.m Reorders bounds to match new constraint concatenation order.
matRad/optimization/@matRad_OptimizationProblemDAO/matRad_daoApertureInfo2Vec.m Updates vector layout to incorporate jacobiScale into weight variables and field renames.
matRad/optimization/@matRad_OptimizationProblemDAO/matRad_constraintJacobian.m Reorders Jacobian rows and adds analytic Jacobian shortcut when available.
matRad/optimization/@matRad_OptimizationProblemDAO/matRad_constraintFunctions.m Reorders constraint vector to match new convention (dosimetric first).
matRad/matRad_sequencing.m Refactors sequencing wrapper: photon vs particle paths, VMAT bridging, and safe dose-cube merging.
matRad/matRad_directApertureOptimization.m Adds VMAT/DAO polymorphic dispatch, preconditioning/prescription scaling flow, and result merging.
matRad/MatRad_Config.m Adds default propSeq.numLevels, fixes loadobj merge recursion, and minor platform checks/formatting.
matRad/gui/widgets/matRad_WorkflowWidget.m GUI spelling cleanups and minor refactors.
matRad/gui/widgets/matRad_ViewingWidget.m Uses existing stf (if present) to determine number of beams; adds fallback logic.
matRad/gui/widgets/matRad_PlanWidget.m Updates GUI plan serialization to propSeq.numLevels, spelling fixes, and stores propStf.numOfBeams.
matRad/gui/.miss_hit Adds relaxed miss_hit rules for legacy GUI code.
matRad/doseCalc/vmc++/matRad_vmcOptions.m New VMC++ option assembly helper.
matRad/doseCalc/vmc++/matRad_readDoseVmc.m New VMC++ binary dose reader.
matRad/doseCalc/vmc++/matRad_matRad2vmcSourceAngles.m New gantry/couch→VMC++ angle conversion helper.
matRad/doseCalc/vmc++/matRad_exportCtVmc.m New CT export utility for VMC++.
matRad/doseCalc/vmc++/matRad_createVmcInput.m New VMC++ input file writer.
matRad/doseCalc/vmc++/matRad_createVmcBatchFile.m New batch launcher script generator for VMC++.
matRad/doseCalc/vmc++/matRad_bixelPhspVmc.m New phase-space splitting helper for bixelized sources.
matRad/doseCalc/vmc++/.miss_hit Adds relaxed miss_hit rules for legacy VMC++ wrapper code.
matRad/doseCalc/+DoseEngines/matRad_PhotonVmcEngine.m New DoseEngines wrapper integrating VMC++ as an optional Monte Carlo engine.
matRad/doseCalc/+DoseEngines/matRad_PhotonPencilBeamSVDEngine.m Adds weightToMU initialization fallback in photon PB-SVD engine.
matRad/doseCalc/+DoseEngines/matRad_ParticleMCsquareEngine.m Fixes typos, improves warnings, minor refactors, and Windows binary invocation hardening.
matRad/doseCalc/+DoseEngines/@matRad_ParticleFREDEngine/calcDose.m Fixes LICENSE link typo.
matRad/doseCalc/+DoseEngines/.miss_hit Adds relaxed miss_hit rules for dose engine package.
matRad/dicom/@matRad_DicomImporter/matRad_importDicomSteeringPhotons.m Improves isocenter handling (single vs per-beam) and formatting.
examples/matRad_example3_photonsDAO.m Clarifies that this example is static delivery; points to VMAT example.
examples/matRad_example22_photonsVMAT.m New end-to-end VMAT example script (FMO → sequencing → DAO → analysis → refinement).
CHANGELOG.md Updates changelog entry to reflect removal of legacy sequencing wrappers and VMAT OO integration.
.miss_hit Updates miss_hit naming rules for methods/classes/attributes.
.codespellrc Adds assignin to ignore list.
Comments suppressed due to low confidence (1)

matRad/optimization/@matRad_OptimizationProblemVMAT/leafTouching.m:76

  • Same issue as above: doseAngleBorders belongs to apertureInfo.arc.beam, not apertureInfo.beam. Using the wrong struct path breaks gantry-angle sampling for the final border.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread matRad/optimization/@matRad_OptimizationProblemVMAT/leafTouching.m Outdated
Comment thread matRad/sequencing/matRad_calcMaxLeafSpeed.m
Comment thread matRad/optimization/@matRad_OptimizationProblemDAO/matRad_getJacobianStructure.m Outdated
wahln and others added 14 commits July 27, 2026 02:18
The max*GantryAngleSpacing properties are upper bounds, but the arc
subdivision derived the dose and DAO counts with ceil while deriving the
FMO:DAO ratio with floor, then rounded even ratios down to satisfy the
odd-centring requirement. Both steps paid for the bounds with apertures,
the one quantity that must not be sacrificed: example 22's 15/30/45
collapsed to a single aperture per fluence map, stripping all modulation
out of the sequenced plan and leaving DAO nothing to recover. Any maxFMO
below 3x the realized DAO spacing behaved the same way.

Derive the nesting top-down instead (FMO -> DAO -> dose), with every count
from ceil and every odd correction rounding up. Going finer can never
violate a maximum, so the aperture budget is now the invariant and the
spacings give way. Add minAperturesPerFMOBeam (default 3), optional
explicit aperturesPerFMOBeam/doseAnglesPerDAOBeam factors, and
maxNumOfDAOAngles as an explosion guard.

Unify both delivery modes on centre sampling: every beam sits at the centre
of its own sector. This makes the three angle sets nest exactly, keeps each
beam strictly inside the arc (a closed 360 deg arc previously placed two
beams on the same physical gantry position in step-and-shoot), and lets the
DAO/FMO sets be selected by index so membership tests compare bit-identical
values. It also means dose beams now exist outside the DAO-bracketed span,
which broke three assumptions that beam 1 is always a DAO beam:
prepareArcs carried lastDAOBeamIx/nextDAOBeamIx forward from the previous
iteration, leafTouching probed beam(1).shape(1) to pick its sampling mode,
and the first DAO beam's leading leaf-sweep segment got index 0.

Other fixes found while reviewing the branch:

- Time interpolation fractions took abs() of the numerators, which defeated
  the [0,1] clamp when the DAO border fell outside the dose sector: both
  fractions clamped to 1 instead of to 0 and 1, so they no longer summed to
  1 and the interpolated gantry speed came out ~1.5x off. Use a signed
  denominator so the arc direction cancels and the clamp still discriminates.
- matRad_recalcApertureInfo anchored the beam-centre trajectory only at the
  old beam centres, which do not bracket the outermost new centres, so
  refinement returned NaN leaf positions on the first and last beam.
- leafTouching built its interpolation support as setdiff(1:numBeams, ...),
  covering only half the columns in its twice-per-beam mode. Behaviour is
  unchanged on the live path; that mode has never been reachable from the
  sole call site, which is now recorded in the file.
- discardApertures leaked input class and orientation into its output and
  silently zeroed a beam whose kept apertures had no dose-area product.
- matRad_getWorldAxes reduced a 1x3 isfield result with an implicit all(),
  so a partially populated grid struct errored instead of being completed.
- VMC++: portable batch invocation, clearer doseGrid override naming, and a
  single identified error for the scenario/grid contract.

Add dispInfo/dispDebug coverage across the VMAT pipeline: realized arc
geometry and aperture budget, per-map stratification and shape
distribution, apertureInfo problem size, delivery-limit checks, DAO
variable breakdown and machine limits, and the refinement resampling step.
None of the above was visible before, which is why the aperture collapse
went unnoticed.

Move leafTouching, maxLeafSpeed and optDelivery out of
@matRad_OptimizationProblemVMAT into sequencing as matRad_leafTouching,
matRad_calcMaxLeafSpeed and matRad_enforceDeliveryConstraints. They are
arc-sequencing helpers that the optimization problem never used as methods.
The differentiable leaf-speed constraint stays in the optimization problem.

Tests: new test/steering/test_stfGeneratorPhotonVMAT.m covers the
subdivision invariants across configurations, arc directions and both
delivery modes, pins the formerly degenerate 15/30/45, and checks the
validation paths. The sequencing suite gains a fixture with interpolated
beams, since the reworked geometry makes 15/30/45 resolve to one dose angle
per DAO sector and would otherwise have lost that coverage entirely.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The private helpers introduced by the previous split are single-use: the
views and the player share the aperture geometry, weight colouring and
panel bookkeeping, so splitting them only moved that code into files
nothing else calls. Inline them again and exempt the file instead:

- an in-file MISS_HIT pragma justifies the file_length metric, placed at
  the end of the file so it does not become the function's help text
- matRad/util/.miss_hit turns off mh_style's duplicate of that check,
  which has no per-file justification mechanism; the metric itself stays
  enabled, so other files still have to stay under the limit

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The fluence objective work touches a number of files that predate the current
pre-commit configuration and do not satisfy it, so no commit including them
could pass the hooks. This is that mechanical pass, kept separate so that the
commits following it show behaviour changes only:

- mh_style formatting: indentation, comma and comment spacing, statement
  layout. No functional edits.
- 9 codespell findings, all in comments.
- 24 lines wrapped to the 150 character limit. The 64 row jet colormap literal
  in matRad_OptimizationWidget is split over its matrix rows; the numeric
  sequence was compared against the original and is identical.
- mh_metric justifications for the pre-existing cyclomatic complexity and
  control nesting deviations of matRad_objectiveFunction,
  matRad_objectiveGradient and matRad_selectVoxelsFromCst, following the
  precedent already set in matRad_fluenceOptimization.
- convertOldOptimizationStruct's parameter renamed old_s -> oldStruct, which
  the parameter naming rule requires and mh_style cannot fix by itself.

Reviewing this commit with --ignore-all-space reduces it to the wrapped lines,
the pragmas and the rename.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Smoothing the fluence during FMO needs objectives that act on w itself instead
of on a dose quantity, so they take no part in the backprojection and their
gradient already is a gradient w.r.t. the weights.

Extract the quantity independent part of matRad_DoseOptimizationFunction into a
new common ancestor matRad_OptimizationFunction (parameters, struct,
createInstanceFromStruct) and put matRad_FluenceOptimizationFunction beside it.
matRad_DoseOptimizationFunction keeps its name, its API, and the robustness and
dose parameter accessors.

FluenceObjectives.matRad_FluenceVariance penalizes the fluence variance within
each beam, normalized either per beam ('relative', invariant under a rescaling
of the fluence so penalties transfer between plans) or not at all ('absolute',
the plain quadratic form w'Sw). The per-beam denominator is deliberate: pooling
the mean square over all beams would put the differences between the beam
levels into the denominator, so the objective could be lowered by driving the
beams apart in intensity rather than by smoothing, which on a VMAT arc
concentrates the plan on a few control points.

It needs only the bixel-to-beam mapping the dij carries, so it works for
photons and particles alike. Smoothing that couples neighbouring bixels needs
the beamlet geometry from the stf and has to wait for an optimization entry
point that receives it.

Fluence objectives are accepted both in pln.propOpt.fluenceObjectives and next
to the dose objectives in cst{i,6}; matRad_getFluenceObjectives collects and
initializes them. Direct aperture optimization warns and ignores them, since
smoothing a sum of deliverable apertures has no well defined meaning -
dedicated aperture objectives would be needed there.

Also extract the cst objective promotion and per-fraction dose rescaling that
matRad_fluenceOptimization and matRad_directApertureOptimization duplicated
into matRad_fractionateCstFunctions; the DAO copy had lost the compatibility
branch for the old struct array format.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The VMAT branch of the siochi sequencer applied a hard coded Gaussian blur to
every FMO fluence map before stratifying it. Turn that into the sequencer
property arcFluenceSmoothing ('gaussian' | 'none'), settable from pln.propSeq.

It has to stay on by default. A fluence that is flat over its field decomposes
into exactly one aperture at any number of stratification levels (measured:
1 1 1 1 1 1 1 1 1 1 for levels 1 to 10, against 1 2 3 4 4 4 5 5 5 7 with the
blur), so the loop that raises numLevels until every FMO beam yields one
aperture per DAO control point depends on the blurred field rim to converge at
all. Selecting 'none' used to spin there forever, so that loop is now bounded
and reports which setting to change instead of hanging.

Add apertureSelection ('doseAreaProduct' | 'leastSquares') for reducing the
apertures the stratification generates to the number the arc can deliver. The
existing heuristic keeps the ones with the highest dose-area product - which,
because the stratification gives every segment the same weight, means the
widest ones - and rescales them by a common factor. 'leastSquares' instead
keeps the apertures that best reconstruct the fluence, by forward stepwise
selection with a non-negative least squares refit at every candidate, and then
rescales to the original dose-area product. Refitting before choosing matters:
plain matching pursuit commits to an aperture before the other weights can
adapt and ends up worse than the heuristic, and without the final rescale the
fit loses 12 to 15% of the monitor units and underdoses.

Measured on TG119 with three apertures per beam, 'leastSquares' moves the
sequenced plan from 0.97 to 1.01 of the fluence optimization target D95 and its
D5 overshoot from 1.29 to 1.13. The default is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Same mechanical pass as the earlier one, for the example test harness, so that
the change adding the VMAT example to it stays readable:

- mh_style formatting, and the matRad_unitTestTextManipulation calls wrapped to
  the 150 character limit
- codespell: supressing -> suppressing
- the local function renamed runSingleExampleTest -> helper_runSingleExampleTest,
  which the function naming rule requires for local functions in test files

No behaviour change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Each fluence map is optimized at a single gantry angle but delivered as
minAperturesPerFMOBeam separate apertures spread over the FMO sector. The sum
reproduces the optimized fluence, yet every individual control point receives
only one fragment of it, so the wider that sector, the more the delivered dose
clusters into a few entrance directions.

Measured on this case as the peak-to-mean of the out-of-target dose binned by
gantry angle, the excess that sequencing introduces is +38% at 72 deg, +30% at
45 deg and +2% at 24 deg. Direct aperture optimization then amplifies whatever
it inherits rather than repairing it - a nearly closed aperture has almost no
gradient, so its weight drifts to zero while the already dominant control points
grow: the angular dynamic range goes 2.5 -> 5.7 at 72 deg but only 1.36 -> 1.65
at 24 deg. Target coverage and maximum dose look healthy in every case (after
DAO the coarse plan even has a lower maximum than the fluence optimization
result), so this is invisible to the usual metrics and shows up only in the
spatial dose distribution. Drop maxFMOGantryAngleSpacing to 24 deg accordingly.

Add the example to the example tests. Arc plans are expensive in every stage, so
preprocess the gantry angle spacings the same way the bixel width and the dose
grid resolution already are: collapse the arc to three angles that are each
their own DAO control point and FMO angle, one aperture per beam, with a coarse
fine-arc recalculation and a static aperture view instead of the real time
playback. minAperturesPerFMOBeam has to be overridden as well, or the stf
generator subdivides the DAO angles again and undoes the coarsening. Runs in
about 46 s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the entries this line of work was missing: VMAT planning and the fluence
objectives as one Added item each, the VMC++ dose engine and the new aperture
visualization views individually, plus the individual DAO, sequencing and API
changes and the correctness fixes made along the way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@wahln

wahln commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

I did quite a substantial overhaul (thanks to the help of Claude Code) to fit the branch more to matRad's current philosophy.
Here's a summary:

What is new since my last comment

  • The properties on optimization, VMAT angle configuration, and sequencing are cleary separated
  • Sequencing was adapted to the object-oriented structure contributed Class based Sequencing #891
  • I added a preliminary implementatinon on fluence smoothing objectives to help the sequencer. This will be further expaneded in the coming overhaul of optimization
  • VMC++ is now integrated into the DoseEngine concept for those who have an installation

Other refactors

  • The DAO and VMAT optimization problems share one core instead of two
    near-duplicate copies.
  • Arc bookkeeping was renamed for readability (propVMATarc,
    lim_l/lim_rlimLeft/limRight, and similar). matRad_upgradeApertureInfo
    accepts aperture info saved by older versions.
  • FMO no longer needs the stf: the FMO-beam bookkeeping travels in the dij.

Roughly a dozen correctness bugs in the pre-existing DAO/VMAT code were fixed
along the way — the DAO Jacobian, leaf bookkeeping, arc pruning and reverse
arcs among them. New tests cover VMAT stf generation, sequencing, aperture
conversion, DAO/VMAT gradient and Jacobian consistency, the fluence objectives
and the VMC++ engine.

Notes

  • maxFMOGantryAngleSpacing matters more than it looks. Each fluence map is
    delivered as several apertures spread over its FMO sector, so a wide sector
    makes the delivered dose cluster into a few entrance directions, and DAO
    amplifies that rather than repairing it. Measured on the example, sequencing
    adds +38% angular dose non-uniformity at 72°, +2% at 24°. Target coverage and
    maximum dose look healthy either way, so it is only visible in the spatial
    dose distribution. The example now uses 24°.

wahln and others added 5 commits July 30, 2026 15:51
The old scheme (matRad_SequencingModalityTypeCollimator) put the
distinguishing part of the name last and read as an action rather than an
object, so the classes neither sorted together in the folder nor matched
the dose engine naming they mirror:

  matRad_PhotonSequencerAbstract     -> matRad_PhotonLeafSequencerAbstract
  matRad_PhotonSequencerVMATAbstract -> matRad_PhotonLeafSequencerVMATAbstract
  matRad_SequencingPhotonsSiochiLeaf -> matRad_PhotonLeafSequencerSiochi
  matRad_SequencingPhotonsXiaLeaf    -> matRad_PhotonLeafSequencerXia
  matRad_SequencingPhotonsEngelLeaf  -> matRad_PhotonLeafSequencerEngel
  matRad_ParticleSequencer           -> matRad_ParticleScanningSequencerSpill

matRad_SequencerBase keeps its name, carrying neither modality nor
collimator. The particle sequencer gains a type, since its timing model
(spill recharge, spill size and intensity) is specific to a spilled beam
and leaves room for a continuous-beam variant; its display name now ends
in "Sequencer" like the other three. shortName is untouched throughout,
so pln.propSeq.sequencer is unaffected, and sequencer discovery goes
through matRad_findSubclasses rather than a name pattern.

Since the whole hierarchy is unreleased, the changelog entry that
introduces it simply names the final classes instead of recording a
rename.

Also brings the touched files up to the pre-commit standard, which had
never seen them: three typos, trailing whitespace, one 202-character
line and five test functions whose underscore names violate
regex_function_name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
sphinx-build reported 24 warnings, 9 of them ERROR and 5 CRITICAL. All of
them were malformed reStructuredText in docstrings or .rst sources, so
the affected pages rendered wrongly rather than not at all:

- Parameter tables written as a description ending in ':' followed by an
  aligned block are not RST. Where the block was only as deep as the
  description text, docutils read the wrapped continuation lines as
  unexpected indentation and ran the whole table together into prose.
  These are now literal blocks, indented one level past their
  description.
- matRad_ImageRegistrationBase had a bullet list with no blank line
  before it, so the first item was absorbed into the preceding paragraph.
- Five functions opened their help text with the '%%%%' banner instead of
  a summary line. docutils read it as a section-title overline (missing
  underline -> CRITICAL), and MATLAB's own H1 mechanism, lookfor
  included, saw a row of percent signs as the summary. The banner is
  dropped and the license block merged into the help text, which is what
  the other 368 files in matRad/ do.
- The docs referenced :ref:`basedata_photons` as '_basedata_photons',
  underlined a title too short, and indented a bullet list one level too
  deep inside an admonition.
- test_TopasMCEngine.m declares function test_topasMCEngine, which the
  MATLAB domain flagged as a name mismatch. The file is renamed rather
  than the function, since 'test_TopasMCEngine' does not satisfy
  regex_function_name.

Touching a .rst file for the first time also exposed that the rstcheck
hook cannot pass on documentation that sphinx builds cleanly: it checks
one file at a time, so cross-file :ref: labels look like unreferenced
targets, and it does not load third-party extensions, so the MATLAB
domain's :scpt: role is an unknown role. Its report level is raised to
warning and scpt is declared. codespell likewise flagged a German thesis
title and a hostname, both now declared in .codespellrc.

sphinx-build -E -W --keep-going now exits 0 with an empty warning log.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…dard

Both files predate the current pre-commit configuration, so no commit
touching them could pass the hooks. Separating the mechanical part keeps
the following Octave fix reviewable - it is three lines across these two
files, against roughly 190 lines of reformatting here.

Beyond what mh_style fixes on its own (comma and operator whitespace,
trailing whitespace, missing final newline) this wraps six lines over the
150 character limit, renames the local functions cutAtArgmin and
parseFromDicom to satisfy regex_function_name, fixes two typos codespell
flags, and justifies the report generator's cyclomatic complexity of 54
following the precedent of matRad_fluenceOptimization.

No behaviour change: the two-argument round() calls that motivated
touching these files are deliberately left as they are.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Octave's round takes the value to be rounded only; passing a digit count
fails with "Invalid call to round". Since arguments are evaluated before
the call, this errored even where the result was only going to be
discarded, as in the VMAT steering generator's dispDebug calls at a log
level that does not print them.

The new matRad_roundCompat joins the other util/octaveCompat helpers and
implements Matlab's round(x,n) semantics for both environments, using the
native call where it exists. Eleven call sites were affected: three in
VMAT steering information generation, six in the sampling report, one in
the DVH band plot and one in a VMAT steering test. Five of them passed a
digit count of zero, which is plain round(x), so only six actually need
the helper.

Five hot paths already carry a local copy of the scaling idiom
(matRad_calcBixelWeightAndGradient, matRad_daoVec2ApertureInfo,
matRad_ParticlePencilBeamEngineAbstract, matRad_postprocessing and
matRad_StfGeneratorParticleSingleBeamlet). Those are left untouched: they
are Octave-safe as written, and routing them through this function would
change what they compute, since scaling by 10^n and rounding is not
bit-identical to round(x,n) - which matters at the 10 and 15 digits the
DAO leaf position bookkeeping asks for. The docstring says so, so that
they do not get unified later by mistake.

test_roundCompat covers the semantics against Matlab's native round and
scans matRad/, test/ and examples/ for reintroduced two-argument calls.
Verified with Octave 8.4.0: test_roundCompat and the VMAT steering tests
pass there, 14 passed and 1 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
VMAT had no user facing documentation at all, and the technical guide
only mentioned sequencing in passing, as a parenthesis on the fluence
optimization step. Since sequencing and DAO are workflow steps that users
invoke rather than cross cutting topics, this adds them as a step of
their own between fluence optimization and visualization, rather than
under "Additional information".

The new page covers the three photon delivery techniques and how they are
switched on, the available leaf sequencers and how to add one, what DAO
optimizes and why it reuses the fluence optimization machinery, and VMAT:
the nested FMO / DAO / dose angle hierarchy, the machine limits that
constrain the arc with their fallback values, the arc specific sequencing
options, and how to check a finished plan. It also records that
maxFMOGantryAngleSpacing drives how faithfully an arc can be sequenced,
which is not obvious from the parameter name. The three line DAO stub in
planopt.rst is replaced by a pointer, and the quickstart no longer tells
users to call matRad_engelLeafSequencing, which the sequencer refactor
removed.

Two gaps in the API documentation are fixed along the way. The
FluenceObjectives package was absent from both the module index and the
API pages, because nothing referenced it - only packages named by an
automodule directive are indexed. Classes in @ folders were missing for
the same kind of reason: automodule does not descend into them, so
matRad_OptimizationProblem with its DAO and VMAT subclasses,
matRad_DoseEngineBase and matRad_ParticleFREDEngine were documented
nowhere, and the references to them in planopt.rst silently rendered as
plain text. They are now pulled in with explicit autoclass directives.

Documenting those files for the first time exposed malformed docstrings
in matRad_FluenceVariance, matRad_ParticleFREDEngine and
matRad_daoApertureInfo2Vec, of the same kind fixed in 55fd43e, so
sphinx-build -E -W --keep-going still exits 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@wahln

wahln commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

I think the time has come that this is (finally) ready to be merged 🎉
I will let it age for a few days in case you are still following this and have a few comments, @eric11210, then I will merge. @JenHardt you can also check, I changed the naming structures on the OOP sequencing.

Due to a bunch of API changes this will also mean that next matRad after this merge will be a new major release, into which I will fold an optimization overhaul as well, which will likely change the fluence and aperture optimization call structure a bit.

Thanks to everybody involved for the hard work on this.

@github-actions

Copy link
Copy Markdown

Code Coverage

Package Line Rate Health
coverage Package 1 59%
Summary 59% (14513 / 24561)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants