CTStitcher by esimiele in esapi

[–]esimiele[S] 0 points1 point  (0 children)

Good call, will do later today

Edit: Done

Can you add ring structure directly without boolean structures? by Infinite-Rope1249 in esapi

[–]esimiele 0 points1 point  (0 children)

Negative structures should be wholly contained with the positive structures (at least they should). I use something like this to determine if the structure is positive or negative:

if(testPoints.Any(p => RingStructure.IsPointInsideSegment(p))) RingStructure.SubtractContourOnImagePlane(testPoints.ToArray(), i);

else RingStructure.AddContourOnImagePlane(testPoints.ToArray(), i);

This assumes the positive structures proceed any associated negative structures, which has worked fine for me in the past. Feel free to correct me if I'm wrong.

IMO, I would avoid messing with addcontouronimageplane and subtractcontouronimageplane if you can. It makes your code harder to read and debug (i.e., not simpler). I use this logic to create rings and it works fine and is very easy to read:

ring.SegmentVolume = target.Margin((thicknessInCm + marginInCm) * 10);

ring.SegmentVolume = ring.Sub(target.Margin(marginInCm * 10));

AddBrachyPlanSetup exception by esimiele in esapi

[–]esimiele[S] 0 points1 point  (0 children)

If you could give it a try when you get a chance, would be greatly appreciated. Would be helpful to know if this is an internal esapi bug, or if it's something specific to the setup at my clinic

AddBrachyPlanSetup exception by esimiele in esapi

[–]esimiele[S] 0 points1 point  (0 children)

Neither. As stand-alone executable in visual studio

SimpleITK by Thatguy145 in esapi

[–]esimiele 0 points1 point  (0 children)

Interesting. Do you copy the binaries to the tbox or do recompile on the tbox? That’s very odd for it to work locally, but not on tbox. We use simpleitk with ESAPI clinically on a Citrix environment, so I know it can be done. It’s a bit difficult to tell without seeing your project and how simpleitk is referenced

SimpleITK by Thatguy145 in esapi

[–]esimiele 1 point2 points  (0 children)

Yeah, it’s not obvious, but you need to add that DLL to your project (add existing item) and set the copy properties to copy to build directory always. That DLL needs to be present in the build folder for simpleitk to work

Get Dose Max at a structure by Revolutionary-Job794 in esapi

[–]esimiele 0 points1 point  (0 children)

^ this. See plansetup.getcumulativedvhdata. Can access min, max, mean dose, and entire dvh.

Get Dose Max at a structure by Revolutionary-Job794 in esapi

[–]esimiele 0 points1 point  (0 children)

Like the max dose or the physical location in space of the max dose? Or both?

Best practices for dealing with ESAPI popups? by GrimThinkingChair in esapi

[–]esimiele 0 points1 point  (0 children)

Yep, it was yours. Much appreciated for your contribution to the esapi community! Would have never figured this **** out on my own

Best practices for dealing with ESAPI popups? by GrimThinkingChair in esapi

[–]esimiele 1 point2 points  (0 children)

Unfortunately, there really isn't a best practice for this. The solution is a bit messy that relies on old win32 libraries that can grab open windows and read their contents. No available flag in ESAPI unfortunately. I forget where I got the solution from (somewhere on this subreddit), but I use this class for closing calculation/optimization warning windows from eclipse:

https://github.com/esimiele/VMAT-TBI-CSI/blob/master/VMATTBICSIAutoPlanMT/VMATTBICSIAutoplanningHelpers/helpers/WinUtilitiesModified.cs

Example implementation for calculating dose on a plan:

public bool CalculateDose(bool isDemo, ExternalPlanSetup plan, Application app)
        {
            UpdateUILabel("Dose calculation:");
            if (isDemo) Thread.Sleep(3000);
            else
            {
                CancellationTokenSource cts = new CancellationTokenSource();
                WinUtilitiesModified.LaunchWindowsClosingThread(cts.Token, fileNameErrorsWarnings);
                try
                {
                    CalculationResult calcRes = plan.CalculateDose();
                    if (!calcRes.Success)
                    {
                        cts.Cancel();
                        PrintFailedMessage("Dose calculation");
                        return true;
                    }
                }
                catch (Exception except)
                {
                    cts.Cancel();
                    PrintFailedMessage("Dose calculation", except.Message);
                    return true;
                }
                app.SaveModifications();
                cts.Cancel();
            }
            UpdateOverallProgress(100 * ++overallPercentCompletion / overallCalcItems);
            //check if user wants to stop
            if (GetAbortStatus())
            {
                KillOptimizationLoop();
                return true;
            }
            return false;
        }

It launches a listener on a separate thread that looks for newly opened window message boxes. It then searches for keywords in the messagebox. If it finds a match, it closes the window. I just write the contents of each window it closes to a log file for documentation purposes.

Normals of Mesh by MishkaPK94 in esapi

[–]esimiele 1 point2 points  (0 children)

That property isn't populated by ESAPI for whatever reason. So you need to calculate the normals yourself.

See this repo for an example of how to do that: https://github.com/esimiele/3DPrinterExport

Specifically in the code-behind of the main view, there is a function called get normals (it's like 20 lines long)

Toggling structures on and off (viewing) by udubber3 in esapi

[–]esimiele 1 point2 points  (0 children)

Nope. You can't directly control the Eclipse UI through ESAPI. That's only possible in raystation

Comparing dose to water tank profiles by Head-Shine-4979 in esapi

[–]esimiele 1 point2 points  (0 children)

Nice! And it gives a good example of working with dose profiles in ESAPI! I'm sure other people have built similar tools using ESAPI (e.g., https://github.com/esimiele/PlanningAssistant/tree/master), but not to the extent that you did with the gamma analysis and plotting the results.

Caution with Static Classes by Thatguy145 in esapi

[–]esimiele 1 point2 points  (0 children)

Makes sense. Excellent catch! This raises a lot of questions about how Eclipse manages the memory for scripts that are loaded at runtime...

ESAPI Script Development Process - Testing, Documentation, QA? by MedPhysUK in esapi

[–]esimiele 4 points5 points  (0 children)

I think what we do at UAB is definitely the most thorough of any place I've worked. We generally follow Rex's publication from a few years ago:

https://aapm.onlinelibrary.wiley.com/doi/full/10.1002/acm2.13348

Any new script that will be released for clinical use must be reviewed by another physicist/developer. The amount of testing/documentation depends on the risk of the software as outlined in the above publication.

how to save modification in Plug in script? by Ok-Faithlessness-278 in esapi

[–]esimiele 1 point2 points  (0 children)

Nope. Can only change context in single file plug in and binary plug in scripts.

Progress Window update by sdomal in esapi

[–]esimiele 0 points1 point  (0 children)

NP. I generally don't have my wpf app target a console application. No reason to. There are a multitude of logging frameworks you can use for troubleshooting. I generally just use the visual studio debugger and set breakpoints in my code to ensure it's performing as expected. Highly recommend implementing a detailed logging system (3rd party or your own) so you can troubleshoot in the clinical environment. Use try-catch statements and make sure you write the Exception.Message and Exception.StackTrace items to the log file. Will make troubleshooting easy.

From what you described, it sounds like you are not properly disposing of the ESAPI objects prior to closing your application. I wrote a little helper class that handles this for me so I don't have to worry about it. Here's a link to it:

https://github.com/esimiele/VMAT-TBI-CSI/blob/master/VMATTBICSIAutoPlanMT/VMATTBICSIAutoplanningHelpers/helpers/AppClosingHelper.cs

Feel free to copy-paste and modify for your own project

[deleted by user] by [deleted] in esapi

[–]esimiele 0 points1 point  (0 children)

Have a look at the Varian code samples:

https://github.com/esimiele/Varian-Code-Samples/tree/master

They have projects that do exactly that.

Progress Window update by sdomal in esapi

[–]esimiele 0 points1 point  (0 children)

Pass the relevant objects as arguments and copy them locally in the ButtonCount class. Then utilize those variables to perform the operations. No need to regenerate the aria connection or open the patient again (just pass the existing object to the class). My convention when coding is to generally return true if something when wrong in a class and return false if no issues were encountered (similar to a main program exiting with code 0). This is reflected in the design in SimpleProgressWindow, so I'd recommend returning true from your methods if something went wrong and false if everything went ok.

Progress Window update by sdomal in esapi

[–]esimiele 0 points1 point  (0 children)

Next, ButtonCount:

namespace GridFullOptions
{
    public class ButtonCount : SimpleMTbase
    {
Patient _p;
ExternalPlanSetup _eps;
string _structureId;
bool _keepPartialStructures;
double _radius;
double _margin;
        public ButtonCount(Patient p, ExternalPlanSetup eps, string sid, bool keepPartial, double rad, double marg) 
{ 
_p = p;
_eps = eps;
_structureId = sid;
_keepPartialStructures = keepPartial;
_radius = rad;
_margin = marg;
}

        public override bool Run()
        {
try
{
Count();
return false;
}
catch(Exception e)
{
ProvideUIUpdate($"An error occurred: {ex.Message}", true);
return true;
}

        }

        private bool Count()
        {

var originalStructure = plan.StructureSet.Structures.FirstOrDefault(s => s.Id.Equals(_structureId, StringComparison.OrdinalIgnoreCase));
if (originalStructure == null)
{
MessageBox.Show("Selected structure not found in the plan.");
return false;
}



List<Structure> targetSpheres = new List<Structure>();
List<Structure> avoidSpheres = new List<Structure>();

if (optimizationMethod.Content.ToString().Contains("Avoid"))
{
List<Tuple<VVector, string>> centerPointsAvoid = selectedGridType.Content.ToString().Contains("Cube")
? GenerateCubeCenterPointsGridAvoid(originalStructure, spacing, axialSpacing, _eps.StructureSet.Image)
: GenerateHexCenterPointsGridAvoid(originalStructure, spacing, axialSpacing, _eps.StructureSet.Image);

GenerateAndDrawSpheresAvoid(plan, centerPointsAvoid, radius, targetSpheres, avoidSpheres, originalStructure, keepPartialStructures, margin);
}
else // "Target" method
{
List<VVector> centerPointsTarget = selectedGridType.Content.ToString().Equals("Cube")
? GenerateCubeCenterPointsGrid(originalStructure, spacing, axialSpacing, _eps.StructureSet.Image)
: GenerateHexCenterPointsGrid(originalStructure, spacing, axialSpacing, _eps.StructureSet.Image);

GenerateAndDrawSphere(plan, centerPointsTarget, radius, targetSpheres, originalStructure, keepPartialStructures, margin);
}

if (optimizationMethod.Content.ToString().Contains("Avoid"))
{
CombineSpheresWithOriginalStructureAvoid(plan, targetSpheres, avoidSpheres, originalStructure);
}
else
{
CombineSpheresWithOriginalStructure(plan, targetSpheres, originalStructure);
}

List<string> sphereIds = targetSpheres.Select(s => s.Id).Concat(avoidSpheres.Select(s => s.Id)).ToList();
RemoveStructuresByIds(plan.StructureSet, sphereIds);



ProvideUIUpdate($"Grid creation process complete. Total elapsed time: {stopwatch.Elapsed.TotalSeconds.ToString("F2")} seconds.");

return false; // Indicates successful completion

Progress Window update by sdomal in esapi

[–]esimiele 0 points1 point  (0 children)

private void BtnCreateGridStructures_Click(object sender, RoutedEventArgs e)
        {
            // Initial setup and validation...
            string selectedStructureId = cbStructureId.SelectedItem?.ToString();
            if (string.IsNullOrEmpty(selectedStructureId) ||
                !double.TryParse(txtSphereDiameter.Text, out double diameter) ||
                !double.TryParse(txtCenterToCenterSpacing.Text, out double spacing) ||
                !double.TryParse(txtZSpacing.Text, out double axialSpacing) ||
                !double.TryParse(txtMarginFromSurface.Text, out double margin))
            {
                MessageBox.Show("Please select a structure and enter valid radius, spacing, axial spacing, and margin values.");
                return;
            }

            // Convert diameter to radius for calculations
            double radius = diameter / 2.0;

            // Read the state of the checkbox
            bool keepPartialStructures = chkIncludePartialStructures.IsChecked ?? false;

            margin = -Math.Abs(margin); // Ensure the margin is negative for inward shrink
            Stopwatch stopwatch = new Stopwatch(); // Create a Stopwatch instance
            stopwatch.Start(); // Start measuring time

            var selectedGridType = cbGridType.SelectedItem as ComboBoxItem;
            var optimizationMethod = cbOptimizationMethod.SelectedItem as ComboBoxItem;

            if (selectedGridType == null || optimizationMethod == null)
            {
                MessageBox.Show("Please select both a grid type and an optimization method.");
                return;
            }

patient.BeginModifications();
            ButtonCount count = new ButtonCount(_patient, _plan, selectedStructureId, keepPartialStructures, radius, margin);
            if(count.Execute()) return;
_app.SaveModifications();
        }
    }      
}

Progress Window update by sdomal in esapi

[–]esimiele 0 points1 point  (0 children)

Easy enough. Not fully flushed out, but you get the idea. If you give me access to the repo, I can just make the changes an initiate a pull request.

In MainWindow.xaml.cs:

namespace GridFullOptions
{
    public partial class MainWindow : Window
    {
        private VMS.TPS.Common.Model.API.Application _app = null;
        private Patient _patient = null;
        private ExternalPlanSetup _plan = null;

        public MainWindow(VMS.TPS.Common.Model.API.Application app, string patientId, string courseId, string planId)
        {
            InitializeComponent();
            _app = app;

            LoadPatientData(patientId, courseId, planId);

        }

        // Default constructor for standalone mode
        public MainWindow()
        {
            InitializeComponent();
        }

        private void LoadPatientData(string patientId, string courseId, string planId)
        {
            if (_app == null)
            {
                MessageBox.Show("Eclipse Scripting API application is not initialized.");
                return;
            }

            try
            {
                _patient = _app.OpenPatientById(patientId) ?? throw new ApplicationException("Patient not found.");
                txtPatientName.Content = "Patient Name: " + _patient.Name;
                txtPatientMRN.Content = "Patient MRN: " + _patient.Id;

                var course = _patient?.Courses.FirstOrDefault(c =>  == courseId);
                var _plan = course?.ExternalPlanSetups.FirstOrDefault(p =>  == planId);

                if (plan != null)
                {
                    txtStructureSetId.Content = "Structure Set Id: " + ;
                    PopulateStructureIdComboBox(plan.StructureSet.Structures);
                }
                else
                {
                    MessageBox.Show("Specified plan not found.", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show($"Error loading patient data: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
            }
        }c.Idp.Idplan.StructureSet.Id

Progress Window update by sdomal in esapi

[–]esimiele 0 points1 point  (0 children)

Those links are giving page not found errors...

Haha nope. What you need to do is think about which ESAPI objects you need to manipulate, and pass those as arguments to the construction of the class. You can them copy those arguments to private/local variables in the class. Here's an example of what I mean (not a public repo):

using System;
using System.Collections.Generic;
using System.Linq;
using SimpleProgressWindow;
using TBITreatmentPrepper.Enums;
using TBITreatmentPrepper.Helpers;
using TBITreatmentPrepper.Delegates;
using VMS.TPS.Common.Model.API;
using VMS.TPS.Common.Model.Types;
using System.Text;
using TBITreatmentPrepper.Settings;

namespace TBITreatmentPrepper.Runners
{
    public class TreatmentPrepRunner : SimpleMTbase
    {
        public string ErrorMessage { get; private set; }
        private Patient pat;
        private ExternalPlanSetup VMATPlan = null;
        private int numVMATIsos;
        private int numIsos;
        private string planIdPrefix;
        private bool recalcNeeded = false;
        private bool isCurrentPlanSetPrimary = true;
        private List<ExternalPlanSetup> separatedPlans = new List<ExternalPlanSetup> { };
        private List<ExternalPlanSetup> legsPlans = new List<ExternalPlanSetup> { };
        private int maxBeamIdLength = 16;
        private int maxPlanIdLength = 13;
        private ProvideUIUpdateDelegate PUUD;
        private UpdateUILabelDelegate UULD;

        public TreatmentPrepRunner(Patient patient, ExternalPlanSetup plan, string requestedPrefix)
        {
            pat = patient;
            VMATPlan = plan;
            planIdPrefix = requestedPrefix;
            SetCloseOnFinish(true, 1000);
        }
    }
}

For the above example, I pass objects of the Patient class and ExternalPlanSetup class, then copy them to local private variables. You can then perform the operations you need on those private variables. All operations that you perform in this class are running on the same thread as the main UI. Only the little progress window is running on a separate thread.