Plan quality metrics scripts related question

Oct 11, 2016 at 10:10 PM
Can anyone show me how to inquire the volume of structures in UserDefinedMetrics.cs?
RTOG 0813 needs the volume ratio of 50% Rx to target volume. I am thinking of calculating that in PQMs.cs but not sure how to access such information. Thanks!
Oct 13, 2016 at 6:27 AM
Edited Oct 13, 2016 at 6:25 PM
Hi yixaingl,

This is a little tricky to get in plan quality metrics. The PlanQualityMetric interface signature on the addPWMInfo method doesn't accept multiple structures:
public interface PlanQualityMetric 
    {
      void addPQMInfo(PlanningItem plan, Structure organ, XmlWriter writer);
    }
When calculating the 50% Rx / Volume PTV as defined in RTOG 0813 you need to get the PTV Volume but the 50% needs to be looked up from the DVH using the Body or some other structure of your choosing (50% contained in a specific region defined by that structure). The addPWMInfo method signature doesn't allow two structures to be passed in.

A work around would be to define another method that does accept two structures and add that to a new PlanQualityMetric struct
struct ConformityIndex : PlanQualityMetric
    {
        public double CiConstraint;
        public double upperLimit;
        public VolumePresentation vp;
        public DoseValue dv;
        public string name;
        PQMUtilities.LimitType lt;

        public ConformityIndex(string name_, DoseValue doseValue, double ciConstraint, 
            double upperLimitTol,
            VolumePresentation vpp = VolumePresentation.AbsoluteCm3,
            PQMUtilities.LimitType lt_ = PQMUtilities.LimitType.upper)
        {
            CiConstraint = ciConstraint;
            upperLimit = upperLimitTol;
            vp = vpp;
            dv = doseValue;
            name = name_;
            lt = lt_;
        }

        public void addPQMInfoCi(PlanningItem plan, Structure organ, Structure body, 
                                                    XmlWriter writer)
        {
            //
            // implement writing
            //

            double volume = plan.GetVolumeAtDose( body, dv, vp );
            double organVolume = organ.Volume;
            double ci = volume/organVolume;

        }

        public void addPQMInfo(PlanningItem plan, Structure organ, XmlWriter writer)
        {
            throw new System.NotImplementedException();
        }
    }
On your UserDefiniedMetrics you would add this to your Target or implement a new one for the new ConformityIndex PQM.
public class Target
    {
      public static PlanQualityMetric[] getPQMs(DoseValue totalPrescribedDose)
      {
        PlanQualityMetric[] PQMs = 
          {
            new VolumeAtDose("V[95%(Rx) > 95%]", new DoseValue(totalPrescribedDose.Dose*.95,  
                    totalPrescribedDose.Unit),   95.0, 1.0, 
                    VolumePresentation.Relative, PQMUtilities.LimitType.lower),
            new ConformityIndex("V[50%(Rx) < 5]", new DoseValue(totalPrescribedDose.Dose*.5, 
                    totalPrescribedDose.Unit), 5.0, 1.0, VolumePresentation.AbsoluteCm3, 
                    PQMUtilities.LimitType.lower )
          };
        return PQMs;
      }
    }
In the PQMReporter.cs when looping through all your pqmStats you'd have to implement a special case to determine if it is a ConformityIndex and pass the body in.
foreach (PlanQualityMetric pqm in pqmStats)
        {
            if ( pqm is ConformityIndex)
            {
                var body = ss.Structures.FirstOrDefault(s=>s.DicomType.Equals("EXTERNAL"));
                ((ConformityIndex)pqm).addPQMInfoCi(plan, oar, body, writer);
            }
            else
            {
                pqm.addPQMInfo( plan, oar, writer );
            }
            
        }
Other issue to note is in RTOG 0813 a Table of values is defined that varies with PTV volume. You'd have to make ConformityIndex accept a range of acceptable values that varies with volume instead of a single ciConstraint value. The target volume would have to be looked up and table values interpolated. Also, if you want the 50% in a specific region you'd have to create a custom structure to define that region and use that structure to lookup the 50% volume. This is more important in multi-lesion VMAT SRS cases to determine each lesion's CI. When we were developing ClearCheck we thought it would be easiest to allow users to add or delete values from a table for the acceptable limits just like the table in RTOG 0813 and automatically do the interpolation for them.

-Kurt
www.radformation.com
Oct 13, 2016 at 7:07 PM
Thank you Kurt for the detailed explanation! It seems harder than I originally thought. You are absolutely right about the range of acceptable values. Need to look into that too. :-)
Oct 14, 2016 at 5:45 AM
Edited Oct 14, 2016 at 5:47 AM
Yeah it's not as straight forward as one would like given the way things are. For the range of acceptable values you could define a struct or class to hold the different conformity constraints you would want in your table of CI constraints
public class ConformityValue
      {
          public double Volume { get; set; }
          public double RatioIdeal { get; set; }
          public double RatioLimit { get; set; }
      }
On the Target build the table of constraints that vary with volume size
public class Target
    {
      public static PlanQualityMetric[] getPQMs(DoseValue totalPrescribedDose)
      {
          var conformityValues = new ConformityValue[]
          {
            new ConformityValue(){Volume = 1.8, RatioIdeal = 5.9, RatioLimit = 7.5},
            new ConformityValue(){Volume = 3.8, RatioIdeal = 5.5, RatioLimit = 6.5},
            new ConformityValue(){Volume = 7.4, RatioIdeal = 5.1, RatioLimit = 6},
            new ConformityValue(){Volume = 13.2, RatioIdeal = 4.7, RatioLimit = 5.8}
            // etc....
          };
          PlanQualityMetric[] PQMs = 
          {
            new VolumeAtDose("V[95%(Rx) > 95%]", new DoseValue(totalPrescribedDose.Dose*.95, 
                                           totalPrescribedDose.Unit),   95.0, 1.0, 
                                          VolumePresentation.Relative, PQMUtilities.LimitType.lower),
            new ConformityIndex("V[50%(Rx) < 5]", new DoseValue(totalPrescribedDose.Dose*.5, 
                                          totalPrescribedDose.Unit), conformityValues, 1.0, 
                                          VolumePresentation.AbsoluteCm3, PQMUtilities.LimitType.lower )
          };
        return PQMs;
      }
    }
On the PWM update it to accept an array and have it do the volume search and interpolation
  struct ConformityIndex : PlanQualityMetric
    {
        public ConformityValue[] CiConstraints;
        public double upperLimit;
        public VolumePresentation vp;
        public DoseValue dv;
        public string name;
        PQMUtilities.LimitType lt;

        public ConformityIndex(string name_, DoseValue doseValue, ConformityValue[] ciConstraints, double upperLimitTol,
            VolumePresentation vpp = VolumePresentation.AbsoluteCm3,
            PQMUtilities.LimitType lt_ = PQMUtilities.LimitType.upper)
        {
            CiConstraints = ciConstraints;
            upperLimit = upperLimitTol;
            vp = vpp;
            dv = doseValue;
            name = name_;
            lt = lt_;
        }

        public void addPQMInfoCi(PlanningItem plan, Structure organ, Structure body, XmlWriter writer)
        {
            //
            // implement writing
            //

            double volume = plan.GetVolumeAtDose( body, dv, vp );
            double organVolume = organ.Volume;
            double ci = volume/organVolume;

            // Search for volume in table
            foreach (var conformityValue in CiConstraints)
            {
                if (organVolume < conformityValue.Volume)
                {
                    // interpolate to find ideal and limit constraint
                }
            }
        }

        public void addPQMInfo(PlanningItem plan, Structure organ, XmlWriter writer)
        {
            throw new System.NotImplementedException();
        }
    }
Ultimately it would be nice to build a UI where users can add/edit all these constraints and make them patient specific.

Image


-Kurt
www.radformation.com
Oct 14, 2016 at 3:53 PM
Thanks so much, Kurt! It was great!
Oct 14, 2016 at 4:12 PM
Edited Oct 14, 2016 at 4:13 PM
I'm glad I could help.
Oct 18, 2016 at 4:28 PM
Hi Kurt,

I have tried to add the writing to pqm based your suggestion but ran into the problem of displaying the calculated CI. The resulting table would have a row looking like this:
V[50%(Rx)/VPTV < 5] VolumeRatio 2500.0 cGy NaN cc 5.0 cc 6.0 cc PASS
It seems that the CI wasn't calculated. The code calculating ConformityIndex is shown below:
        public ConformityIndex(string name_, DoseValue doseValue, double ciConstraint, 
            double upperLimitTol,
            VolumePresentation vpp = VolumePresentation.AbsoluteCm3,
            PQMUtilities.LimitType lt_ = PQMUtilities.LimitType.upper)
        {
            CiConstraint = ciConstraint;
            upperLimit = upperLimitTol;
            vp = vpp;
            dv = doseValue;
            name = name_;
            lt = lt_;
        }

        public void addPQMInfoCi(PlanningItem plan, Structure organ, Structure body, 
                                                    XmlWriter writer)
        {
            //
            // implement writing
            //
            double volume = plan.GetVolumeAtDose( body, dv, vp );
            double organVolume = organ.Volume;
            double ci = volume/organVolume;
            
            writer.WriteStartElement("PQM");
            writer.WriteAttributeString("type", "VolumeRatio");
            writer.WriteAttributeString("name", name);

            writer.WriteStartElement("DoseValue");
            writer.WriteAttributeString("units", dv.UnitAsString);
            writer.WriteString(dv.ValueAsString);
            writer.WriteEndElement(); // </DoseValue>

            writer.WriteStartElement("Volume");
            writer.WriteAttributeString("units", "");
            writer.WriteAttributeString("calculated", true.ToString());
            writer.WriteString(ci.ToString("0.0"));
            writer.WriteEndElement(); // </Volume>

            string limType = lt.ToString(), tolType;
            if (lt == PQMUtilities.LimitType.upper)
            {
                tolType = "leq";
            }
            else
            {
                tolType = "geq";
            }
            writer.WriteStartElement("Evaluate");

            writer.WriteStartElement("Limit");
            writer.WriteAttributeString("type", limType);
            writer.WriteString(CiConstraint.ToString("0.0"));
            writer.WriteEndElement(); // </Limit>

            writer.WriteStartElement("Tolerance");
            writer.WriteAttributeString("type", tolType);
            writer.WriteString(upperLimit.ToString("0.0"));
            writer.WriteEndElement(); // </Tolerance>

            writer.WriteStartElement("Result");
            double limitMax = (CiConstraint * upperLimit);
            writer.WriteElementString("MaxLimit", limitMax.ToString("0.0"));
            string pfw = "PASS";
            if (lt == PQMUtilities.LimitType.upper)
            {
                if (ci > CiConstraint && ci <= limitMax)
                    pfw = "WARN";
                else if (ci > limitMax)
                    pfw = "FAIL";
            }
            else
            {
                if (ci < limitMax && ci >= CiConstraint)
                    pfw = "WARN";
                else if (ci < limitMax)
                    pfw = "FAIL";
            }
            writer.WriteElementString("PFW", pfw);
            writer.WriteEndElement(); // </Result>
            writer.WriteEndElement(); // </Evaluate>

            writer.WriteEndElement(); // </PQM>

        }
Can you please let me know what I did wrong? Thank you!

-Yixiang
Oct 19, 2016 at 11:48 PM
Hi Yixiang,

I would verify that it is finding the Body structure and the Volume lookup is returning a value and not NaN.
double volume = plan.GetVolumeAtDose( body, dv, vp );
When I run the code I get a value returned:

Image

-Kurt
Oct 20, 2016 at 2:42 PM
Hi Kurt,

You are absolutely right!!! It was the Body structure! It's so tricky. In Eclipse you can see the structure type was "BODY" but in the script, you have to find "EXTERNAL".

THANK YOU so much!

By the way, how did you get rid of the unit for the Volume Ratio?

Sincerely
Yixiang
Oct 20, 2016 at 10:24 PM
Hi Kurt,

One last question, hopefully: in the effort of making the conformity constraint a variable of the PTV volume, I added the two variables: ciCons, and limitMax and declared them in the struct ConformityIndex. However, I have this error message saying that these two fields are not fully assigned. The message reads like this:
"Field 'ConformityIndex.ciCons' must be fully assigned before control is returned to the caller" and the same message for "limitMax".
The code I am using is included as following. Can you please tell me where did I use it wrong? Thank you!

Yixiang
    struct ConformityIndex : PlanQualityMetric
    {
        public ConformityValue[] CiConstraints;
        public double upperLimit;
        public VolumePresentation vp;
        public DoseValue dv;
        public string name;
        PQMUtilities.LimitType lt;
        double ciCons, limitMax;

        public ConformityIndex(string name_, DoseValue doseValue, ConformityValue[] ciConstraints, 
            double upperLimitTol,
            VolumePresentation vpp = VolumePresentation.AbsoluteCm3,
            PQMUtilities.LimitType lt_ = PQMUtilities.LimitType.upper)
        {
            CiConstraints = ciConstraints;
            upperLimit = upperLimitTol;
            vp = vpp;
            dv = doseValue;
            name = name_;
            lt = lt_;
        }

        public void addPQMInfoCi(PlanningItem plan, Structure organ, Structure body, 
                                                    XmlWriter writer)
        {
            //
            // implement writing
            //
            double volume = plan.GetVolumeAtDose( body, dv, vp );
            double organVolume = organ.Volume;
            double ci = volume/organVolume;
            // Search for volume in table
            foreach (var conformityValue in CiConstraints)
            {
                if (organVolume < conformityValue.Volume)
                {
                    // interpolate to find ideal and limit constraint
                    ciCons = conformityValue.RatioIdeal;
                    limitMax = conformityValue.RatioLimit;
                }
            }

            writer.WriteStartElement("PQM");
            writer.WriteAttributeString("type", "VolumeRatio");
            writer.WriteAttributeString("name", name);

            writer.WriteStartElement("DoseValue");
            writer.WriteAttributeString("units", dv.UnitAsString);
            writer.WriteString(dv.ValueAsString);
            writer.WriteEndElement(); // </DoseValue>

            writer.WriteStartElement("Volume");
            writer.WriteAttributeString("units", "");
            writer.WriteAttributeString("calculated", true.ToString());
            writer.WriteString(ci.ToString("0.0"));
            writer.WriteEndElement(); // </Volume>

            string limType = lt.ToString(), tolType;
            if (lt == PQMUtilities.LimitType.upper)
            {
                tolType = "leq";
            }
            else
            {
                tolType = "geq";
            }
            writer.WriteStartElement("Evaluate");

            writer.WriteStartElement("Limit");
            writer.WriteAttributeString("type", limType);
            writer.WriteString(ciCons.ToString("0.0"));
            writer.WriteEndElement(); // </Limit>

            writer.WriteStartElement("Tolerance");
            writer.WriteAttributeString("type", tolType);
            writer.WriteString(upperLimit.ToString("0.0"));
            writer.WriteEndElement(); // </Tolerance>

            writer.WriteStartElement("Result");
            writer.WriteElementString("MaxLimit", limitMax.ToString("0.0"));
            string pfw = "PASS";
            if (lt == PQMUtilities.LimitType.upper)
            {
                if (ci > ciCons && ci <= limitMax)
                    pfw = "WARN";
                else if (ci > limitMax)
                    pfw = "FAIL";
            }
            else
            {
                if (ci < limitMax && ci >= ciCons)
                    pfw = "WARN";
                else if (ci < limitMax)
                    pfw = "FAIL";
            }
            writer.WriteElementString("PFW", pfw);
            writer.WriteEndElement(); // </Result>
            writer.WriteEndElement(); // </Evaluate>

            writer.WriteEndElement(); // </PQM>

        }
Developer
Oct 20, 2016 at 10:41 PM
Edited Oct 20, 2016 at 10:41 PM
Just assign any values to them in the constructor
 public ConformityIndex(string name_, DoseValue doseValue, ConformityValue[] ciConstraints, 
            double upperLimitTol,
            VolumePresentation vpp = VolumePresentation.AbsoluteCm3,
            PQMUtilities.LimitType lt_ = PQMUtilities.LimitType.upper)
        {
            CiConstraints = ciConstraints;
            upperLimit = upperLimitTol;
            vp = vpp;
            dv = doseValue;
            name = name_;
            lt = lt_;


            ciCons = 0;
            limitMax=0;
        }
Developer
Oct 20, 2016 at 10:45 PM
Not sure if it helps, this is my implementation of CI
     public CI_Limit(string desc, double normalValue, double MinVarValue, long InstanceId, string requestedType,
            string limitType, string ptvId)
        {
            lt = PQMUtilities.ResolveLimitType(limitType);
            idealValueConstraint = normalValue;
            name = desc;
            minorVariationConstraint = MinVarValue;
            testSummary = new TestSummary();
            testSummary.MetricInstanceId = InstanceId;
            requestedMeasure = requestedType;
            ptvName = ptvId;
        }

        public void addPQMInfo(PlanningItem plan, Structure organ, XmlWriter writer)
        {


            string temp = ptvName;

            double CI = -999;
            double PTV_Volume = 1;
            double RI_Volume = 1;
            Structure ptv = (from ss in (plan as PlanSetup).StructureSet.Structures
                             where (ss.Id.Equals(temp) && ss.DicomType == "PTV")
                             select ss).FirstOrDefault();
            if (ptv == null)
            {
                MessageBox.Show("Error calculating CI_Limit : " + name
                    + "\nCould not obtain PTV structure details."
                    + "\nMake sure the PTV is set as the target volume, and has dicom type=PTV."
                    +"\n\nSkipping metric calculation.");
                return;
            }
            PTV_Volume = ptv.Volume;


            Structure RI = organ; // this is the TPD_Isodose structure
            RI_Volume = RI.Volume;
            if (PTV_Volume != 0)
                CI = RI_Volume / PTV_Volume;
            else
                CI = -1;


            string type = requestedMeasure;
            double value = 0;

            value = CI;

            string units = "Ratio";


            {
                XElement pqm = new XElement("PQM",
                                            new XAttribute("type", type),
                                            new XAttribute("name", name),
                                            PQMUtilities.getEudXML(value, units),
                                            PQMUtilities.getEvaluateXMLDouble(value, idealValueConstraint, 
                                            minorVariationConstraint, lt));
                string pfw = PQMUtilities.EvaluateResult(value, idealValueConstraint, minorVariationConstraint, lt);

                pqm.WriteTo(writer);
                testSummary.ActualValue = value;
                testSummary.Result = pfw;
            }


        }
Oct 20, 2016 at 11:44 PM
Yes the constructor assignment should it.

I would still recommend using the DVH volume lookup to determine the isodose volume.
double volume = plan.GetVolumeAtDose( body, dv, vp );
Converting an isodose level to a structure can lead to inaccuracies due to the conversion process and report a CI that is lower than expected.

http://variandeveloper.codeplex.com/discussions/570163

Image
Oct 21, 2016 at 3:08 PM
Thank you, shashankthebest and Kurt! That worked.
Nov 4, 2016 at 5:39 PM
What can you do when you have the Body structure with <100% dose coverage? In my system there are some cases (maybe only with thin CT slices), the way the Body and dose grid are arranged it can come back as NaN also.

Thanks!
Nov 30, 2016 at 3:50 PM
Thanks for this thread. Very helpful.
I've also used this to implement V50% and Conformity Index in RTOG 0813.
Has anyone figured out how to remove the units from reporting as 'cc' for the Volume Ratio?

Thanks
Jan 9 at 6:31 AM
Dear all
thank you for the detailed desciption
I Am getting the following error

Severity Code Description Project File Line Suppression State
Error CS1503 Argument 3: cannot convert from 'UserDefinedMetrics.NonHypoFractionated.ConformityValue[]' to 'VMS.TPS.ConformityIndex.ConformityValue[]' PlanQualityMetrics C:\Users\admin.ONCOLOGY\Desktop\test\Eclipse Scripting API\projects\PlanQualityMetrics\UserDefinedMetrics.cs 73 Active

could someone please help me in this
thank you