top of page
  • Black LinkedIn Icon
  • YouTube
  • Gateway Scripts
  • Whatsapp

Clinical Investigation - Trajectory Log Quality Assurance

Background

Modern radiotherapy accelerators provide extremely detailed delivery records through trajectory logs. These logs capture machine state information at high frequency during treatment delivery, including multileaf collimator (MLC) positions, gantry angle, collimator angle, jaw positions, and cumulative monitor units (MU). Because of this level of detail, trajectory logs have become an increasingly valuable tool for delivery verification and patient-specific quality assurance (PSQA).

At our clinic, trajectory log analysis is integrated into our PSQA workflow. In addition to Portal Dosimetry measurements, we evaluate the agreement between the planned control point parameters and the actual machine delivery recorded in the trajectory logs. This comparison allows us to confirm that the treatment machine executed the plan geometry as intended.


The analysis tool we developed performs this comparison by matching each planned control point to the corresponding machine state in the trajectory log using the cumulative MU as a reference. For each control point, the tool evaluates:

  • MLC leaf positions

  • Gantry angle

  • Jaw positions

The trajectory log samples the machine state at 20 ms intervals, which typically provides sufficient temporal resolution to interpolate the machine state at the MU corresponding to each planned control point.


Under normal conditions, this approach works very well. However, during routine QA analysis we began observing occasional large deviations in leaf position comparisons that appeared inconsistent with the expected mechanical accuracy of the machine.

These discrepancies were particularly surprising because:

  • They occurred only at a small number of control points

  • The errors appeared suddenly, often affecting many leaves simultaneously

  • The magnitude of the deviation was much larger than typical MLC positioning tolerance

Initial investigation suggested that the issue was appearing on Halcyon machines (fast leaves) during instances of drastically decreasing dose rates.


The Problem Statement

The issue consistently appeared in portions of treatment plans where the dose rate dropped sharply between control points.

In these regions, the change in MU between adjacent control points can become extremely small. For example (keep in mind the logs are 0-indexed and the plan control points start with 1):

Control Point

MU

CP108 (109 in plan)

66.84

CP109 (110 in plan)

66.85

When the machine delivers such small MU increments, the MU accumulation between trajectory log samples may be insufficient to uniquely resolve the individual control points. Because trajectory logs are sampled at a fixed time interval (approximately 20 ms), it is possible for multiple planned control points to fall between the same pair of trajectory log samples.


Trajectory Log Interpretation

This plan comparison with trajectory logs is made possible by the use of an open-source trajectory interpretation library TrajectoryLog.NET. TrajectoryLog.NET is an open-source C# API developed to interpret the binary trajectory log files written by Varian linear accelerators during treatment delivery. As introduced previously on the Gateway Scripts blog, TrajectoryLog.NET Part I: Introducing TrajectoryLog.NET, the library was created to bring trajectory-log analysis into the .NET ecosystem, making it much easier to integrate machine log evaluation with ESAPI and related Varian scripting workflows.


For our workflow, the value of TrajectoryLog.NET is not just that it reads the log file. Its real strength is that it enables direct comparison between the planned control point data extracted from ESAPI and the actual delivery data recorded by the machine. That integration provides the foundation for a plan-versus-delivery QA tool that can evaluate whether the intended geometry of a beam was reproduced throughout treatment delivery. It is that exact comparison framework that led us to the issue discussed in this post: unexpected discrepancies in low-dose-rate regions where straightforward MU-based matching between planned control points and trajectory-log samples begins to break down.


MLC Comparison Plan vs Log Actual

Original MLC comparison method was based on a straightforward assumption: for each planned control point, the corresponding MLC positions in the trajectory log could be estimated by matching the control point’s cumulative monitor unit (MU) to the cumulative MU recorded in the log.


Using ESAPI, the planned MLC leaf positions were extracted from each control point in the beam. For the corresponding trajectory log, the cumulative delivered MU and the actual MLC positions were parsed using TrajectoryLog.NET. Because the log samples machine state at discrete time intervals rather than exactly at each planned control point, the planned MU usually did not match a trajectory log sample perfectly. To address this, the algorithm identified the trajectory log sample immediately before the planned control point MU and the sample immediately after it.

In very low dose rate regions, multiple planned control points can fall between the same pair of trajectory log samples.

From there, the actual leaf position at the planned control point was estimated by linear interpolation between those two neighboring trajectory samples.

Conceptually, the workflow looked like this:

  1. Loop through each planned control point in the beam.

  2. Compute the cumulative MU for that control point.

  3. Find the trajectory log sample just before that MU.

  4. Find the trajectory log sample just after that MU.

  5. Interpolate the actual MLC position to the exact planned MU.

  6. Compare the interpolated actual leaf position with the planned leaf position from ESAPI.

  7. Record the leaf differences and summarize the maximum and RMS deviations.


For each leaf, the comparison was performed separately for the X1 and X2 banks. After converting the trajectory log leaf positions into the same units and sign convention as the planned ESAPI values, the position differences were calculated. The algorithm then tracked the largest observed deviation across all moving leaves, along with the corresponding leaf index and bank.

In simplified form, the logic looked like this:

 foreach (var cp in beam.ControlPoints)
 {
     double currentMU = cp.MetersetWeight * beam.Meterset.Value;
     double nextMU = cpIndex == beam.ControlPoints.Count() - 1 ?
         currentMU + 1 :
         beam.ControlPoints.ElementAt(cpIndex + 1).MetersetWeight * beam.Meterset.Value;

     int muIndexBefore = Array.FindLastIndex(mus, x => x <= currentMU - muTolerance);
     int muIndex = Array.FindIndex(mus, x => x >= currentMU + muTolerance);

     if (muIndexBefore >= 0 && muIndex >= 0 && muIndex >= muIndexBefore &&
         priorMU != currentMU && currentMU != nextMU)
     {
         foreach (var leaf in movingLeaves)
         {
             double leafPosX1 = cp.LeafPositions[0, leaf];
             double leafPosX2 = cp.LeafPositions[1, leaf];

             // Add one interpolated value across the window endpoints
             double muBefore = mus.ElementAt(muIndexBefore);
             double muAfter = mus.ElementAt(muIndex);

             if (muAfter != muBefore)
             {
                 double mlcX1Before = mLCAct.ElementAt(leaf + 2 + cpCount).ElementAt(muIndexBefore) * 10.0;
                 double mlcX2Before = mLCAct.ElementAt(leaf + 2).ElementAt(muIndexBefore) * 10.0;
                 double mlcX1After = mLCAct.ElementAt(leaf + 2 + cpCount).ElementAt(muIndex) * 10.0;
                 double mlcX2After = mLCAct.ElementAt(leaf + 2).ElementAt(muIndex) * 10.0;

                 double mlcX1 = mlcX1Before + (currentMU - muBefore) * ((mlcX1After - mlcX1Before) / (muAfter - muBefore));
                 double mlcX2 = mlcX2Before + (currentMU - muBefore) * ((mlcX2After - mlcX2Before) / (muAfter - muBefore));

                 double x1Diff = leafPosX1 + mlcX1;
                 double x2Diff = leafPosX2 - mlcX2;

             }


             if (Math.Abs(x1Diff) > maxDiff)
             {
                 maxDiff = Math.Abs(x1Diff);
                 maxLeaf = $"X1: {leaf + 1}";
             }

             if (Math.Abs(x2Diff) > maxDiff)
             {
                 maxDiff = Math.Abs(x2Diff);
                 maxLeaf = $"X2: {leaf + 1}";
             }

             mlc.Add(x1Diff );
             mlc.Add(x2Diff);
             squares.Add(x1Diff * x1Diff );
             squares.Add(x2Diff* x2Diff);
         }
     }

     priorMU = currentMU;
     cpIndex++;
 }
 return (mlc, maxLeaf, Math.Sqrt(squares.Average()));

At a high level, this method assumes that MU is a reliable surrogate for delivery state. In most treatment regions, that assumption holds well enough that the interpolated log positions closely match the planned control point positions. That is why this original implementation worked well in the majority of cases and why the later failures were initially so surprising.


The problem only became apparent when the beam entered regions with very small MU separation between adjacent control points, especially during abrupt dose-rate reductions. In those situations, the neighboring trajectory log samples surrounding a planned MU were sometimes no longer representative of the intended control point geometry.


Investigating MLC Position Discrepancy

Once the leaf positions were extracted with TrajectoryLog.NET and a custom script exported the control point information for the fields with failing MLC, the data was plotted together with python to highlight the issue. In the first field investigation, it was found that the 4 worst control points for MLC discrepancy were the same control points where the dose rate decreased to the low double-digits. Looking at a plot of the MU, its apparent where this low dose rate is occurring (hint: its at the point where the curve goes flat.



Looking at the MLC with the largest discrepancy throughout this same range, it is noticeable that the leaf position is also moving drastically throughout these control points. In the plot below a faster moving leaf would have a steeper curve.


Let's zoom in on these control points in question to magnify these disagreements. As a descriptor to the following plot:

  • The orange dots are the control points from the treatment plan. The MU is recorded and the leaf position determines the Y-axis position of the orange dot.

  • Looking at CP108, both the trajectory log sampled prior (red x) and the trajectory log sampled after (gray x) would both fail the leaf position tolerance of 2mm.

  • It seems apparent that the Halcyon Control system receives instruction to move the leaves just before the MU is reached for CP108. This causes the leaf to drastically shift position to a new location just before the control point MU is met.




From this plot, it was determined to place a 0.1MU search window around each control point and determine if the leaf position traverses through the planned position within this search window.

Effectively, instead of asking:

  • “What was the leaf position exactly at MU = CP MU?”

ask:

  • “Within a small local neighborhood around this control point, what is the closest delivered leaf position to the planned control point geometry?”

For each control point, define a small trajectory window, for example:

  • Starting log position can be 0.1 MU less than planned MU.

  • End control point can be 0.1 MU more than planned MU.

Then compute:

  • the leaf error at every sample in that local window

  • the minimum absolute error for each leaf

  • or the minimum over all leaves / max over leaves, depending on your QA metric.


To implement this, the following changes have been made to the code (shown in bold).

 double muTolerance = 0.1;
 foreach (var cp in beam.ControlPoints)
 {
     double currentMU = cp.MetersetWeight * beam.Meterset.Value;
     double nextMU = cpIndex == beam.ControlPoints.Count() - 1 ?
         currentMU + 1 :
         beam.ControlPoints.ElementAt(cpIndex + 1).MetersetWeight * beam.Meterset.Value;

     int muIndexBefore = Array.FindLastIndex(mus, x => x <= currentMU - muTolerance);
     int muIndex = Array.FindIndex(mus, x => x >= currentMU + muTolerance);

     if (muIndexBefore >= 0 && muIndex >= 0 && muIndex >= muIndexBefore &&
         priorMU != currentMU && currentMU != nextMU)
     {
         foreach (var leaf in movingLeaves)
         {
             double leafPosX1 = cp.LeafPositions[0, leaf];
             double leafPosX2 = cp.LeafPositions[1, leaf];

             List<double> leafDiffx1 = new List<double>();
             List<double> leafDiffx2 = new List<double>();

             // Add all direct sample comparisons in the window
             for (int ind = muIndexBefore; ind <= muIndex; ind++)
             {
                 double mlcX1ind = mLCAct.ElementAt(leaf + 2 + cpCount).ElementAt(ind) * 10.0;
                 double mlcX2ind = mLCAct.ElementAt(leaf + 2).ElementAt(ind) * 10.0;

                 double x1DiffInd = leafPosX1 + mlcX1ind;
                 double x2DiffInd = leafPosX2 - mlcX2ind;

                 leafDiffx1.Add(x1DiffInd);
                 leafDiffx2.Add(x2DiffInd);
             }

             // Add one interpolated value across the window endpoints
             double muBefore = mus.ElementAt(muIndexBefore);
             double muAfter = mus.ElementAt(muIndex);

             if (muAfter != muBefore)
             {
                 double mlcX1Before = mLCAct.ElementAt(leaf + 2 + cpCount).ElementAt(muIndexBefore) * 10.0;
                 double mlcX2Before = mLCAct.ElementAt(leaf + 2).ElementAt(muIndexBefore) * 10.0;
                 double mlcX1After = mLCAct.ElementAt(leaf + 2 + cpCount).ElementAt(muIndex) * 10.0;
                 double mlcX2After = mLCAct.ElementAt(leaf + 2).ElementAt(muIndex) * 10.0;

                 double mlcX1 = mlcX1Before + (currentMU - muBefore) * ((mlcX1After - mlcX1Before) / (muAfter - muBefore));
                 double mlcX2 = mlcX2Before + (currentMU - muBefore) * ((mlcX2After - mlcX2Before) / (muAfter - muBefore));

                 double x1Diff = leafPosX1 + mlcX1;
                 double x2Diff = leafPosX2 - mlcX2;

                 leafDiffx1.Add(x1Diff);
                 leafDiffx2.Add(x2Diff);
             }

             // Pick the value with the smallest absolute error
             double minDiffX1 = leafDiffx1.OrderBy(x => Math.Abs(x)).First();
             double minDiffX2 = leafDiffx2.OrderBy(x => Math.Abs(x)).First();

             if (Math.Abs(minDiffX1) > maxDiff)
             {
                 maxDiff = Math.Abs(minDiffX1);
                 maxLeaf = $"X1: {leaf + 1}";
             }

             if (Math.Abs(minDiffX2) > maxDiff)
             {
                 maxDiff = Math.Abs(minDiffX2);
                 maxLeaf = $"X2: {leaf + 1}";
             }

             mlc.Add(minDiffX1);
             mlc.Add(minDiffX2);
             squares.Add(minDiffX1 * minDiffX1);
             squares.Add(minDiffX2 * minDiffX2);
         }
     }

     priorMU = currentMU;
     cpIndex++;
 }
 return (mlc, maxLeaf, Math.Sqrt(squares.Average()));

Conclusion

Trajectory log analysis can be a powerful addition to a clinic’s patient-specific QA workflow. By directly comparing the planned treatment geometry with the actual machine delivery recorded in the logs, it becomes possible to detect deviations that might otherwise go unnoticed. However, as this investigation demonstrated, the interpretation of trajectory logs is not always straightforward.

The machine may already begin transitioning to the next control point geometry before the MU of the current control point is reached.

Because trajectory logs are sampled at fixed time intervals, multiple planned control points may fall within the same pair of trajectory log samples. In these situations, the machine may have already begun transitioning to the next control point geometry before the MU associated with the current control point has been fully accumulated. When the comparison algorithm attempts to interpolate leaf positions exactly at the planned MU, the surrounding trajectory samples may already represent the next geometric state. The result is an apparent leaf position error that does not reflect the true delivery accuracy.


By introducing a small MU search window around each control point, the comparison algorithm becomes more robust to these sampling limitations. Instead of relying on a single interpolated value, the method evaluates the delivered geometry across a narrow neighborhood of trajectory samples and selects the position that most closely matches the planned control point geometry. This adjustment preserves the intent of the original comparison while avoiding false discrepancies that arise purely from timing resolution.


In practice, a ±0.1 MU window proved sufficient to resolve the issue observed in this investigation. The updated method correctly identified that the delivered leaf positions passed through the planned control point geometry even though the trajectory log samples immediately surrounding the control point MU suggested otherwise.


This experience highlights an important lesson when working with high-frequency machine logs: the temporal resolution of recorded data and the physical behavior of the control system must both be considered when designing analysis algorithms. What initially appears to be a delivery error may instead be a limitation of how the data is sampled and interpreted.


By refining the comparison method to account for these effects, trajectory log analysis can remain a reliable and informative tool for verifying treatment delivery as part of routine PSQA.

Comments


©2035 by NWS. Powered and secured by Wix

bottom of page