TrajectoryLog.NET Part II: Visualizing Fluences with OxyPlot
- mschmidt33
- May 2, 2024
- 6 min read
In a previous post, TrajectoryLog.NET, an open-source API that allowed for the interpretation of TrueBeam and Halcyon Trajectory Logs, was introduced. Within the library, several public methods are dedicated to facilitating the analysis of Trajectory Log files. In this post, the BuildFluence() method will be introduced, providing examples of visualizing fluence and conducting fluence analysis. This method provides a convenient tool in visualizing the comparisons between expected and delivered MLC leaf trajectories.
Setting up the Application
Begin by cloning the TrajectoryLog.NET repository to your workstation using visual Studio. Open Visual Studio and select Clone a Repository. Paste in the URL of the repository to be cloned and provide a location for the repository. Click Clone in Visual Studio.

Once the repository has been cloned, ensure that the Target Framework matches the framework that will be used for the application we will build going forward. This will be limited to the version of ESAPI if this application is intended to be integrated with Eclipse. One of the principle advantages to the TrajectoryLog.NET API is that it can be integrated into current ESAPI applications. To check the target framework, click Project in the Visual Studio Menu and select TrajectoryLog.NET properties. In the Application properties, the Target Framework is available. Lastly, if the application is intended to be integrated with ESAPI, please check the Platform Target for consistency with the ESAPI libraries. This Platform Target should be x64. Select the Build menu in Visual Studio, and then select Build Solution to compile the code.
One of the principle advantages to the TrajectoryLog.NET API is that it can be integrated into current ESAPI applications.
Open a new instance of Visual Studio. This time, select the option Create a new project. A good project type that allows for image visualization is the WPF App (.NET Framework). Select this option and click Next. In this example, the project will be named FluenceCheck. Ensure that the Framework matches the Target Framework verified in the paragraph above. Click Create to begin coding the project.

With Visual Studio open, right mouse click on the References and select the option Add Reference. Click Browse and navigate to the recently compiled TrajectoryLog.NET.dll file.

Building the Initial UI
In this new application, OxyPlot will be used to visualize the fluences. Right mouse click on the project in the Solution Explorer and select the menu item ManageNugetPackages. Click on the Browse tab and search oxyplot and install the Nuget package OxyPlot.Wpf. Additionally, install Prism.Core to assist in wiring up the Delegates for button clicks. In this small example application, we can use the MainWindow.xaml provided by the WPF template in Visual Studio. To use OxyPlot, add the oxyplot namespace into the Window tag.
xmlns:oxy="http://oxyplot.org/wpf"

In this example, a Dockpanel control will place the button to search for Trajectory Log files and an internal grid that will holds three plots. Once completed, the code inside the Window element should look as follows:
<Grid>
<DockPanel>
<Button Margin="10" DockPanel.Dock="Top" Command="{Binding OpenFileCommand}"
Content="Select Trajectory Log"/>
<Grid DockPanel.Dock="Bottom">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<oxy:PlotView Model="{Binding ExpectedFluencePlotModel}"/>
<oxy:PlotView Model="{Binding ActualFluencePlotModel}" Grid.Column="1"/>
<oxy:PlotView Model="{Binding GammaPlotModel}" Grid.Column="2"/>
</Grid>
</DockPanel>
</Grid>
Finally, open the MainWindow.xaml.cs file and set the Datacontext to a new MainViewModel class which can be constructed with the Visual Studio Quick Actions.

Within the MainViewModel create properties for the plot models and instantiate the button command.
public class MainViewModel
{
public PlotModel ExpectedFluencePlotModel { get; set; }
public PlotModel ActualFluencePlotModel { get; set; }
public PlotModel GammaPlotModel { get; set; }
public DelegateCommand OpenFileCommand { get; set; }
public string FileName {get; set;}
private double[,] _actualFluence { get; set; }
private double[,] _expectedFluence { get; set; }
public MainViewModel()
{
SetInitialParameters();
}
private void SetInitialParameters()
{
ExpectedFluencePlotModel = new PlotModel() { Title = "Expected Fluence" };
ActualFluencePlotModel = new PlotModel() { Title = "Actual Fluence" };
GammaPlotModel = new PlotModel() { Title = "Gamma Analysis [3%/3mm]" };
OpenFileCommand = new DelegateCommand(OnOpenFile);
}
private void OnOpenFile()
{
throw new NotImplementedException();
}
}
Remove the throw new NotImplementedException(); and replace it with an OpenFileDialog (from Microsoft.Win32 namespace) that searches for a binary file and calls another method GetFluenceImages().
private void OnOpenFile()
{
OpenFileDialog ofd = new OpenFileDialog();
ofd.Filter = "Trajectory Log Files (*.bin)|*.bin";
ofd.Title = "Open Trajectory Log File";
if (ofd.ShowDialog() == true)
{
FileName = ofd.FileName;
GetFluenceImages();
}
}
private void GetFluenceImages()
{
throw new NotImplementedException();
}
Remove the throw new NotImplementedException(); and replace it with code that will read the trajectory logs from the given filename. This is the first use of the TrajectoryAPI class which requires the use of a using statement for TrajectoryLog.NET. From the API, the LoadLog() method is used to load the trajectory log file, and then the BuildFluence() method is used for both the expected fluence calculation and the actual fluence calculation.
private void GetFluenceImages()
{
var log = TrajectoryAPI.LoadLog(FileName);
_expectedFluence = TrajectoryAPI.BuildFluence(log, "Expected");
ExpectedFluencePlotModel.Axes.Add(new LinearColorAxis
{
Palette = OxyPalettes.Plasma(100),
IsAxisVisible = true
});
HeatMapSeries expectedSeries = new HeatMapSeries()
{
X0 = 0,
X1 = _expectedFluence.GetLength(1),
Y0 = 0,
Y1 = _expectedFluence.GetLength(0),
RenderMethod = HeatMapRenderMethod.Bitmap,
Data = _expectedFluence
};
ExpectedFluencePlotModel.Series.Add(expectedSeries);
ExpectedFluencePlotModel.InvalidatePlot(true);
_actualFluence = TrajectoryAPI.BuildFluence(log, "Actual");
ActualFluencePlotModel.Axes.Add(new LinearColorAxis
{
Palette = OxyPalettes.Plasma(100),
IsAxisVisible = true
});
HeatMapSeries actualSeries = new HeatMapSeries()
{
X0 = 0,
X1 = _actualFluence.GetLength(1),
Y0 = 0,
Y1 = _actualFluence.GetLength(0),
RenderMethod = HeatMapRenderMethod.Bitmap,
Data = _actualFluence
};
ActualFluencePlotModel.Series.Add(actualSeries);
ActualFluencePlotModel.InvalidatePlot(true);
}
Testing the Application
At this point, the application can be tested. Run the application in Visual Studio, and open a sample trajectory file. Note the fluence visualization for the Expected and Actual Fluence.

Integrating Gamma Analysis
In the following sample code, a Gamma Analysis calculation will be performed on the fluence values that are above a 5% threshold, using the gamma comparison criteria of 3% of the overall maximum dose and 3mm distance-to-agreement. For this calculation, it is important to note that the BuildFluence() method in the Trajectory.NET API has a resolution of 1mm. In the OnOpenFile() method of our MainViewModel, another method is created beneath the GetFluenceImages() method. This method will be called CalculateGamma(). Allow visual studio to create such a method.
Due to the course resolution of the fluence images, if a more rigorous gamma analysis criteria is desired, please consider interpolating the comparison image to a resolution 0.1mm or higher using a bilinear interpolation that can be passed into the inner search loop within the CalculateGamma() method.
Within this method, we will first set some parameters for the gamma calculation, such as the maximum value of the fluence (this will be used in the dose difference tolerance as well as the threshold), the distance to agreement, the size of each image, and the threshold. Next, two for loops will iterate through the reference image (the expected fluence in this case). To save time, instead of iterating through the entire comparison image, we will iterate through 10x the distance-to-agreement distance (or about +/- 30 pixels in this instance). Looping through the comparison image, the gamma calculation will be performed using the following equation:

After the comparison image has been searched, the minimum gamma value is stored to the gamma image. As the gamma image is stored in the matrix, any failing gammas are multiplied by a factor of 100 in order to scale the image to show the failing gammas in the highest color level. This technique is described in detail in a prior blog post about Portal Dosimetry Scripting with PDSAPI. Finally, the Gamma image is plotted, and the overall gamma value is shared with the user.
private void CalculateGamma()
{
double maxFluence = _expectedFluence.Cast<double>().Max();
int sizeX = _expectedFluence.GetLength(0);
int sizeY = _expectedFluence.GetLength(1);
int compareX = _actualFluence.GetLength(0);
int compareY = _actualFluence.GetLength(1);
List<double> gammaPoints = new List<double>();
//3% dose difference
double ddTol = maxFluence * 0.03;
//3mm difference-to-agreement
double dta = 3.0;
//5% threshold
double threshold = 0.05;
double[,] gammaImage = new double[sizeX, sizeY];
for (int i = 0; i < sizeX; i++)
{
for (int ii = 0; ii < sizeY; ii++)
{
if (_expectedFluence[i, ii] > maxFluence * threshold)
{
double pval = _expectedFluence[i, ii];
//loop through from 10*dta and calculate gamma
int rowStart = 0 > i - (10 * dta)
? 0 : i - 10 * (int)dta;
int rowEnd = compareX < i + (10 * dta)
? compareX : i + (10 * (int)dta);
int colStart = 0 > ii - (10 * dta)
? 0 : ii - (10 * (int)dta);
int colEnd = compareY < ii + (10 * dta)
? compareY : ii + (10 * (int)dta);
List<double> localGamma = new List<double>();
for (int j = rowStart; j < rowEnd - 1; j++)
{
for (int jj = colStart; jj < colEnd - 1; jj++)
{
double distPart = Math.Sqrt((ii - jj) * (ii - jj) + (i - j) * (i - j)) / (dta);
double tval = _actualFluence[j, jj];
double dosePart = (pval - tval) / ddTol;
localGamma.Add(Math.Sqrt(dosePart * dosePart + distPart * distPart));
}
}
var gammaMin = localGamma.Min();
gammaPoints.Add(gammaMin);
//this line exemplifies failing gammas.
gammaImage[i, ii] = gammaMin <= 1 ? gammaMin : gammaMin * 100.0;
}
}
}
var gammaValue = (double)gammaPoints.Count(gp => gp < 1.0) / (double)gammaPoints.Count() * 100.0;
GammaPlotModel.Axes.Add(new LinearColorAxis
{
Palette = OxyPalettes.Plasma(100),
IsAxisVisible = true
});
HeatMapSeries gammaSeries = new HeatMapSeries()
{
X0 = 0,
X1 = gammaImage.GetLength(1),
Y0 = 0,
Y1 = gammaImage.GetLength(0),
RenderMethod = HeatMapRenderMethod.Bitmap,
Data = gammaImage
};
GammaPlotModel.Series.Add(gammaSeries);
GammaPlotModel.InvalidatePlot(true);
MessageBox.Show($"Gamma Value: {gammaValue:F2}");
}
To test this application, run the program in Visual Studio once again and select the same trajectory log file as before. Depending on the Trajectory file, the Gamma Image may contain less detail compared to the fluence images.

Implementation Notes
A 3%/3mm gamma value between expected and actual fluence will likely give a value of 100.0% for nearly every field. It may be desired to compare these fluence images with a stricter criterion (i.e. 2%/2mm or even 1%/1mm). Due to the course resolution of the fluence images, if a more rigorous gamma analysis criteria is desired, please consider interpolating the comparison image to a resolution 0.1mm or higher using a bilinear interpolation that can be passed into the inner search loop within the CalculateGamma() method.
Comments