def fit_objectives(plan=None, beamset=None): # Function which adjusts DVH and EUD values in objectives to reflect obtained values. if plan is None: plan = lib.get_current_plan() if beamset is None: beamset = lib.get_current_beamset() for objective in plan.PlanOptimizations[beamset.Number - 1].Objective.ConstituentFunctions: try: f_type = objective.DoseFunctionParameters.FunctionType # Dose falloff objectives do not have a FunctionType and must be skipped except: continue if f_type == "MaxDvh": roi_name = objective.ForRegionOfInterest.Name dose_level = objective.DoseFunctionParameters.DoseLevel target_vol = objective.DoseFunctionParameters.PercentVolume obtained_vol = plan.TreatmentCourse.TotalDose.GetRelativeVolumeAtDoseValues(RoiName=roi_name, DoseValues=[dose_level]) if obtained_vol[0] >= 0.02: objective.DoseFunctionParameters.PercentVolume = int(obtained_vol[0] * 100 - 2) elif f_type == "MaxEud": roi_name = objective.ForRegionOfInterest.Name dose_level = objective.DoseFunctionParameters.DoseLevel a = objective.DoseFunctionParameters.EudParameterA obtained_eud = compute_eud(plan.TreatmentCourse.TotalDose, roi_name, a) if obtained_eud > 200: objective.DoseFunctionParameters.DoseLevel = int(obtained_eud - 200)
def _add_objective(roi_name, obj_type, is_constraint=False, plan=None, plan_opt=0): """ Private function used to add an optimization objective. Args: roi_name (str): the name of the ROI for which to add the optimization objective obj_type (str): the type of the optimization objective is_constraint (bool): whether or not the objective is a constraint plan (RayStation plan object, optional): the patient plan. If not specified, will get the currently selected one. plan_opt (int): index of the PlanOptimization to use - allows for the addition of objectives to beamsets other than the first (note that this value can be obtained by subtracting 1 from beamset.Number) Returns: the RayStation objective object """ if plan is None: plan = lib.get_current_plan() try: with CompositeAction('Add Optimization Function'): retval_1 = plan.PlanOptimizations[plan_opt].AddOptimizationFunction(FunctionType=obj_type, RoiName=roi_name, IsConstraint=is_constraint, RestrictAllBeamsIndividually=False, RestrictToBeam=None, IsRobust=False, RestrictToBeamSet=None) logger.info('Objective of type %s for ROI "%s" added.', obj_type, roi_name) except SystemError as e: if str(e).startswith('No ROI named'): logger.warning('Warning: no ROI named "%s". Skip adding objective.', roi_name) return None else: logger.exception(e) raise return retval_1
def double_opt_extra(): plan = lib.get_current_plan() beamset = lib.get_current_beamset() plan.PlanOptimizations[0].OptimizationParameters.Algorithm.MaxNumberOfIterations = 100 plan.PlanOptimizations[0].RunOptimization() plan.PlanOptimizations[0].RunOptimization() fit_objectives_orl(plan, beamset) plan.PlanOptimizations[0].OptimizationParameters.Algorithm.MaxNumberOfIterations = 29 plan.PlanOptimizations[0].RunOptimization()
def optimization_90_30(plan=None, beamset=None): patient = lib.get_current_patient() if plan is None: plan = lib.get_current_plan() if beamset is None: beamset = lib.get_current_beamset() opt = plan.PlanOptimizations[beamset.Number - 1] set_optimization_parameters(plan=plan,fluence_iterations=30, max_iterations=90, compute_intermediate_dose=False) opt.RunOptimization() set_optimization_parameters(plan=plan,fluence_iterations=30, max_iterations=30, compute_intermediate_dose=False) opt.RunOptimization()
def double_optimization(plan=None, beamset=None): # Function which runs two consecutive optimizations. patient = lib.get_current_patient() if plan is None: plan = lib.get_current_plan() if beamset is None: beamset = lib.get_current_beamset() # Beamsets are numbered starting at 1 whereas Plan Optimizations are numbered starting at 0. # Determine what the number of the selected beamset is, then subtract one to locate the associated Plan Optimization. plan.PlanOptimizations[beamset.Number - 1].RunOptimization() plan.PlanOptimizations[beamset.Number - 1].RunOptimization()
def display_external_roi(): # WIP, do not use yet patient = lib.get_current_patient() plan = lib.get_current_plan() for roi in patient.PatientModel.StructureSets[exam.Name].RoiGeometries: if roi.OfRoi.Type == "External": external_roi = roi.OfRoi.Name try: ui = get_current("ui") ui.MenuItem[1].Button_PatientModeling.Click() except: pass
def double_opt_save(plan=None, beamset=None): # Function which runs two consecutive optimizations and saves the plan afterwards (NB this removes the ability to undo the optimizations). patient = lib.get_current_patient() if plan is None: plan = lib.get_current_plan() if beamset is None: beamset = lib.get_current_beamset() # Beamset.Number starts at 1 whereas Plan Optimizations are numbered starting at 0. # Determine what the number of the selected beamset is, then subtract one to locate the associated Plan Optimization. plan.PlanOptimizations[beamset.Number - 1].RunOptimization() plan.PlanOptimizations[beamset.Number - 1].RunOptimization() patient.Save()
def fit_objectives_orl(plan=None, beamset=None): # Function which adjusts DVH and EUD values in objectives to reflect obtained values. patient = lib.get_current_patient() if plan is None: plan = lib.get_current_plan() if beamset is None: beamset = lib.get_current_beamset() for objective in plan.PlanOptimizations[beamset.Number - 1].Objective.ConstituentFunctions: try: f_type = objective.DoseFunctionParameters.FunctionType # Dose falloff objectives do not have a FunctionType and must be skipped except: continue roi_name = objective.ForRegionOfInterest.Name if roi_name in ['BLOC MOELLE','MOELLE','prv5mmMOELLE']: continue else: if f_type == "MaxDvh": dose_level = objective.DoseFunctionParameters.DoseLevel obtained_vol = plan.TreatmentCourse.TotalDose.GetRelativeVolumeAtDoseValues(RoiName=roi_name, DoseValues=[dose_level]) if obtained_vol[0] >= 0.02: objective.DoseFunctionParameters.PercentVolume = int(obtained_vol[0] * 90) if obtained_vol[0] < 0.02: objective.DoseFunctionParameters.DoseLevel = int(dose_level*0.9) elif f_type == "MaxEud": dose_level = objective.DoseFunctionParameters.DoseLevel a = objective.DoseFunctionParameters.EudParameterA obtained_eud = compute_eud(plan.TreatmentCourse.TotalDose, roi_name, a) if obtained_eud > 100: objective.DoseFunctionParameters.DoseLevel = round(obtained_eud*0.99,-1) elif f_type == "MaxDose": dose_level = objective.DoseFunctionParameters.DoseLevel vol_roi=patient.PatientModel.StructureSets['CT 1'].RoiGeometries[roi_name].GetRoiVolume() if vol_roi >= 0.1: vol_cc=0.1/vol_roi max_dose_obtenue = plan.TreatmentCourse.TotalDose.GetDoseAtRelativeVolumes(RoiName=roi_name, RelativeVolumes=[vol_cc]) else: max_dose_obtenue = plan.TreatmentCourse.TotalDose.GetDoseAtRelativeVolumes(RoiName=roi_name, RelativeVolumes=[vol_roi]) if max_dose_obtenue[0] < (dose_level/1.05) and max_dose_obtenue[0] > 500: objective.DoseFunctionParameters.DoseLevel = int(max_dose_obtenue[0]*1.05) elif max_dose_obtenue[0] < (dose_level/1.05) and max_dose_obtenue[0] <= 500: objective.DoseFunctionParameters.DoseLevel = 500
def add_mindose_objective(roi_name, dose, weight=1.0, plan=None, plan_opt=0): """ Adds a min dose objective to a ROI. Args: roi_name (str): the name of the ROI dose (float): the dose value in cGy weight (float, optional): the weight of the objective (default 1.0) plan (RayStation plan object, optional): the patient plan. If not specified, will get the currently selected one. """ if plan is None: plan = lib.get_current_plan() r = _add_objective(roi_name, 'MinDose', plan=plan, plan_opt=plan_opt) if r: r.DoseFunctionParameters.DoseLevel = dose if weight != 1.0: r.DoseFunctionParameters.Weight = weight
def compute_qa_plan_dose(compute_beam_doses=False, dose_algorithm='CCDose', scale_to_prescription=False, plan=None): """ Calculates the QA plan dose for QA plans associated with the currently selected plan. If the dose is already computed with the same algorithm, this function will not force recomputation. Args: compute_beam_doses (bool, optional): whether or not to compute individual beam doses (default *False*) dose_algorithm (str, optional): one onef *CCDose* (accurate) or *SVD* (fast) (default *CCDose*) scale_to_prescription (bool, optional): whether or not to scale MUs to dose prescription ? (default *False*) """ if plan is None: plan = lib.get_current_plan() for vp in plan.VerificationPlans: if vp.ForTreatmentPlan.Name == plan.Name: vp.BeamSet.ComputeDose(ComputeBeamDoses=compute_beam_doses, DoseAlgorithm=dose_algorithm, ScaleToPrescription=scale_to_prescription)
def essai_autre_technique(): #pour faire un plan qui essai une autre technique (si VMAT essai IMRT et si IMRT essai VMAT) #mais en gardant les meme objectifs d'optimisations et clinical goals patient = lib.get_current_patient() exam = lib.get_current_examination() plan = lib.get_current_plan() beamset = lib.get_current_beamset() #va chercher l'info du plan original plan_name = plan.Name nb_fx = beamset.FractionationPattern.NumberOfFractions ptv = beamset.Prescription.DosePrescriptions[0].OnStructure.Name rx_dose = beamset.Prescription.DosePrescriptions[0].DoseValue actual_technique = beamset.DeliveryTechnique patient_position = beamset.PatientPosition planner_name = plan.PlannedBy #crée un plan VMAT si le plan est IMRT if actual_technique == 'SMLC': new_plan = patient.AddNewPlan(PlanName= plan_name+' VMAT', PlannedBy=planner_name, Comment="", ExaminationName=exam.Name, AllowDuplicateNames=False) new_plan.SetDefaultDoseGrid(VoxelSize={'x': 0.2, 'y': 0.2, 'z': 0.2}) new_beamset = new_plan.AddNewBeamSet(Name='VMAT', ExaminationName=exam.Name, MachineName='BeamMod', NominalEnergy=None,Modality="Photons", TreatmentTechnique='VMAT', PatientPosition=patient_position, NumberOfFractions=nb_fx, CreateSetupBeams=False, Comment='VMAT') new_beamset.AddDosePrescriptionToRoi(RoiName=ptv, DoseVolume=99, PrescriptionType="DoseAtVolume", DoseValue=rx_dose, RelativePrescriptionLevel=1) new_beams = beams.add_beams_brain_stereo(beamset=new_beamset) set_optimization_parameters(plan=new_plan) set_vmat_conversion_parameters(max_arc_delivery_time=350.0, plan=new_plan) #crée un plan IMRT si le plan est VMAT else: new_plan = patient.AddNewPlan(PlanName=plan_name+' IMRT', PlannedBy=planner_name, Comment="", ExaminationName=exam.Name, AllowDuplicateNames=False) new_plan.SetDefaultDoseGrid(VoxelSize={'x': 0.2, 'y': 0.2, 'z': 0.2}) new_beamset = new_plan.AddNewBeamSet(Name='IMRT', ExaminationName=exam.Name, MachineName='BeamMod', NominalEnergy=None,Modality="Photons", TreatmentTechnique='SMLC', PatientPosition=patient_position, NumberOfFractions=nb_fx, CreateSetupBeams=False, Comment='VMAT') new_beamset.AddDosePrescriptionToRoi(RoiName=ptv, DoseVolume=99, PrescriptionType="DoseAtVolume", DoseValue=rx_dose, RelativePrescriptionLevel=1) new_beams = beams.add_beams_brain_static(beamset=new_beamset) new_plan.PlanOptimizations[0].OptimizationParameters.Algorithm.OptimalityTolerance = 1E-10 new_plan.PlanOptimizations[0].OptimizationParameters.Algorithm.MaxNumberOfIterations = 100 new_plan.PlanOptimizations[0].OptimizationParameters.DoseCalculation.IterationsInPreparationsPhase = 60 new_plan.PlanOptimizations[0].OptimizationParameters.DoseCalculation.ComputeFinalDose = True new_plan.PlanOptimizations[0].OptimizationParameters.SegmentConversion.MinSegmentMUPerFraction = 20 new_plan.PlanOptimizations[0].OptimizationParameters.SegmentConversion.MinLeafEndSeparation = 1 new_plan.PlanOptimizations[0].OptimizationParameters.SegmentConversion.MinNumberOfOpenLeafPairs = 3 new_plan.PlanOptimizations[0].OptimizationParameters.SegmentConversion.MinSegmentArea = 2 new_plan.PlanOptimizations[0].OptimizationParameters.SegmentConversion.MaxNumberOfSegments = 40 #lit les objectifs d'optimisation du plan original et en crée des pareils dans le nouveau plan for objective in plan.PlanOptimizations[beamset.Number-1].Objective.ConstituentFunctions: nom_roi = objective.ForRegionOfInterest.Name nweight = objective.DoseFunctionParameters.Weight if hasattr(objective.DoseFunctionParameters, 'FunctionType'): #les critères dose fall off n'ont pas de FunctionType, les autres en ont tous un type = objective.DoseFunctionParameters.FunctionType if type in ('MinDose','MaxDose','MinDvh','MaxDvh','UniformDose') : pourcent = objective.DoseFunctionParameters.PercentVolume dose_level = objective.DoseFunctionParameters.DoseLevel new_objective = new_plan.PlanOptimizations[new_beamset.Number-1].AddOptimizationFunction(FunctionType=type, RoiName=nom_roi, IsConstraint=False,RestrictAllBeamsIndividually=False, RestrictToBeam=None, IsRobust=False, RestrictToBeamSet=None) new_objective.DoseFunctionParameters.DoseLevel = dose_level new_objective.DoseFunctionParameters.Weight = nweight new_objective.DoseFunctionParameters.PercentVolume = pourcent if type in ('MinEud','MaxEud'): dose_level = objective.DoseFunctionParameters.DoseLevel parametre_a = objective.DoseFunctionParameters.EudParameterA new_objective = new_plan.PlanOptimizations[new_beamset.Number-1].AddOptimizationFunction(FunctionType=type, RoiName=nom_roi, IsConstraint=False,RestrictAllBeamsIndividually=False, RestrictToBeam=None, IsRobust=False, RestrictToBeamSet=None) new_objective.DoseFunctionParameters.DoseLevel = dose_level new_objective.DoseFunctionParameters.Weight = nweight new_objective.DoseFunctionParameters.EudParameterA = parametre_a else: # si pas de FunctionType on suppose que c'est un dose fall off high_dose_level = objective.DoseFunctionParameters.HighDoseLevel low_dose_level = objective.DoseFunctionParameters.LowDoseLevel low_dose_distance = objective.DoseFunctionParameters.LowDoseDistance new_objective = new_plan.PlanOptimizations[new_beamset.Number-1].AddOptimizationFunction(FunctionType='DoseFallOff', RoiName=nom_roi, IsConstraint=False,RestrictAllBeamsIndividually=False, RestrictToBeam=None, IsRobust=False, RestrictToBeamSet=None) new_objective.DoseFunctionParameters.HighDoseLevel = high_dose_level new_objective.DoseFunctionParameters.LowDoseLevel = low_dose_level new_objective.DoseFunctionParameters.LowDoseDistance = low_dose_distance new_objective.DoseFunctionParameters.Weight = nweight for goal in plan.TreatmentCourse.EvaluationSetup.EvaluationFunctions: nom_roi = goal.ForRegionOfInterest.Name goal_criteria = goal.PlanningGoal.GoalCriteria goal_type = goal.PlanningGoal.Type acceptance_level = goal.PlanningGoal.AcceptanceLevel parameter_value = goal.PlanningGoal.ParameterValue new_goal = new_plan.TreatmentCourse.EvaluationSetup.AddClinicalGoal(RoiName=nom_roi, GoalCriteria=goal_criteria, GoalType=goal_type, AcceptanceLevel=acceptance_level, ParameterValue=parameter_value, IsComparativeGoal=False) # Optimize plan twice new_plan.PlanOptimizations[new_beamset.Number-1].RunOptimization() new_plan.PlanOptimizations[new_beamset.Number-1].RunOptimization()
def set_vmat_conversion_parameters(max_leaf_travel_per_degree=0.1, constrain_leaf_motion=True, arc_gantry_spacing=4, max_arc_delivery_time=350.0, plan=None, beamset=None): """ Sets the VMAT conversion parameters. **RESETS OPTIMIZATION IF ALREADY STARTED** Args: max_leaf_travel_per_degree (float, optional): maximum distance a leaf can move in cm per degree of gantry movement (default: 0.1 cm/degree) constrain_leaf_motion (bool, optional): whether or not to impose the *max_leaf_travel_per_degree* constraint (default: True) arc_gantry_spacing (int, optional): the spacing in degrees between control points of the arc (default: 4 degrees) max_arc_delivery_time (float, optional): the maximum are delivery time in seconds (defautl 350.0 s) plan (RS object, optional): the RayStation plan object (if none specified, will get current) beamset (str or int, optional): the name of the beamset to modify. If not used, all beamsets in the plan are affected. An int can be provided as well (1 for first beamset, 2 for second beamset, etc.). """ if plan is None: plan = lib.get_current_plan() if beamset is None: logger.info('No beamset specified. All beamsets will be modified.') l = 0 for po in plan.PlanOptimizations: l += 1 indices = range(l) elif isinstance(beamset, str): # figure out what index of PlanOptimizations is this beamset i = 0 for po in plan.PlanOptimizations: for obs in po.OptimizedBeamSets: if beamset.startswith(obs.DicomPlanLabel): break break i += 1 indices = [i] elif isinstance(beamset, int): try: indices = [beamset - 1] test = plan.PlanOptimizations[beamset - 1] except IndexError as e: lib.error('The index provided for the beamset argument was out of range. Specify the beamset number in the plan (i.e. 1, 2, ...) or provide the beamset name as a string.') if arc_gantry_spacing not in [2, 3, 4]: lib.error('The arc gantry spacing must be of 2, 3 or 4 degrees.') try: for i in indices: if plan.PlanOptimizations[i].ProgressOfOptimization is not None: # Reset optimization because this changes parameters that RS doesn't # allow changing without resetting. plan.PlanOptimizations[i].ResetOptimization() logger.info('Optimization reset.') po = plan.PlanOptimizations[i] logger.info('PlanOptimizations[%s] selected.', i) po.OptimizationParameters.SegmentConversion.ArcConversionProperties.MaxLeafTravelDistancePerDegree = max_leaf_travel_per_degree logger.info('Max leaf travel per degree set to %s cm/deg.', max_leaf_travel_per_degree) po.OptimizationParameters.SegmentConversion.ArcConversionProperties.UseMaxLeafTravelDistancePerDegree = constrain_leaf_motion logger.info('Constrain leaf motion set to %s.', constrain_leaf_motion) for ts in po.OptimizationParameters.TreatmentSetupSettings: for bs in ts.BeamSettings: bs.ArcConversionPropertiesPerBeam.FinalArcGantrySpacing = arc_gantry_spacing logger.info('Arc gantry spacing set to %s degrees for arc "%s".', arc_gantry_spacing, bs.ForBeam.Name) bs.ArcConversionPropertiesPerBeam.MaxArcDeliveryTime = max_arc_delivery_time logger.info('Max arc delivery time set to %s s for arc "%s".', max_arc_delivery_time, bs.ForBeam.Name) except Exception as e: logger.exception(e) raise
def set_optimization_parameters(fluence_iterations=60, max_iterations=100, optimality_tolerance=1E-09, compute_intermediate_dose=True, compute_final_dose=True, plan=None, beamset=None): """ Sets the optimization parameters for inverse planning. **RESETS OPTIMIZATION IF ALREADY STARTED** Args: fluence_iterations (int, optional): the number of fluence iterations (default: 60) max_iterations (int, optional): the maximum number of iterations (default: 100) optimality_tolerance (float, optional): the tolerance of the optimizer such that it has reached an optimal solution with respect to the objective value (default: 1E-9) compute_intermediate_dose (bool, optional): whether of not to compute clinical dose after completing fluence iterations. This dose is taken into account in the following optimization iterations (default: True). compute_final_dose (bool, optional): whether or not to compute clinical dose after optimization is done (default: True) plan (RS Object, optional): the RayStation plan object (if none specified, will get current) beamset (str or int, optional): the name of the beamset to modify. If not used, all beamsets in the plan are affected. An int can be provided as well (1 for first beamset, 2 for second beamset, etc.). """ if plan is None: plan = lib.get_current_plan() if fluence_iterations > max_iterations: lib.error('Fluence iterations cannot be greater than the maximum number of iterations.') if beamset is None: logger.info('No beamset specified. All beamsets will be modified.') l = 0 for po in plan.PlanOptimizations: l += 1 indices = range(l) elif isinstance(beamset, str): # figure out what index of PlanOptimizations is this beamset i = 0 for po in plan.PlanOptimizations: for obs in po.OptimizedBeamSets: if beamset.startswith(obs.DicomPlanLabel): break break i += 1 indices = [i] elif isinstance(beamset, int): try: indices = [beamset - 1] test = plan.PlanOptimizations[beamset - 1] except IndexError as e: lib.error('The index provided for the beamset argument was out of range. Specify the beamset number in the plan (i.e. 1, 2, ...) or provide the beamset name as a string.') # TODO Ask confirmation with yes/no dialog before proceeding. try: for i in indices: #if not plan.PlanOptimizations[i].ProgressOfOptimization is None: # Reset optimization because this changes parameters that RS doesn't # allow changing without resetting. #plan.PlanOptimizations[i].ResetOptimization() #logger.info('Optimization was reset.') logger.info('PlanOptimizations[%s] selected.', i) op = plan.PlanOptimizations[i].OptimizationParameters op.DoseCalculation.IterationsInPreparationsPhase = fluence_iterations logger.info('Fluence iterations set to %s.', fluence_iterations) op.Algorithm.MaxNumberOfIterations = max_iterations logger.info('Max iterations set to %s.', max_iterations) op.Algorithm.OptimalityTolerance = optimality_tolerance logger.info('Optimality tolerance set to %s.', optimality_tolerance) op.DoseCalculation.ComputeIntermediateDose = compute_intermediate_dose logger.info('Compute intermediate dose set to %s.', compute_intermediate_dose) op.DoseCalculation.ComputeFinalDose = compute_final_dose logger.info('Compute final dose set to %s.', compute_final_dose) except Exception as e: logger.exception(e) raise
def create_ac_qa_plans(plan=None, phantom_name='ARCCHECK', iso_name='ISO AC'): ui = get_current("ui") patient = lib.get_current_patient() if plan is None: plan = lib.get_current_plan() # Change dose color table reference value to dose of one fraction rx_dose = plan.BeamSets[0].Prescription.DosePrescriptions[0].DoseValue nb_fx = plan.BeamSets[0].FractionationPattern.NumberOfFractions patient.CaseSettings.DoseColorMap.ReferenceValue = rx_dose * 1.1 / nb_fx # Increase ref dose by 10% because small size of ArcCheck typically leads to higher max dose in phantom than in patient # RayStation asks you to save the plan before continuing. This is done automatically, because errors can occur if the user doesn't respond promptly and the script continues. patient.Save() # Create QA plan (for each beamset) - NOPE just this one #for bs in plan.BeamSets: bs = lib.get_current_beamset() bs_name = bs.DicomPlanLabel if len(bs_name) > 6: # To prevent crashes if resulting name will be > 16 characters long bs_name = bs.DicomPlanLabel[:6] if iso_name =='ISO AC': name = 'QA ' + patient.PatientName[:4] + ' ' + bs_name else: name = 'PD ' + patient.PatientName[:4] + ' ' + bs_name # Open the QA Preparation tab if lib.check_version(4.7): ui.MenuItem[6].Button_QAPreparation.Click() elif lib.check_version(4.6): ui.MenuItem[8].Button_QAPreparation.Click() # Click button "New QA plan" ui.TabControl_ToolBar.ToolBarGroup.__0.Button_NewQAPlan.Click() # Enter a name in the plan name text box ui.TextBox_VerificationPlanName.Text = name # Open the phantom list dropdown and selects phantom ui.ComboBox_PhantomList.ToggleButton.Click() ui.ComboBox_PhantomList.Popup.ComboBoxItem[phantom_name].Select() # ui.ComboBox_PhantomList.Popup.ComboBoxItem['QAVMAT ARCCHECK_2016'].Select() #Phantom for use in Dev database # Check the box to compute dose after plan is created ui.CheckBox._Compute_dose_when_the_QA_plan_is_created.Click() # Select POI position for isocenter, open dropdown menu and select ISO AC ui.RayRadionButton._1.Click() ui.ComboBox_PointsOfInterests.ToggleButton.Click() ui.ComboBox_PointsOfInterests.Popup.ComboBoxItem[iso_name].Select() # Marks checkbox to use uniform resolution and set to 0.2cm ui.CheckBox._Use_uniform_resolution.Click() ui.TextBox_DoseGridResolutionPresentationX.Text = ".2" # Collapse all angles couch, gantry and coll to 0 degrees if iso_name == '2cm EPID'or iso_name == '4.8cm MapCheck IMF': ui.RayRadionButton._4.Click() ui.RayRadionButton._6.Click() ui.RayRadionButton._8.Click() ui.Button_OK.Click() #si c'est des Mapcheck if iso_name == '2cm EPID' or iso_name == '4.8cm MapCheck IMF': ui.TabControl_ToolBar.ToolBarGroup._DATA_EXPORT.Button_ExportPlan___.Click() # Unselect RT Dose for each beam, RT structures and CT Image ui.QADicomExportDialogContent.CheckBox[3].Click() ui.QADicomExportDialogContent.CheckBox[4].Click() # Export plan, total dose and beam doses to Maggie ui.QADicomExportDialogContent.PropertyRow[1].RadioButton.Click() # Select DICOM Store for export ui.QADicomExportDialogContent.PropertyRow[1].ComboBox.ToggleButton.Click() ui.QADicomExportDialogContent.PropertyRow[1].ComboBox.Popup.ComboBoxItem['MAGGIE [11.1.7.5]'].Select() ui.QADicomExportDialogContent.PropertyRow[1].ComboBox.ToggleButton.Click() #to actually click OK for export ui.QADicomExportDialogContent.Button['Export'].Click() #message.message_window('Les fichiers des plannar doses sont maintenant dans MAGGIE') # Open the Export QA plan else: ui.TabControl_ToolBar.ToolBarGroup._DATA_EXPORT.Button_ExportPlan___.Click() # Unselect RT Dose for each beam, RT structures and CT Image ui.QADicomExportDialogContent.CheckBox[2].Click() ui.QADicomExportDialogContent.CheckBox[3].Click() ui.QADicomExportDialogContent.CheckBox[4].Click() # Export plan, total dose and beam doses to Maggie ui.QADicomExportDialogContent.PropertyRow[1].RadioButton.Click() # Select DICOM Store for export ui.QADicomExportDialogContent.PropertyRow[1].ComboBox.ToggleButton.Click() ui.QADicomExportDialogContent.PropertyRow[1].ComboBox.Popup.ComboBoxItem['MAGGIE [11.1.7.5]'].Select() ui.QADicomExportDialogContent.PropertyRow[1].ComboBox.ToggleButton.Click() #to actually click OK for export ui.QADicomExportDialogContent.Button['Export'].Click()
def preparation_qa(): #QA preparation script for dosimetrists # - Creates QA plan for the currently selected beamset on the ArcCheck phantom # - Notes dose to MicroLion # - Notes max dose and its location # - Shifts the isocenter to put the point of max dose on the MicroLion (most of the time) # - Recomputes dose and then exports dose to MicroLion before and after shift, along with instructions for the shift patient = lib.get_current_patient() plan = lib.get_current_plan() beamset = lib.get_current_beamset() output = "Patient: %s\nPlan: %s\nBeamset: %s\n" % (patient.PatientName.replace("^", ", "),plan.Name,beamset.DicomPlanLabel) #Create QA plan centered on ISO AC create_ac_qa_plans() ui = get_current("ui") ui.MessageBoxWindowContent.Button['OK'].Click() #Clears away dialogue after exporting DICOM files (or failing to) vp = find_current_verification_plan(plan,beamset) output += "Plan QA: " + vp.BeamSet.DicomPlanLabel + '\n' #Find coordinates of current isocenter (rounded to a tenth of a millimeter) iso_x = round(vp.BeamSet.Beams[0].PatientToBeamMapping.IsocenterPoint.x, 2) iso_y = round(vp.BeamSet.Beams[0].PatientToBeamMapping.IsocenterPoint.y, 2) iso_z = round(vp.BeamSet.Beams[0].PatientToBeamMapping.IsocenterPoint.z, 2) #Get total dose at isocenter as well as each beam's contribution isocenter_rsp = lib.RSPoint(iso_x, iso_y, iso_z) isocenter_dose = vp.BeamSet.FractionDose.InterpolateDoseInPoint(Point=isocenter_rsp.value) output += "\nPLAN CENTRÉ\nDose à l'isocentre: %.2fGy" % (isocenter_dose/100.0) for beamdose in vp.BeamSet.FractionDose.BeamDoses: dose_per_beam = beamdose.InterpolateDoseInPoint(Point=isocenter_rsp.value) output += "\n %s : %.2fGy" % (beamdose.ForBeam.Name, dose_per_beam / 100.0) #Determine location of max dose point and the dose it receives fraction_dose = vp.BeamSet.FractionDose grid = vp.DoseGrid max_dose,max_coords = poi.get_max_dose_coordinates(fraction_dose,grid) #Create a DSP at this point vp.BeamSet.CreateDoseSpecificationPoint(Name="DSP", Coordinates={ 'x': max_coords.x, 'y': max_coords.y, 'z': max_coords.z }) # Formula for new isocenter: 2*(iso coordinate) - dose max coordinate (rounded) new_iso = lib.RSPoint(0, 0, 0) new_iso.x = 2 * iso_x - round(max_coords.x, 2) new_iso.y = 2 * iso_y - round(max_coords.y, 2) new_iso.z = 2 * iso_z - round(max_coords.z, 2) # Round DSP coordinates to avoid sub-millimeter table displacements new_iso.x = round(new_iso.x, 1) + (iso_x - round(iso_x, 1)) new_iso.y = round(new_iso.y, 1) + (iso_y - round(iso_y, 1)) # If iso_y = 2.75, then you get (2.75-2.8) = -0.05 new_iso.z = round(new_iso.z, 1) + (iso_z - round(iso_z, 1)) # Eliminate displacements that are 2mm or less if abs(new_iso.x - iso_x) < 0.21: new_iso.x = iso_x if abs(new_iso.y - iso_y) < 0.21: new_iso.y = iso_y if abs(new_iso.z - iso_z) < 0.21: new_iso.z = iso_z #Set all beams to the new isocenter for beam in vp.BeamSet.Beams: beam.PatientToBeamMapping.IsocenterPoint = {'x': new_iso.x, 'y': new_iso.y, 'z': new_iso.z} #Now that we're done with our DSP, set it back to the original isocenter coordinates to get dose to MicroLion dsp = vp.BeamSet.DoseSpecificationPoints[0] dsp.Name = "ISO MicroLion" dsp.Coordinates = {'x': iso_x, 'y': iso_y, 'z': iso_z} for beam in vp.BeamSet.Beams: beam.SetDoseSpecificationPoint(Name="ISO MicroLion") # Compute dose (this is done last, because otherwise changing beam spec points erases dose) vp.BeamSet.ComputeDose(ComputeBeamDoses=True, DoseAlgorithm="CCDose", ForceRecompute=False) #Calculate shift shift_x = new_iso.x - iso_x shift_y = new_iso.y - iso_y shift_z = new_iso.z - iso_z if shift_x == 0 and shift_y == 0 and shift_z == 0: output += "\n\nL'isocentre se trouve sur le point de dose max. Aucun shift automatique a été calculé." else: if (isocenter_dose/max_dose) < 0.75: output += "\n\nLa dose à l'isocentre est inférieure à 75% de la dose maximale.\nIl est nécessaire de mesurer la dose au MicroLion avec le shift suivant:\n\n" else: output += "\n\nLa dose à l'isocentre est supérieure à 75% de la dose maximale.\nIl n'est probablement pas nécessaire de faire un shift, mais voici une possibilité de shift au cas ou:\n\n" if shift_x > 0: direction_x = 'vers A' elif shift_x < 0: direction_x = 'vers B' if shift_x != 0: output += ' LATERAL: ' + str(abs(shift_x)) + 'cm ' + direction_x + '\n' else: output += ' LATERAL: Aucun shift\n' if shift_y > 0: direction_y = 'UP' elif shift_y < 0: direction_y = 'DOWN' if shift_y != 0: output += ' HAUTEUR: ' + str(abs(shift_y)) + 'cm ' + direction_y + '\n' else: output += ' HAUTEUR: Aucun shift\n' if shift_z < 0: direction_z = 'IN' else: direction_z = 'OUT' #We have to account for 0 shift because I call on this variable later (which isn't true for the other axes) if shift_z != 0: output += ' LONGITUDINAL: ' + str(abs(shift_z)) + 'cm ' + direction_z + '\n' else: output += ' LONGITUDINAL: Aucun shift\n' if direction_z == 'IN': output += "ATTENTION: Suite au déplacement vers IN, assurez-vous que le faisceau n'irradierais pas l'électronique du ArcCheck\n" # Print dose to MicroLion with AC shift isocenter_dose = vp.BeamSet.FractionDose.InterpolateDoseInPoint(Point=isocenter_rsp.value) output += "\nPLAN AVEC SHIFT\nDose à l'isocentre: %.2fGy" % (isocenter_dose/100.0) for beamdose in vp.BeamSet.FractionDose.BeamDoses: dose_per_beam = beamdose.InterpolateDoseInPoint(Point=isocenter_rsp.value) output += "\n %s : %.2fGy" % (beamdose.ForBeam.Name, dose_per_beam / 100.0) #For IMRT plans, make a second QA plan to create EPIDStan files if vp.BeamSet.DeliveryTechnique == "SMLC": num_beams = 0 num_segments = 0 for beam in vp.BeamSet.Beams: num_beams += 1 for seg in beam.Segments: num_segments += 1 if num_segments > num_beams: #If this is an IMRT plan and not a 3DC plan if lib.check_version(4.7): phantom_name = '48x48x48 FANTOME' elif lib.check_version(4.6): phantom_name = '48x48x48 FANTOME' create_ac_qa_plans(plan=None,phantom_name=phantom_name,iso_name='2cm EPID') ui = get_current("ui") ui.MessageBoxWindowContent.Button['OK'].Click() #Clears away dialogue after exporting DICOM files (or failing to) output += "\n\nLe plan est fait en IMRT, n'oubliez pas de vérifier la dose 2D à l'aide de EPIDStan\nLes fichiers EPID se trouvent sur MAGGIE" #Print all results to text file try: patient_ID = patient.PatientID patient_name = patient.PatientName.replace("^", ", ") file_path = r'\\radonc.hmr\Departements\Physiciens\Clinique\IMRT\QA' file_path += '\\' + patient_name + '~' + patient_ID + '-TESTSUPERBRIDGE' with open(file_path + '\\QA patient ' + vp.BeamSet.DicomPlanLabel + ".txt", 'w') as dvh_file: dvh_file.write(output) except Exception as e: texte = 'Impossible de créer le fichier texte avec les résultats.\n%s\nSVP, notez les valeurs ci-dessous:\n\n' % e message.message_window(texte + output)
def shift_plans_QA(print_results=True): """ Checks all existing QA plans to see if they have a dose specification point (which should correspond to the point of dose max). If a DSP is found, the isocenter is shifted and the plan is recalculated, which should usually move the MicroLion to the point of dose max. This script should only be used if the beams are currently on the phantom isocenter point (where the MicroLion is situated). If not, then the displacements printed to file may be incorrect! """ patient = lib.get_current_patient() plan = lib.get_current_plan() beamset = lib.get_current_beamset() grid = {'x': 0.2, 'y': 0.2, 'z': 0.2} #Determine which verification plan we are currently vp = None for vpl in plan.VerificationPlans: if vpl.ForTreatmentPlan.Name == plan.Name and vpl.OfRadiationSet.DicomPlanLabel == beamset.DicomPlanLabel: vp = vpl break #If no appropriate verification plan is found, do nothing if vp == None: message.message_window('Aucun plan QA trouvé') return message.message_window('Le nom du plan QA trouvé est ' + vpl.BeamSet.DicomPlanLabel) #for vp in plan.VerificationPlans: try: p = lib.RSPoint(point=vp.BeamSet.DoseSpecificationPoints[0].Coordinates) # get coords of first DSP (if it exists) except: message.message_window('Aucun DSP trouvé dans le plan QA') return # Find coordinates of current isocenter (rounded to a tenth of a millimeter) iso_x = round(vp.BeamSet.Beams[0].PatientToBeamMapping.IsocenterPoint.x, 2) iso_y = round(vp.BeamSet.Beams[0].PatientToBeamMapping.IsocenterPoint.y, 2) iso_z = round(vp.BeamSet.Beams[0].PatientToBeamMapping.IsocenterPoint.z, 2) # Formula for new isocenter: 2*(iso coordinate) - dose max coordinate (rounded) p.x = 2 * iso_x - round(p.x, 2) p.y = 2 * iso_y - round(p.y, 2) # difference between DICOM and patient coordinate systems, y=-z p.z = 2 * iso_z - round(p.z, 2) # difference between DICOM and patient coordinate systems, z=y # Round DSP coordinates to avoid sub-millimeter table displacements p.x = round(p.x, 1) + (iso_x - round(iso_x, 1)) p.y = round(p.y, 1) + (iso_y - round(iso_y, 1)) # If iso_y = 2.75, then you get (2.75-2.8) = -0.05 p.z = round(p.z, 1) + (iso_z - round(iso_z, 1)) # Eliminate displacements that are 2mm or less if abs(p.x - iso_x) < 0.21: p.x = iso_x if abs(p.y - iso_y) < 0.21: p.y = iso_y if abs(p.z - iso_z) < 0.21: p.z = iso_z # Add SHIFT to plan name (truncate name if resulting name will be >16 characters) if len(vp.BeamSet.DicomPlanLabel) > 9: # To prevent crashes if resulting name will be > 16 characters long name = vp.BeamSet.DicomPlanLabel[:9] + ' SHIFT' else: name = vp.BeamSet.DicomPlanLabel + ' SHIFT' vp.BeamSet.DicomPlanLabel = name # Shift isocenter #plan.BeamSets[vp.OfRadiationSet.Number-1].CreateQAPlan(PhantomName='QA VMAT ARCCHECK', QAPlanName=name, IsoCenter=p.value, DoseGrid=grid, ComputeDoseWhenPlanIsCreated=True) for beam in vp.BeamSet.Beams: beam.PatientToBeamMapping.IsocenterPoint = {'x': p.x, 'y': p.y, 'z': p.z} # Move DSP to isocenter coordinates and assign as spec point for all beams try: dsp = vp.BeamSet.DoseSpecificationPoints[0] dsp.Name = "ISO MicroLion" dsp.Coordinates = {'x': iso_x, 'y': iso_y, 'z': iso_z} except IndexError as e: logger.error('You must create a dose specification point manually before executing this script.') logger.exception(e) raise except Exception as e: logger.exception(e) raise for beam in vp.BeamSet.Beams: beam.SetDoseSpecificationPoint(Name="ISO MicroLion") # Compute dose (this is done last, because otherwise changing beam spec points erases dose) vp.BeamSet.ComputeDose(ComputeBeamDoses=True, DoseAlgorithm="CCDose", ForceRecompute=False) # Write displacement to file # Get demographic information patient_ID = patient.PatientID patient_name = patient.PatientName.replace("^", ", ") if print_results is True: try: file_path = r'\\radonc.hmr\Departements\Physiciens\Clinique\IMRT\QA' file_path += '\\' + patient_name + '~' + patient_ID + '-TESTSUPERBRIDGE' # Write to file with open(file_path + '\\Déplacement ArcCheck ' + name + ".txt", 'w') as dvh_file: dvh_file.write('Patient: ' + patient_name + '\n') dvh_file.write('No. HMR: ' + patient_ID + '\n\n\n') dvh_file.write('Déplacement ArcCheck pour QA plan: ' + name + '\n\n') shift_x = p.x - iso_x if shift_x > 0: direction_x = 'vers A' elif shift_x < 0: direction_x = 'vers B' if shift_x != 0: dvh_file.write(' LATERAL: ' + str(abs(shift_x)) + 'cm ' + direction_x + '\n') else: dvh_file.write(' LATERAL: Aucun shift\n') shift_z = p.z - iso_z if shift_z > 0: direction_z = 'OUT' elif shift_z < 0: direction_z = 'IN' if shift_z != 0: dvh_file.write(' LONGITUDINAL: ' + str(abs(shift_z)) + 'cm ' + direction_z + '\n') else: dvh_file.write(' LONGITUDINAL: Aucun shift\n') shift_y = p.y - iso_y if shift_y > 0: direction_y = 'UP' elif shift_y < 0: direction_y = 'DOWN' if shift_y != 0: dvh_file.write(' HAUTEUR: ' + str(abs(shift_y)) + 'cm ' + direction_y + '\n') else: dvh_file.write(' HAUTEUR: Aucun shift\n') # Print dose to MicroLion with AC shift dsp_temp = lib.RSPoint(dsp.Coordinates.x, dsp.Coordinates.y, dsp.Coordinates.z) dsp_dose = vp.BeamSet.FractionDose.InterpolateDoseInPoint(Point=dsp_temp.value) dvh_file.write("\n\nNouveau dose au point %s : %.3fGy" % (dsp.Name, dsp_dose / 100.0)) dvh_file.write("\n\nDose par faisceau: ") for beamdose in vp.BeamSet.FractionDose.BeamDoses: dose_per_beam = beamdose.InterpolateDoseInPoint(Point=dsp_temp.value) dvh_file.write("\n %s : %.3fGy" % (beamdose.ForBeam.Name, dose_per_beam / 100.0)) except: texte = 'Impossible de créer le fichier texte avec les déplacements. SVP, notez les valeurs ci-dessous:\n\n' shift_x = p.x - iso_x if shift_x > 0: direction_x = 'vers A' elif shift_x < 0: direction_x = 'vers B' if shift_x != 0: texte += ' LATERAL: ' + str(abs(shift_x)) + 'cm ' + direction_x + '\n' else: texte += ' LATERAL: Aucun shift\n' shift_z = p.z - iso_z if shift_z > 0: direction_z = 'OUT' elif shift_z < 0: direction_z = 'IN' if shift_z != 0: texte += ' LONGITUDINAL: ' + str(abs(shift_z)) + 'cm ' + direction_z + '\n' else: texte += ' LONGITUDINAL: Aucun shift\n' shift_y = p.y - iso_y if shift_y > 0: direction_y = 'UP' elif shift_y < 0: direction_y = 'DOWN' if shift_y != 0: texte += ' HAUTEUR: ' + str(abs(shift_y)) + 'cm ' + direction_y + '\n' else: texte += ' HAUTEUR: Aucun shift\n' message.message_window(texte)